From 218d76a41cb82b872c6c7e7cb007892a290e79f8 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:37:34 +0200 Subject: [PATCH 01/29] ctest: parse rust modules in crates Add support for parsing modules in the input Rust crate to `ctest`. This should allow more easily implementing support for a number of recent feature additions that have been needed in `libc`. --- ctest/src/ast/constant.rs | 1 + ctest/src/ast/function.rs | 1 + ctest/src/ast/mod.rs | 2 + ctest/src/ast/module.rs | 19 +++++ ctest/src/ast/static_variable.rs | 1 + ctest/src/ast/structure.rs | 1 + ctest/src/ast/type_alias.rs | 1 + ctest/src/ast/union.rs | 1 + ctest/src/ffi_items.rs | 119 +++++++++++++++++++++++++++---- ctest/src/lib.rs | 1 + 10 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 ctest/src/ast/module.rs diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index b14a0db6b0ee1..710aed44e34f3 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -5,6 +5,7 @@ use crate::BoxStr; pub struct Const { pub(crate) public: bool, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) ty: syn::Type, } diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index e266a53efdbbd..c4afb491f87a2 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, diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 325e05cb40056..49b58cfb2ee35 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,6 +13,7 @@ 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; diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs new file mode 100644 index 0000000000000..ad0fa006c3411 --- /dev/null +++ b/ctest/src/ast/module.rs @@ -0,0 +1,19 @@ +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 identifier of the parsed module. + pub fn ident(&self) -> &str { + &self.ident + } +} diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index 665bd06eb8e4b..fcf45c65134f2 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -13,6 +13,7 @@ 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, } diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index 1c935890d4d23..00ef065fb4f20 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -8,6 +8,7 @@ use crate::{ pub struct Struct { pub(crate) public: bool, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) fields: Vec, } diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index 4947e934dde51..73cacb88f81c1 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -5,6 +5,7 @@ use crate::BoxStr; pub struct Type { pub(crate) public: bool, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) ty: syn::Type, } diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index 990d6db0efca5..2fd443651d410 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -8,6 +8,7 @@ use crate::{ pub struct Union { pub(crate) public: bool, pub(crate) ident: BoxStr, + pub(crate) path: syn::Path, pub(crate) fields: Vec, } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index c7198082c946a..0c020a158b013 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 @@ -165,6 +224,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 +234,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 +243,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a public, abi, ident, + path, link_name, ty, }); @@ -190,15 +252,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 +277,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 +331,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/lib.rs b/ctest/src/lib.rs index c03f9de54d524..81f945c8a2d0b 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, From 54e6897d0848be7b2f8431581cb1c84498908c97 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:32:48 +0200 Subject: [PATCH 02/29] ctest: add support for filtering modules Add support in `TranslationHelper` to filter out modules based off of a new type of skip accepted in `TestGenerator`'s public API. Filtering containing items is done by filtering on stringified paths (e.g. `foo::bar` to skip function `bar` inside top-level module `crate::foo`.) --- ctest/src/generator.rs | 31 ++++++++++++++ ctest/src/lib.rs | 7 +++ ctest/src/template.rs | 96 ++++++++++++++++++++++-------------------- 3 files changed, 88 insertions(+), 46 deletions(-) diff --git a/ctest/src/generator.rs b/ctest/src/generator.rs index 8f31af4488375..b206a4ba40604 100644 --- a/ctest/src/generator.rs +++ b/ctest/src/generator.rs @@ -26,6 +26,7 @@ use crate::{ Field, Language, MapInput, + Module, Parameter, Result, Static, @@ -470,6 +471,32 @@ impl TestGenerator { 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::ident`] 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 @@ -1184,6 +1211,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 81f945c8a2d0b..0e9da383e3bcc 100644 --- a/ctest/src/lib.rs +++ b/ctest/src/lib.rs @@ -91,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. @@ -168,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..93b8a2b6d5b2a 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -620,67 +620,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.ident()))) + }); + + for item in skipped { + if generator.verbose_skip { + eprintln!("Skipping C enum type {}", item.ident()); + } } - } - 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.ident()); + } } - } - 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.ident()) + } } - } - }}; + }}; + } + + 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. @@ -711,6 +714,7 @@ impl<'a> TranslateHelper<'a> { 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(|_| { From 60b0cdd604ecdd7c9e6f4e760749679a241fe022 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 4 Sep 2026 08:42:42 +0200 Subject: [PATCH 03/29] ctest(remove): showcase skipping items with mods --- ctest/src/template.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ctest/src/template.rs b/ctest/src/template.rs index 93b8a2b6d5b2a..ed0a657288cc7 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -730,3 +730,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.ident() == "t::r::ctime"); + let mut translator = TranslateHelper::new(&items, &generator); + println!("{:#?}", translator.filtered_ffi_items); +} From 30cf3541c5589b021b3e02a31fbc7345e2f34591 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:10:29 +0200 Subject: [PATCH 04/29] ctest: fmt comments Trim comment lines and strings to not surpass an 80-character mark from the very first character after a newline in source. This has been applied automatically through relevant unstable options in `rustfmt`. --- ctest/src/generator.rs | 176 ++++++++++++++++++++-------------------- ctest/src/template.rs | 24 ++++-- ctest/src/translator.rs | 29 ++++--- ctest/templates/test.c | 2 +- 4 files changed, 119 insertions(+), 112 deletions(-) diff --git a/ctest/src/generator.rs b/ctest/src/generator.rs index b206a4ba40604..049fb1a8a9dea 100644 --- a/ctest/src/generator.rs +++ b/ctest/src/generator.rs @@ -41,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>; @@ -50,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>; @@ -59,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, @@ -70,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, @@ -80,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, @@ -94,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, @@ -111,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}")] @@ -152,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 /// @@ -174,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, @@ -190,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); @@ -245,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()))); @@ -337,6 +349,7 @@ impl TestGenerator { /// Indicate that a type alias is actually a C enum. /// /// # Examples + /// /// ```no_run /// use ctest::TestGenerator; /// @@ -353,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| { @@ -379,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| { @@ -402,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, @@ -428,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, @@ -451,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 /// @@ -460,11 +479,10 @@ 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)); @@ -505,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| { @@ -528,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| { @@ -600,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| { @@ -623,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| { @@ -646,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| { @@ -669,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| { @@ -686,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 /// @@ -737,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 /// @@ -746,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 @@ -763,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 @@ -849,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| { @@ -989,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| { @@ -1016,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| { @@ -1043,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| { @@ -1072,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)); @@ -1094,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)); @@ -1190,7 +1185,8 @@ 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)) { diff --git a/ctest/src/template.rs b/ctest/src/template.rs index ed0a657288cc7..1e3d41df3b535 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -93,7 +93,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 +114,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, @@ -451,7 +453,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, @@ -477,12 +480,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)] diff --git a/ctest/src/translator.rs b/ctest/src/translator.rs index 9f14fe65154d3..1bb7e3287314d 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,9 +264,9 @@ 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) => { @@ -346,9 +350,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 +373,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 From c75758bc7ccd56aff79bf72cc989f653623709e9 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:44:41 +0200 Subject: [PATCH 05/29] ctest: differentiate between `ident` and `path` - Rename `ident` function on all parsed items to `path`. After the changes in the last few patches, this function was returning the cached string we keep in each item representative of the stringified token stream from each item's `path` field. Thus, speaking of an identifier here is a bit misleading, and considering it a path to the item is more correct. - Add `ident` function to return the last segment of each parsed item's `path` field. This corresponds now with the previous semantics of `ident`, now `path`, but breaks the API because it returns a fully-owned `String` and not a `&str`. It also incurrs an additional allocation on each call, but that can be easily fixed by also "caching" this last segment of the item's path in one of the item's fields. --- ctest/src/ast/constant.rs | 18 ++++++++++++++++-- ctest/src/ast/function.rs | 18 ++++++++++++++++-- ctest/src/ast/module.rs | 18 ++++++++++++++++-- ctest/src/ast/static_variable.rs | 18 ++++++++++++++++-- ctest/src/ast/structure.rs | 18 ++++++++++++++++-- ctest/src/ast/type_alias.rs | 18 ++++++++++++++++-- ctest/src/ast/union.rs | 18 ++++++++++++++++-- ctest/src/generator.rs | 14 +++++++------- 8 files changed, 119 insertions(+), 21 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index 710aed44e34f3..4b78b6efbe6bc 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -10,8 +10,22 @@ pub struct Const { } 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 c4afb491f87a2..60ee882ba8c1c 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -22,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/module.rs b/ctest/src/ast/module.rs index ad0fa006c3411..faedb33c50137 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -12,8 +12,22 @@ pub struct Module { } impl Module { - /// Returns the identifier of the parsed module. - pub fn ident(&self) -> &str { + /// 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 fcf45c65134f2..ab1797174b933 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -19,11 +19,25 @@ pub struct Static { } 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 00ef065fb4f20..dc47731c44773 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -13,8 +13,22 @@ pub struct Struct { } 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 73cacb88f81c1..0d5c79a021938 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -10,8 +10,22 @@ pub struct 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 2fd443651d410..a49278d0de737 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -13,8 +13,22 @@ pub struct Union { } 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/generator.rs b/ctest/src/generator.rs index 049fb1a8a9dea..d1a914e35c566 100644 --- a/ctest/src/generator.rs +++ b/ctest/src/generator.rs @@ -494,7 +494,7 @@ impl TestGenerator { /// 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::ident`] function. + /// [`Module::path`] function. /// /// # Examples /// @@ -1193,12 +1193,12 @@ impl TestGenerator { 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}"), From 3c74044660a4315057ca65828d64c413fa69299c Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:55:12 +0200 Subject: [PATCH 06/29] ctest: adjust `ident` call sites with new fns - Replace and tweak call sites where there were uses of the `ident` function exposed by parsed items into using a combination of both the new `ident` (which replicates the former's semantics,) `path` (which provides the full path to the item from the crate root,) and a new routine; `escape_item_path`. The latter is used to replace the default path separator `::` with `_`. Use of the these three is necessary to get the tests to both refer to the right Rust items in the parsed crate submodules, while keeping the same (single-segment) identifiers for C symbols. - Add `rust_ty` and `rust_val` fields to some of the types gathering data for the test templates. This is also necessary for keeping track of the actual paths to the types and symbols exposed by the crate, as modules introduce the possibility for items to be referred to by a path with more than one segment. - Tweak uses of Rust to C `MapInput` type variants for remapping into specifically only returning `CEnum`s in the current module. Before modules were supported, checking straight with the list of skips in the running `TestGenerator` was enough. This is not the case anymore, as those skips are provided by the user with respect to full paths (i.e. they apply globally across parsed modules.) The change in this patch ensures that when returning a `MapInput` variant that yields a type and not an item, the `CEnumType` variant is returned only when the current module being parsed contains an alias with the passed identifier, and further checks with the `TestGenerator` if the full path to that alias (if found) has a `CEnum` remapping set. - Tweak the Rust test template to reflect the changes made to the `template` module. This uses the new fields introduced in this same patch, and changes some uses of the `id` field to either one of the `rust_ty` or `rust_val` fields, as those keep the full path to the item type or item, respectively. --- ctest-test/build.rs | 20 +-- ctest/src/ast/mod.rs | 18 +++ ctest/src/template.rs | 208 ++++++++++++++++++++---------- ctest/src/translator.rs | 15 ++- ctest/templates/test.rs | 24 ++-- ctest/tests/basic.rs | 14 +- libc-test/build/main.rs | 274 ++++++++++++++++++++-------------------- 7 files changed, 339 insertions(+), 234 deletions(-) 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/mod.rs b/ctest/src/ast/mod.rs index 49b58cfb2ee35..816357f10971e 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -20,6 +20,24 @@ 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/template.rs b/ctest/src/template.rs index 1e3d41df3b535..7bb48285152f4 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -19,6 +19,7 @@ use crate::{ TestGenerator, TranslationError, VolatileItemKind, + ast, cdecl, }; @@ -127,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(), @@ -160,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()); @@ -170,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()); @@ -180,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()); @@ -204,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(), }; @@ -240,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)), @@ -256,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()); @@ -290,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(()) @@ -308,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. @@ -352,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 { @@ -381,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)), @@ -389,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)?, @@ -406,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, @@ -428,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()); @@ -463,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()); @@ -531,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, @@ -542,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, } @@ -551,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, } @@ -559,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, } @@ -568,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 { @@ -637,12 +697,12 @@ impl<'a> TranslateHelper<'a> { generator .skips .iter() - .any(|f| f(&MapInput::CEnumType(alias.ident()))) + .any(|f| f(&MapInput::CEnumType(alias.path()))) }); for item in skipped { if generator.verbose_skip { - eprintln!("Skipping C enum type {}", item.ident()); + eprintln!("Skipping C enum type {}", item.path()); } } @@ -656,7 +716,7 @@ impl<'a> TranslateHelper<'a> { for item in skipped { if generator.verbose_skip { - eprintln!("Skipping C enum constant {}", item.ident()); + eprintln!("Skipping C enum constant {}", item.path()); } } @@ -668,7 +728,7 @@ impl<'a> TranslateHelper<'a> { }); for item in skipped { if generator.verbose_skip { - eprintln!("Skipping {} \"{}\"", $label, item.ident()) + eprintln!("Skipping {} \"{}\"", $label, item.path()) } } }}; @@ -701,24 +761,38 @@ 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!"), }; @@ -726,7 +800,7 @@ impl<'a> TranslateHelper<'a> { let ty = cdecl::cdecl(&ty, "".to_string()).map_err(|_| { TranslationError::new( TranslationErrorKind::InvalidReturn, - ident, + item_path, Span::call_site(), ) })?; @@ -747,7 +821,7 @@ fn tmp() { let file = syn::parse_file(file).unwrap(); items.visit_file(&file); let mut generator = TestGenerator::new(); - generator.skip_fn(|it| it.ident() == "t::r::ctime"); - let mut translator = TranslateHelper::new(&items, &generator); + 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/translator.rs b/ctest/src/translator.rs index 1bb7e3287314d..c33ba1f926336 100644 --- a/ctest/src/translator.rs +++ b/ctest/src/translator.rs @@ -293,7 +293,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) diff --git a/ctest/templates/test.rs b/ctest/templates/test.rs index 3a71c73ba3242..4527f9459d32a 100644 --- a/ctest/templates/test.rs +++ b/ctest/templates/test.rs @@ -184,18 +184,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 +213,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 +242,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 +258,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 +266,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 +280,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 +320,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 +359,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 +371,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/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); From b6de49253aaf251ea6a719902b809fa8e2b7f82d Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:22:08 +0200 Subject: [PATCH 07/29] ctest(maybe_remove): tweak a little something --- ctest/src/translator.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ctest/src/translator.rs b/ctest/src/translator.rs index c33ba1f926336..c6a9b133dc057 100644 --- a/ctest/src/translator.rs +++ b/ctest/src/translator.rs @@ -269,8 +269,11 @@ impl<'a> Translator<'a> { /// 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); From da30907b5c6735b5de628f4758603b32259f9aa8 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:59:38 +0200 Subject: [PATCH 08/29] ctest: tweak tests and test template to pass them - Tweak one of the tests to adjust to the way module support has been implemented. Previously, items in nested modules would be expected to surface at the top-level. Now they are meant to be part of the parsed nested module within the initial `FfiItems` instance. - Tweak Rust test template to avoid `unused` lints against some of the utility functions. I am still looking through some stuff in the tests, but thus far these functions sometimes simply don't get used because certain askama loops never iterate when the tests that use them are wholly skipped. --- ctest/src/tests.rs | 18 +++++++++++++++--- ctest/templates/test.rs | 3 +++ 2 files changed, 18 insertions(+), 3 deletions(-) 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/templates/test.rs b/ctest/templates/test.rs index 4527f9459d32a..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); From a15dd2fd2157e373fe2d45be25debe6407972f9e Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:24:14 +0200 Subject: [PATCH 09/29] ctest: update test templates --- ctest/tests/input/hierarchy.out.c | 46 +---- ctest/tests/input/hierarchy.out.rs | 169 +------------------ ctest/tests/input/macro.out.c | 2 +- ctest/tests/input/macro.out.edition-2024.c | 2 +- ctest/tests/input/macro.out.edition-2024.rs | 11 +- ctest/tests/input/macro.out.rs | 11 +- ctest/tests/input/simple.out.with-renames.c | 2 +- ctest/tests/input/simple.out.with-renames.rs | 15 +- ctest/tests/input/simple.out.with-skips.c | 2 +- ctest/tests/input/simple.out.with-skips.rs | 3 + 10 files changed, 34 insertions(+), 229 deletions(-) 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); From 5cc36f182794da54bba2994ab61f021687da9ec2 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:30:23 +0200 Subject: [PATCH 10/29] ctest: fmt a string to 80 characters wide --- ctest/src/ffi_items.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 0c020a158b013..43e652c428524 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -204,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(), From 56204ba146bee3be52c89e22a58e71de90d22813 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 13 Sep 2026 16:35:27 +0200 Subject: [PATCH 11/29] chore: wip --- notes/main.typ | 123 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 notes/main.typ diff --git a/notes/main.typ b/notes/main.typ new file mode 100644 index 0000000000000..fdd4bab5582ae --- /dev/null +++ b/notes/main.typ @@ -0,0 +1,123 @@ +#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: + use foo::*; + ``` + +/ Module ```rust crate::bar```: + ```rust + use foo::*; + mod 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. + +This is just theory, though. From 372aa5651fb6e4713e998dc85b165b70a8cd4c29 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:33:48 +0200 Subject: [PATCH 12/29] chore: wip --- notes/main.typ | 152 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/notes/main.typ b/notes/main.typ index fdd4bab5582ae..4e2bcd9abc31e 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -100,8 +100,9 @@ And for that matter, you could have instead module / Module ```rust crate::bar```: ```rust - use foo::*; + // use foo::*; // `foo` comes from `barfoo`'s reexport below mod barfoo; + use barfoo::foo::*; ``` The above situation is one of a number of potentially complex item resolution @@ -120,4 +121,151 @@ 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. -This is just theory, though. +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: + +/ Algorithm 1: \ + Inputs: + + - An ```rust FfiItems``` with the parsed contents of a full crate. This will + be referred to interchangeably as both the current module and the current + module's ```rust FfiItems``` in the algorithm steps. + + Outputs: + + - *Pending*. + + Steps: + + + Match on the list of child modules to the current module. + + - 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, run algorithm 1. Set its input to be the + current ```rust FfiItems``` with its modules as the tail list of the + current list of modules. + + - If there are any reexports, extract the next reexport. + + + Run algorithm 3. Set the input import to the extracted reexport. + + + Match on the result of step 1.b.1.b.1. + + - If matching against a _glob_ reexport type, proceed as follows. + + + Run algorithm 2. Set the input to algorithm 2 to be the + extracted reexport, and the current input to algorithm 1. + + - If matching against a _specific_ reexport type, proceed as + follows. + + + *Pending*. + + + Repeat from step 1.b.1 with the tail list of reexports. + +/ Algorithm 2: \ + Inputs: + + - An import ```rust use``` statement that is known to be a glob reexport. + + - A base ```rust FfiItems``` corresponding to the module where the above + reexport lives at. + + Outputs: + + - A list of ```rust FfiItems``` instances corresponding with the tail modules + contained in the input import statement (the potentially non-direct + descendedants of the second input.) + + Steps: + + + Match against the type of input import path. + + - If the import is a path, proceed as follows. + + + *Pending*. + + - *Pending*. + + + Perform a lookup of this identifier in the running state's list of child + modules. + + - If a match is found, repeat from step 2. Set the running state to be the + ```rust FfiItems``` associated to the matched module. Set the input path + to be the current input path trimmed from its initial segment. + + - If a match is not found, ... + +/ Algorithm 3: \ + Inputs: + + - An import used in a ```rust use``` statement. + + Outputs: + + - The type of reexport the input ```rust use``` statement was. This can be one + of a _glob_ reexport or a _specific_ reexport. + + Steps: + + + Match against the type of input import path. + + - If the import is a path, run algorithm 3. Set the input path to be the + newly-found rightmost import. + + - If the import is a glob, return a _glob_ reexport type. + + - If the import is an identifier or a renamed identifier, return a + _specific_ reexport type. + + - If the import is a group, proceed as follows. + + + Run a list mapping algorithm over the list of elements in the group. Set + the transform to be algorithm 3. + + + Run a list reduction algorithm over the result of step 1.d.1. Set the + transform to be algorithm 4. + + + Match against the result of step 1.d.2. + + - If the reduction yield some value, return the value. + - Otherwise, return a _specific_ reexport type. + +/ Algorithm 4: \ + Inputs: + + - A reexport type as described in the outputs of algorithm 3. + - A reexport type as described in the outputs of algorithm 3. + + Outputs: + + - A reexport type as described in the outputs of algorithm 3. + + Steps: + + + Match against an ordered pair of the two inputs. + + - If the leftmost element or the rightmost element are _glob_ reexport + types, return a _glob_ reexport type. + + - Otherwise, return a _specific_ reexport type. From 7e16883688f88d146c5b28d74d9d134edea946cb Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:25:57 +0200 Subject: [PATCH 13/29] chore: wip --- notes/main.typ | 101 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 19 deletions(-) diff --git a/notes/main.typ b/notes/main.typ index 4e2bcd9abc31e..58f87441a1ce9 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -94,7 +94,8 @@ And for that matter, you could have instead module / Module ```rust crate::bar::barfoo```: ```rust - mod foo: + mod foo; + mod test; use foo::*; ``` @@ -102,7 +103,7 @@ And for that matter, you could have instead module ```rust // use foo::*; // `foo` comes from `barfoo`'s reexport below mod barfoo; - use barfoo::foo::*; + use barfoo::foo::{test, Bar}; ``` The above situation is one of a number of potentially complex item resolution @@ -138,14 +139,15 @@ 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: +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``` with the parsed contents of a full crate. This will be referred to interchangeably as both the current module and the current - module's ```rust FfiItems``` in the algorithm steps. + module's ```rust FfiItems``` in the steps below. Outputs: @@ -163,7 +165,8 @@ Instead, the algorithm could be something along the lines of: - If there are no reexports, run algorithm 1. Set its input to be the current ```rust FfiItems``` with its modules as the tail list of the - current list of modules. + current list of modules (i.e. discard the extracted + ```rust FfiItems```.) - If there are any reexports, extract the next reexport. @@ -186,35 +189,95 @@ Instead, the algorithm could be something along the lines of: / Algorithm 2: \ Inputs: - - An import ```rust use``` statement that is known to be a glob reexport. + - An import ```rust use``` statement. - A base ```rust FfiItems``` corresponding to the module where the above - reexport lives at. + import statement lives at. Outputs: - - A list of ```rust FfiItems``` instances corresponding with the tail modules - contained in the input import statement (the potentially non-direct - descendedants of the second input.) + - 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 path. + + Match against the type of input import. - If the import is a path, proceed as follows. - + *Pending*. + + 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 found in step 1.a.1.a. + + + Return the result of step 1.a.1.a.1. + + - If the list of modules does not contain a match, return a + single-element list. The element should consist of the value returned + from calling the _unresolved_ data constructor. - - *Pending*. + - If the import is an identifier or a renamed identifier, proceed as + follows. - + Perform a lookup of this identifier in the running state's list of child - modules. + + 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 single-element list. The 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, return a single-element list. The element should + consist of the value returned from calling the _unresolved_ data + constructor. + + - If the import is a glob, return a single-element list. The element should + wrap the input ```rust FfiItems``` instance with a _resolved_ data + constructor. + + - If the import is a group, proceed as follows. - - If a match is found, repeat from step 2. Set the running state to be the - ```rust FfiItems``` associated to the matched module. Set the input path - to be the current input path trimmed from its initial segment. + + Match on the next element of the group. - - If a match is not found, ... + - If there are no elements left, return the empty list. + + - If there are any elements left, extract the next element and proceed + as follows. + + + Match against the extracted element's import type. + + - If the import is a path, extract the path and proceed as follows. + + + Match against the list of modules of the input + ```rust FfiItems```. + + - If a match is found for the path segment or identifier, + proceed as follows. + + + Run algorithm 2. Set the input import statement to be the + extracted path. Set the input ```rust FfiItems``` to be the + matched module among the current input's children. + + - If a match is not found for the path segment or identifier, + proceed as follows. + + + Call the _unresolved_ data constructor. + + - If the import is an identifier (or a renamed identifier), extract + the (original) identifier and proceed as follows. + + + *Pending*. / Algorithm 3: \ Inputs: From eec5bcf0ba55cdcddf1650963c36a595b383c116 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:42:32 +0200 Subject: [PATCH 14/29] chore: wip --- notes/main.typ | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/notes/main.typ b/notes/main.typ index 58f87441a1ce9..cf64d0719ca11 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -234,13 +234,14 @@ Bellman-Ford except without proof of correctness:) - If a match is found for the identifier or original identifier (in the case of a rename,) proceed as follows. - + Return a single-element list. The element should consist of a new + + Return a singleton list. The 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, return a single-element list. The element should - consist of the value returned from calling the _unresolved_ 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, return a single-element list. The element should wrap the input ```rust FfiItems``` instance with a _resolved_ data @@ -262,22 +263,35 @@ Bellman-Ford except without proof of correctness:) + Match against the list of modules of the input ```rust FfiItems```. - - If a match is found for the path segment or identifier, - proceed as follows. + - If a match is found for the path segment, proceed as follows. + Run algorithm 2. Set the input import statement to be the extracted path. Set the input ```rust FfiItems``` to be the - matched module among the current input's children. + matched module. - - If a match is not found for the path segment or identifier, - proceed as follows. + - If a match is not found for the path segment, proceed as + follows. - + Call the _unresolved_ data constructor. + + Call the _unresolved_ data constructor. Construct a + singleton list with the value returned from this call. - If the import is an identifier (or a renamed identifier), extract the (original) identifier and proceed as follows. - + *Pending*. + + Match against the list of all items of the input + ```rust FfiItems```. + + - If a match is found for the identifier, proceed as follows. + + + Return a singleton list. Its one element should be a new + ```rust FfiItems``` instance containing the matched item, + wrapped by the _resolved_ data constructor. + + - If a match is not found for the identifier, proceed as + follows. + + + Return a singleton list. Its one element should be the value + returned from calling the _unresolved_ data constructor. / Algorithm 3: \ Inputs: From 3969d8384c44d4870ab226283b2b3102aa0c8e0a Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:38:20 +0200 Subject: [PATCH 15/29] chore: wip --- notes/main.typ | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/notes/main.typ b/notes/main.typ index cf64d0719ca11..29c06732782f6 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -283,9 +283,8 @@ Bellman-Ford except without proof of correctness:) - If a match is found for the identifier, proceed as follows. - + Return a singleton list. Its one element should be a new - ```rust FfiItems``` instance containing the matched item, - wrapped by the _resolved_ data constructor. + + Call the _resolved_ data constructor with a newly created + ```rust FfiItems``` containig solely the matched item. - If a match is not found for the identifier, proceed as follows. From 4fb056dccd7a02bd064009fa18793aa78c9b055a Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:35:25 +0200 Subject: [PATCH 16/29] chore: wip --- notes/main.typ | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/notes/main.typ b/notes/main.typ index 29c06732782f6..e0eb3f97b5bf7 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -204,6 +204,10 @@ Bellman-Ford except without proof of correctness:) 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. + All reexports are flattened into a single list of either resolved or + unresolved reexports. We differentiate between these with the above sum + type. + Steps: + Match against the type of input import. @@ -272,8 +276,7 @@ Bellman-Ford except without proof of correctness:) - If a match is not found for the path segment, proceed as follows. - + Call the _unresolved_ data constructor. Construct a - singleton list with the value returned from this call. + + Call the _unresolved_ data constructor. - If the import is an identifier (or a renamed identifier), extract the (original) identifier and proceed as follows. @@ -289,8 +292,7 @@ Bellman-Ford except without proof of correctness:) - If a match is not found for the identifier, proceed as follows. - + Return a singleton list. Its one element should be the value - returned from calling the _unresolved_ data constructor. + + Call the _resolved_ data constructor. / Algorithm 3: \ Inputs: From ac79172defa6ab83427ab6c835b259424c622a32 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:00:15 +0200 Subject: [PATCH 17/29] chore: wip --- notes/main.typ | 157 ++++++++++++++----------------------------------- 1 file changed, 44 insertions(+), 113 deletions(-) diff --git a/notes/main.typ b/notes/main.typ index e0eb3f97b5bf7..0fdb7c3bdbd29 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -145,9 +145,9 @@ Bellman-Ford except without proof of correctness:) / Algorithm 1: \ Inputs: - - An ```rust FfiItems``` with the parsed contents of a full crate. This will - be referred to interchangeably as both the current module and the current - module's ```rust FfiItems``` in the steps below. + - 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: @@ -155,7 +155,7 @@ Bellman-Ford except without proof of correctness:) Steps: - + Match on the list of child modules to the current module. + + Match on the list of child modules to the input ```rust FfiItems```. - If there are no child modules, return unity (*Pending*.) @@ -163,28 +163,17 @@ Bellman-Ford except without proof of correctness:) + Match on the list of reexports in the extracted module. - - If there are no reexports, run algorithm 1. Set its input to be the - current ```rust FfiItems``` with its modules as the tail list of the - current list of modules (i.e. discard the extracted - ```rust FfiItems```.) + - If there are no reexports, proceed as follows. - - If there are any reexports, extract the next reexport. - - + Run algorithm 3. Set the input import to the extracted reexport. - - + Match on the result of step 1.b.1.b.1. - - - If matching against a _glob_ reexport type, proceed as follows. - - + Run algorithm 2. Set the input to algorithm 2 to be the - extracted reexport, and the current input to algorithm 1. - - - If matching against a _specific_ reexport type, 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. - + *Pending*. + - If there are any reexports, extract the next reexport. - + Repeat from step 1.b.1 with the tail list of reexports. + + 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: @@ -204,10 +193,6 @@ Bellman-Ford except without proof of correctness:) 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. - All reexports are flattened into a single list of either resolved or - unresolved reexports. We differentiate between these with the above sum - type. - Steps: + Match against the type of input import. @@ -222,13 +207,14 @@ Bellman-Ford except without proof of correctness:) + 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 found in step 1.a.1.a. + ```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, return a - single-element list. The element should consist of the value returned - from calling the _unresolved_ data constructor. + - If the list of modules does not contain a match, proceed as follows. + + + Return a single-element 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. @@ -238,7 +224,7 @@ Bellman-Ford except without proof of correctness:) - If a match is found for the identifier or original identifier (in the case of a rename,) proceed as follows. - + Return a singleton list. The one element should consist of a new + + 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. @@ -247,103 +233,48 @@ Bellman-Ford except without proof of correctness:) + Return a singleton list. The element should consist of the value returned from calling the _unresolved_ data constructor. - - If the import is a glob, return a single-element list. The element should - wrap the input ```rust FfiItems``` instance with a _resolved_ data - constructor. - - - If the import is a group, proceed as follows. - - + Match on the next element of the group. - - - If there are no elements left, return the empty list. - - - If there are any elements left, extract the next element and proceed - as follows. - - + Match against the extracted element's import type. - - - If the import is a path, extract the path and proceed as follows. - - + Match against the list of modules of the input - ```rust FfiItems```. - - - If a match is found for the path segment, proceed as follows. - - + Run algorithm 2. Set the input import statement to be the - extracted path. Set the input ```rust FfiItems``` to be the - matched module. - - - If a match is not found for the path segment, proceed as - follows. - - + Call the _unresolved_ data constructor. + - If the import is a glob, proceed as follows. - - If the import is an identifier (or a renamed identifier), extract - the (original) identifier and proceed as follows. + + Return a single-element list. Its one element should wrap the input + ```rust FfiItems``` instance with a _resolved_ data constructor. - + Match against the list of all items of the input - ```rust FfiItems```. - - - If a match is found for the identifier, proceed as follows. - - + Call the _resolved_ data constructor with a newly created - ```rust FfiItems``` containig solely the matched item. + - If the import is a group, proceed as follows. - - If a match is not found for the identifier, 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```. - + Call the _resolved_ data constructor. + + Return the result of step 1.d.1. / Algorithm 3: \ Inputs: - - An import used in a ```rust use``` statement. + - 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: - - The type of reexport the input ```rust use``` statement was. This can be one - of a _glob_ reexport or a _specific_ reexport. + - A list of ```rust FfiItems``` wrapped with the same sum type as outlined in + the outputs of algorithm 2. Steps: - + Match against the type of input import path. - - - If the import is a path, run algorithm 3. Set the input path to be the - newly-found rightmost import. - - - If the import is a glob, return a _glob_ reexport type. - - - If the import is an identifier or a renamed identifier, return a - _specific_ reexport type. - - - If the import is a group, proceed as follows. - - + Run a list mapping algorithm over the list of elements in the group. Set - the transform to be algorithm 3. + + Match against the input list. - + Run a list reduction algorithm over the result of step 1.d.1. Set the - transform to be algorithm 4. + - If there are no elements left in the group, return the empty list. - + Match against the result of step 1.d.2. - - - If the reduction yield some value, return the value. - - Otherwise, return a _specific_ reexport type. - -/ Algorithm 4: \ - Inputs: - - - A reexport type as described in the outputs of algorithm 3. - - A reexport type as described in the outputs of algorithm 3. - - Outputs: - - - A reexport type as described in the outputs of algorithm 3. - - Steps: + - If there are any elements left, extract the next element and proceed as + follows. - + Match against an ordered pair of the two inputs. + + 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```. - - If the leftmost element or the rightmost element are _glob_ reexport - types, return a _glob_ reexport type. + + 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. - - Otherwise, return a _specific_ reexport type. + + Return the resulting list from appending the lists from step 1.b.1 to + the lists from step 1.b.2. From c2573e7a8a4e23a541326b8f2cb48427712f4da8 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:03:43 +0200 Subject: [PATCH 18/29] chore: wip --- notes/Demo.idr | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++ notes/main.typ | 37 ++++++++++++++++++--- 2 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 notes/Demo.idr diff --git a/notes/Demo.idr b/notes/Demo.idr new file mode 100644 index 0000000000000..697b12cf7750d --- /dev/null +++ b/notes/Demo.idr @@ -0,0 +1,88 @@ +module Demo + +import Data.List + +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 + +total +empty : String -> FfiItems +empty s = MkFfiItems s [] [] [] + +total +normalize : FfiItems -> FfiItems +normalize it = { uses $= foldr f [] } it where + f : UseTree -> List UseTree -> List UseTree + f t l = f' t ++ l where f' : UseTree -> List UseTree + f' (Name id) = [ Name id ] + f' Glob = [ Glob ] + f' (Path id t) = map (\t => Path id t) (f' t) + f' (Group l) = map f' l |> join + +total +resolveOne : FfiItems -> List Resolution +resolveOne it = join . (map $ \u => resolveReexport u u it) . uses it where + data Ty = Mod FfiItems | Item String + + f : String -> FfiItems -> Maybe Ty + f id (MkFfiItems _ is ms _) = find c (map Item is ++ map Mod ms) where + c : Ty -> Bool + c (Mod (MkFfiItems mid _ _ _)) = mid == id + c (Item s) = s == id + + -- [NOTE]: this function is not even covering because that would require + -- making the `UseTree` type a GADT. That is not worth it for a PoC. + partial + resolveReexport : UseTree -> UseTree -> FfiItems -> List Resolution + resolveReexport oid (Name id) it@(MkFfiItems mid _ _ _) = + case f id it of + Just (Mod m) => [ Resolved oid ({ mods := [ m ] } . empty mid) ] + Just (Item i) => [ Resolved oid ({ items := [ i ] } . empty mid) ] + Nothing => [ Unresolved oid ] + resolveReexport _ Glob it = [ Resolved oid it ] + resolveReexport oid (Path (id, t)) it = + case f id it of Just (Mod m) => resolveReexport oid t m + Just _ => [ Unresolved oid ] + Nothing => [ Unresolved oid ] + +total +merge : FfiItems -> List Resolution -> FfiItems +merge it [] = it +merge it ((Unresolved _) :: t) = merge it t +merge it ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge mit t where + mit : FfiItems + mit = { items $= (++ is) + , mods $= (++ ms) + , uses $= deleteBy f oid } it where + -- [NOTE]: this function is semantically partial because it handles + -- not the case for a group import. The wildcard case, which is used + -- for combinations of the considered (partial) cases, makes the + -- function covering even though it is meant to be partial. We use + -- `deleteBy` instead of implementing `Eq` for `UseTree` because + -- partial functions are painful enough. + f : UseTree -> UseTree -> Bool + f (Name id1) (Name id2) = id1 == id2 + f Glob Glob = True + f (Path id1 t1) (Path id2 t2) = id1 == id2 && f t1 t2 + f _ = False + +total +resolve : FfiItems -> FfiItems +resolve it = (merge nit) . resolveOne . { mods $= map resolve } nit where + nit : FfiItems + nit = normalize it diff --git a/notes/main.typ b/notes/main.typ index 0fdb7c3bdbd29..4fe769f5166ca 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -101,9 +101,9 @@ And for that matter, you could have instead module / Module ```rust crate::bar```: ```rust - // use foo::*; // `foo` comes from `barfoo`'s reexport below + use foo::*; // `foo` comes from `barfoo`'s reexport below mod barfoo; - use barfoo::foo::{test, Bar}; + use barfoo::*; ``` The above situation is one of a number of potentially complex item resolution @@ -213,8 +213,8 @@ Bellman-Ford except without proof of correctness:) - If the list of modules does not contain a match, proceed as follows. - + Return a single-element list. Its one element should consist of the - value returned from calling the _unresolved_ data constructor. + + 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. @@ -278,3 +278,32 @@ Bellman-Ford except without proof of correctness:) + 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. From c7b1d2a83a35d9bbfc493e266f11b6573214d006 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:13:46 +0200 Subject: [PATCH 19/29] chore: wip --- notes/Demo.idr | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 697b12cf7750d..9d711443e8dbf 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -27,18 +27,20 @@ empty s = MkFfiItems s [] [] [] total normalize : FfiItems -> FfiItems normalize it = { uses $= foldr f [] } it where + total f : UseTree -> List UseTree -> List UseTree - f t l = f' t ++ l where f' : UseTree -> List UseTree - f' (Name id) = [ Name id ] - f' Glob = [ Glob ] - f' (Path id t) = map (\t => Path id t) (f' t) - f' (Group l) = map f' l |> join + f = (++) . f' where total f' : UseTree -> List UseTree + f' (Name id) = [ Name id ] + f' Glob = [ Glob ] + f' (Path id t) = assert_total map (\t => Path id t) (f' t) + f' (Group l) = assert_total map f' l |> join -total +partial resolveOne : FfiItems -> List Resolution -resolveOne it = join . (map $ \u => resolveReexport u u it) . uses it where +resolveOne it = join . (map $ \u => resolveReexport u u it) . uses $ it where data Ty = Mod FfiItems | Item String + total f : String -> FfiItems -> Maybe Ty f id (MkFfiItems _ is ms _) = find c (map Item is ++ map Mod ms) where c : Ty -> Bool @@ -51,11 +53,11 @@ resolveOne it = join . (map $ \u => resolveReexport u u it) . uses it where resolveReexport : UseTree -> UseTree -> FfiItems -> List Resolution resolveReexport oid (Name id) it@(MkFfiItems mid _ _ _) = case f id it of - Just (Mod m) => [ Resolved oid ({ mods := [ m ] } . empty mid) ] - Just (Item i) => [ Resolved oid ({ items := [ i ] } . empty mid) ] + Just (Mod m) => [ Resolved oid (({ mods := [ m ] } . empty) mid) ] + Just (Item i) => [ Resolved oid (({ items := [ i ] } . empty) mid) ] Nothing => [ Unresolved oid ] - resolveReexport _ Glob it = [ Resolved oid it ] - resolveReexport oid (Path (id, t)) it = + resolveReexport oid Glob it = [ Resolved oid it ] + resolveReexport oid (Path id t) it = case f id it of Just (Mod m) => resolveReexport oid t m Just _ => [ Unresolved oid ] Nothing => [ Unresolved oid ] @@ -75,14 +77,16 @@ merge it ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge mit t where -- function covering even though it is meant to be partial. We use -- `deleteBy` instead of implementing `Eq` for `UseTree` because -- partial functions are painful enough. + total f : UseTree -> UseTree -> Bool f (Name id1) (Name id2) = id1 == id2 f Glob Glob = True f (Path id1 t1) (Path id2 t2) = id1 == id2 && f t1 t2 - f _ = False + f _ _ = False -total +partial resolve : FfiItems -> FfiItems -resolve it = (merge nit) . resolveOne . { mods $= map resolve } nit where +resolve it = (merge nit) . resolveOne . { mods $= map resolve } $ nit where + total nit : FfiItems nit = normalize it From c89b68e99fcb6564b47c82f20254debb000f32b6 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:18:35 +0200 Subject: [PATCH 20/29] chore: wip --- notes/Demo.idr | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 9d711443e8dbf..030b3d341a265 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -29,11 +29,16 @@ normalize : FfiItems -> FfiItems normalize it = { uses $= foldr f [] } it where total f : UseTree -> List UseTree -> List UseTree - f = (++) . f' where total f' : UseTree -> List UseTree - f' (Name id) = [ Name id ] - f' Glob = [ Glob ] - f' (Path id t) = assert_total map (\t => Path id t) (f' t) - f' (Group l) = assert_total map f' l |> join + f = (++) . f' where + -- [NOTE]: this function requires asserting to the totality checker that the + -- trees rooted at paths and group imports are always bound to be smaller + -- than the trees rooted one level above. This is not encoded in the + -- `UseTree` type to keep things simple to port to Rust. + total f' : UseTree -> List UseTree + f' (Name id) = [ Name id ] + f' Glob = [ Glob ] + f' o@(Path id t) = map (\t => Path id t) (f' $ assert_smaller o t) + f' o@(Group l) = map (\t => f' $ assert_smaller o t) l |> join partial resolveOne : FfiItems -> List Resolution From e9c28d43123949a47fc3351374e3e02a3e2496fc Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:01:34 +0200 Subject: [PATCH 21/29] chore: wip --- notes/Demo.idr | 87 +++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 030b3d341a265..98169f841380e 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -2,6 +2,8 @@ module Demo import Data.List +%default total + data UseTree : Type where Path : String -> UseTree -> UseTree Name : String -> UseTree Glob : UseTree @@ -20,78 +22,77 @@ record FfiItems where data Resolution : Type where Resolved : UseTree -> FfiItems -> Resolution Unresolved : UseTree -> Resolution -total +data Ungrouped : UseTree -> Type where NameWitness : Ungrouped (Name _) + GlobWitness : Ungrouped Glob + PathWitness : {auto 0 _ : Ungrouped t} + -> Ungrouped (Path _ t) + +data UngroupedItems : FfiItems -> Type where + ItemsEmptyWitness : UngroupedItems (MkFfiItems _ _ _ []) + ItemsNameWitness : UngroupedItems (MkFfiItems _ _ _ ((Name _) :: _)) + ItemsGlobWitness : UngroupedItems (MkFfiItems _ _ _ (Glob :: _)) + ItemsPathWitness : {auto 0 _ : Ungrouped t} + -> UngroupedItems (MkFfiItems _ _ _ ((Path _ t) :: _)) + empty : String -> FfiItems empty s = MkFfiItems s [] [] [] -total -normalize : FfiItems -> FfiItems -normalize it = { uses $= foldr f [] } it where - total +normalize : FfiItems -> (i : FfiItems ** UngroupedItems i) +normalize it = let normalized = { uses $= foldr f [] } it in + (normalized ** _) where f : UseTree -> List UseTree -> List UseTree f = (++) . f' where -- [NOTE]: this function requires asserting to the totality checker that the -- trees rooted at paths and group imports are always bound to be smaller -- than the trees rooted one level above. This is not encoded in the -- `UseTree` type to keep things simple to port to Rust. - total f' : UseTree -> List UseTree + f' : UseTree -> List UseTree f' (Name id) = [ Name id ] f' Glob = [ Glob ] f' o@(Path id t) = map (\t => Path id t) (f' $ assert_smaller o t) f' o@(Group l) = map (\t => f' $ assert_smaller o t) l |> join -partial -resolveOne : FfiItems -> List Resolution +resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution resolveOne it = join . (map $ \u => resolveReexport u u it) . uses $ it where data Ty = Mod FfiItems | Item String - total f : String -> FfiItems -> Maybe Ty f id (MkFfiItems _ is ms _) = find c (map Item is ++ map Mod ms) where c : Ty -> Bool c (Mod (MkFfiItems mid _ _ _)) = mid == id c (Item s) = s == id - -- [NOTE]: this function is not even covering because that would require - -- making the `UseTree` type a GADT. That is not worth it for a PoC. - partial - resolveReexport : UseTree -> UseTree -> FfiItems -> List Resolution - resolveReexport oid (Name id) it@(MkFfiItems mid _ _ _) = + 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 (Mod m) => [ Resolved oid (({ mods := [ m ] } . empty) mid) ] - Just (Item i) => [ Resolved oid (({ items := [ i ] } . empty) mid) ] + Just (Mod m) => [ Resolved oid ({ mods := [ m ] } . empty $ mid) ] + Just (Item i) => [ Resolved oid ({ items := [ i ] } . empty $ mid) ] Nothing => [ Unresolved oid ] - resolveReexport oid Glob it = [ Resolved oid it ] - resolveReexport oid (Path id t) it = + resolveReexport oid Glob {prf = GlobWitness} it = [ Resolved oid it ] + resolveReexport oid (Path id t) {prf = PathWitness} it = case f id it of Just (Mod m) => resolveReexport oid t m Just _ => [ Unresolved oid ] Nothing => [ Unresolved oid ] -total -merge : FfiItems -> List Resolution -> FfiItems -merge it [] = it +merge : (i : FfiItems ** UngroupedItems i) -> List Resolution -> FfiItems +merge (it ** _) [] = it merge it ((Unresolved _) :: t) = merge it t -merge it ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge mit t where - mit : FfiItems - mit = { items $= (++ is) - , mods $= (++ ms) - , uses $= deleteBy f oid } it where - -- [NOTE]: this function is semantically partial because it handles - -- not the case for a group import. The wildcard case, which is used - -- for combinations of the considered (partial) cases, makes the - -- function covering even though it is meant to be partial. We use - -- `deleteBy` instead of implementing `Eq` for `UseTree` because - -- partial functions are painful enough. - total - f : UseTree -> UseTree -> Bool - f (Name id1) (Name id2) = id1 == id2 - f Glob Glob = True - f (Path id1 t1) (Path id2 t2) = id1 == id2 && f t1 t2 - f _ _ = False +merge (it ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge mit t where + mit : (i : FfiItems ** UngroupedItems i) + mit = let base = { items $= (++ is) + , mods $= (++ ms) + , uses $= deleteBy f oid } it in (base ** _) where + f : UseTree -> UseTree -> Bool + f (Name id1) (Name id2) = id1 == id2 + f Glob Glob = True + f (Path id1 t1) (Path id2 t2) = id1 == id2 && f t1 t2 + f _ _ = False -partial +covering resolve : FfiItems -> FfiItems -resolve it = (merge nit) . resolveOne . { mods $= map resolve } $ nit where - total - nit : FfiItems - nit = normalize it +resolve it = let nit = normalize . { mods $= map resolve } $ it in + (merge nit) . resolveOne $ nit From 5cbef5a46080c0a19c0113c71bcbe281d832283c Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 20 Sep 2026 11:30:52 +0200 Subject: [PATCH 22/29] chore: wip --- notes/Demo.idr | 88 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 98169f841380e..2478a2ba3fb7f 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -38,52 +38,70 @@ empty : String -> FfiItems empty s = MkFfiItems s [] [] [] normalize : FfiItems -> (i : FfiItems ** UngroupedItems i) -normalize it = let normalized = { uses $= foldr f [] } it in - (normalized ** _) where +normalize it = let nit = { uses $= foldr f [] } it in (nit ** _) 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 UseTree -> List UseTree f = (++) . f' where - -- [NOTE]: this function requires asserting to the totality checker that the - -- trees rooted at paths and group imports are always bound to be smaller - -- than the trees rooted one level above. This is not encoded in the - -- `UseTree` type to keep things simple to port to Rust. f' : UseTree -> List UseTree f' (Name id) = [ Name id ] f' Glob = [ Glob ] - f' o@(Path id t) = map (\t => Path id t) (f' $ assert_smaller o t) + f' o@(Path id t) = map (\t => Path id t) (f' t) f' o@(Group l) = map (\t => f' $ assert_smaller o t) l |> join -resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution -resolveOne it = join . (map $ \u => resolveReexport u u it) . uses $ it where - data Ty = Mod FfiItems | Item String +-- resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution +-- resolveOne a@(it ** _) = +resolveOne : _ -> List Resolution +resolveOne _ = + [] where + -- join . (map $ \(u ** _) => resolveReexport u u it) . ex $ a where + data T : Type where Mod : FfiItems -> T + Item : String -> T - f : String -> FfiItems -> Maybe Ty - f id (MkFfiItems _ is ms _) = find c (map Item is ++ map Mod ms) where - c : Ty -> Bool - c (Mod (MkFfiItems mid _ _ _)) = mid == id - c (Item s) = s == id + ex : (i : FfiItems ** UngroupedItems i) -> List (u : UseTree ** Ungrouped u) + ex ((MkFfiItems _ _ _ []) ** _) = + [] + ex a@(it@(MkFfiItems _ _ _ (h :: t)) ** _) = + (h ** _) :: (ex $ assert_smaller a (({ uses := t } it) ** _)) - 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 (Mod m) => [ Resolved oid ({ mods := [ m ] } . empty $ mid) ] - Just (Item i) => [ Resolved oid ({ items := [ i ] } . empty $ mid) ] - Nothing => [ Unresolved oid ] - resolveReexport oid Glob {prf = GlobWitness} it = [ Resolved oid it ] - resolveReexport oid (Path id t) {prf = PathWitness} it = - case f id it of Just (Mod m) => resolveReexport oid t m - Just _ => [ Unresolved oid ] - Nothing => [ Unresolved oid ] + f : String -> FfiItems -> Maybe T + f id (MkFfiItems _ is ms _) = find c ((map Item is) ++ (map Mod ms)) where + c : T -> Bool + c (Mod (MkFfiItems mid _ _ _)) = mid == id + c (Item s) = s == 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 (Mod m) => + [ Resolved oid ({ mods := [ m ] } . empty $ mid) ] + Just (Item i) => + [ Resolved oid ({ items := [ i ] } . empty $ mid) ] + Nothing => + [ Unresolved oid ] + resolveReexport oid Glob {prf = GlobWitness} it + = [ Resolved oid it ] + resolveReexport oid (Path id t) {prf = PathWitness} it + = case f id it of Just (Mod m) => resolveReexport oid t m + Just _ => [ Unresolved oid ] + Nothing => [ Unresolved oid ] merge : (i : FfiItems ** UngroupedItems i) -> List Resolution -> FfiItems -merge (it ** _) [] = it -merge it ((Unresolved _) :: t) = merge it t -merge (it ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge mit t where - mit : (i : FfiItems ** UngroupedItems i) - mit = let base = { items $= (++ is) +merge (it ** _) [] = it +merge it@(_ ** _) ((Unresolved _) :: t) = merge it t +merge (it ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge nit t where + nit : (i : FfiItems ** UngroupedItems i) + nit = let base = { items $= (++ is) , mods $= (++ ms) , uses $= deleteBy f oid } it in (base ** _) where f : UseTree -> UseTree -> Bool From add0dcbc6d077fe75239d607e2f6036b8ed92ba6 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 20 Sep 2026 15:36:07 +0200 Subject: [PATCH 23/29] chore: wip --- notes/Demo.idr | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 2478a2ba3fb7f..cf9674b652be8 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -24,21 +24,24 @@ data Resolution : Type where Resolved : UseTree -> FfiItems -> Resolution data Ungrouped : UseTree -> Type where NameWitness : Ungrouped (Name _) GlobWitness : Ungrouped Glob - PathWitness : {auto 0 _ : Ungrouped t} + PathWitness : {auto 0 prf : Ungrouped t} -> Ungrouped (Path _ t) data UngroupedItems : FfiItems -> Type where ItemsEmptyWitness : UngroupedItems (MkFfiItems _ _ _ []) - ItemsNameWitness : UngroupedItems (MkFfiItems _ _ _ ((Name _) :: _)) - ItemsGlobWitness : UngroupedItems (MkFfiItems _ _ _ (Glob :: _)) - ItemsPathWitness : {auto 0 _ : Ungrouped t} - -> UngroupedItems (MkFfiItems _ _ _ ((Path _ t) :: _)) + ItemsNameWitness : UngroupedItems (MkFfiItems _ _ _ t) + -> UngroupedItems (MkFfiItems _ _ _ ((Name _) :: t)) + ItemsGlobWitness : UngroupedItems (MkFfiItems _ _ _ t) + -> UngroupedItems (MkFfiItems _ _ _ (Glob :: t)) + ItemsPathWitness : UngroupedItems (MkFfiItems _ _ _ t) + -> {auto 0 prf : Ungrouped st} + -> UngroupedItems (MkFfiItems _ _ _ ((Path _ st) :: t)) empty : String -> FfiItems empty s = MkFfiItems s [] [] [] normalize : FfiItems -> (i : FfiItems ** UngroupedItems i) -normalize it = let nit = { uses $= foldr f [] } it in (nit ** _) where +normalize it = let nl = (foldr f []) . uses $ it in fin 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 @@ -47,13 +50,32 @@ normalize it = let nit = { uses $= foldr f [] } it in (nit ** _) where -- 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 UseTree -> List UseTree + f : UseTree + -> List (u : UseTree ** Ungrouped u) + -> List (u : UseTree ** Ungrouped u) f = (++) . f' where - f' : UseTree -> List UseTree - f' (Name id) = [ Name id ] - f' Glob = [ Glob ] - f' o@(Path id t) = map (\t => Path id t) (f' t) - f' o@(Group l) = map (\t => f' $ assert_smaller o t) l |> join + 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 + + fin : List (u : UseTree ** Ungrouped u) + -> FfiItems + -> (i : FfiItems ** UngroupedItems i) + fin [] (MkFfiItems mid is ms _) = + ((MkFfiItems mid is ms []) ** ItemsEmptyWitness) + fin (((Name h) ** _) :: t) i = + let ((MkFfiItems mid is ms us) ** w) = fin t i in + ((MkFfiItems mid is ms ((Name h) :: us)) ** (ItemsNameWitness w)) + fin ((Glob ** _) :: t) i = + let ((MkFfiItems mid is ms us) ** w) = fin t i in + ((MkFfiItems mid is ms (Glob :: us)) ** (ItemsGlobWitness w)) + fin (((Path hid ht) ** htw) :: t) i = + let ((MkFfiItems mid is ms us) ** w) = fin t i in + ((MkFfiItems mid is ms ((Path hid ht) :: us)) ** + (ItemsPathWitness w {prf = htw})) -- resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution -- resolveOne a@(it ** _) = From cf666c41ef5e5e818cd658f1abbcf67c2dac66d3 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:26:41 +0200 Subject: [PATCH 24/29] chore: wip --- notes/Demo.idr | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index cf9674b652be8..221aa4f93048d 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -22,20 +22,20 @@ record FfiItems where 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 0 prf : Ungrouped t} -> Ungrouped (Path _ t) data UngroupedItems : FfiItems -> Type where - ItemsEmptyWitness : UngroupedItems (MkFfiItems _ _ _ []) - ItemsNameWitness : UngroupedItems (MkFfiItems _ _ _ t) - -> UngroupedItems (MkFfiItems _ _ _ ((Name _) :: t)) - ItemsGlobWitness : UngroupedItems (MkFfiItems _ _ _ t) - -> UngroupedItems (MkFfiItems _ _ _ (Glob :: t)) - ItemsPathWitness : UngroupedItems (MkFfiItems _ _ _ t) - -> {auto 0 prf : Ungrouped st} - -> UngroupedItems (MkFfiItems _ _ _ ((Path _ st) :: t)) + EmptyWitness : UngroupedItems (MkFfiItems _ _ _ []) + Witness : {auto 0 prfs : UngroupedItems (MkFfiItems _ _ _ ts)} + -> {auto 0 prf : Ungrouped t} + -> UngroupedItems (MkFfiItems _ _ _ (t :: ts)) empty : String -> FfiItems empty s = MkFfiItems s [] [] [] @@ -55,27 +55,27 @@ normalize it = let nl = (foldr f []) . uses $ it in fin nl it where -> 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' (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 - + map (\(t ** w) => (Path id t ** PathWitness {prf = w})) (f' t) + f' o@(Group l) = + map (\t => f' $ assert_smaller o t) l |> join + + -- [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. fin : List (u : UseTree ** Ungrouped u) -> FfiItems -> (i : FfiItems ** UngroupedItems i) fin [] (MkFfiItems mid is ms _) = - ((MkFfiItems mid is ms []) ** ItemsEmptyWitness) - fin (((Name h) ** _) :: t) i = - let ((MkFfiItems mid is ms us) ** w) = fin t i in - ((MkFfiItems mid is ms ((Name h) :: us)) ** (ItemsNameWitness w)) - fin ((Glob ** _) :: t) i = - let ((MkFfiItems mid is ms us) ** w) = fin t i in - ((MkFfiItems mid is ms (Glob :: us)) ** (ItemsGlobWitness w)) - fin (((Path hid ht) ** htw) :: t) i = - let ((MkFfiItems mid is ms us) ** w) = fin t i in - ((MkFfiItems mid is ms ((Path hid ht) :: us)) ** - (ItemsPathWitness w {prf = htw})) + (MkFfiItems mid is ms [] ** EmptyWitness) + fin ((h ** hw) :: t) i = + let (MkFfiItems mid is ms us ** w) = fin t i in + (MkFfiItems mid is ms (h :: us) ** Witness {prfs = w} {prf = hw}) -- resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution -- resolveOne a@(it ** _) = From 1e2e4402f7989ca7f62c12f8bf2bb2bafda21132 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:00:11 +0200 Subject: [PATCH 25/29] chore: wip --- notes/Demo.idr | 74 +++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 221aa4f93048d..32c0dc3ef2606 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -28,14 +28,14 @@ data Resolution : Type where Resolved : UseTree -> FfiItems -> Resolution data Ungrouped : UseTree -> Type where NameWitness : Ungrouped (Name _) GlobWitness : Ungrouped Glob - PathWitness : {auto 0 prf : Ungrouped t} + PathWitness : {auto prf : Ungrouped t} -> Ungrouped (Path _ t) data UngroupedItems : FfiItems -> Type where EmptyWitness : UngroupedItems (MkFfiItems _ _ _ []) - Witness : {auto 0 prfs : UngroupedItems (MkFfiItems _ _ _ ts)} - -> {auto 0 prf : Ungrouped t} - -> UngroupedItems (MkFfiItems _ _ _ (t :: ts)) + 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 [] [] [] @@ -64,18 +64,28 @@ normalize it = let nl = (foldr f []) . uses $ it in fin nl it where f' o@(Group l) = map (\t => f' $ assert_smaller o t) l |> join - -- [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. - fin : List (u : UseTree ** Ungrouped u) - -> FfiItems - -> (i : FfiItems ** UngroupedItems i) - fin [] (MkFfiItems mid is ms _) = - (MkFfiItems mid is ms [] ** EmptyWitness) - fin ((h ** hw) :: t) i = - let (MkFfiItems mid is ms us ** w) = fin t i in - (MkFfiItems mid is ms (h :: us) ** Witness {prfs = w} {prf = hw}) +-- [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}) -- resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution -- resolveOne a@(it ** _) = @@ -86,12 +96,6 @@ resolveOne _ = data T : Type where Mod : FfiItems -> T Item : String -> T - ex : (i : FfiItems ** UngroupedItems i) -> List (u : UseTree ** Ungrouped u) - ex ((MkFfiItems _ _ _ []) ** _) = - [] - ex a@(it@(MkFfiItems _ _ _ (h :: t)) ** _) = - (h ** _) :: (ex $ assert_smaller a (({ uses := t } it) ** _)) - f : String -> FfiItems -> Maybe T f id (MkFfiItems _ is ms _) = find c ((map Item is) ++ (map Mod ms)) where c : T -> Bool @@ -119,18 +123,20 @@ resolveOne _ = Nothing => [ Unresolved oid ] merge : (i : FfiItems ** UngroupedItems i) -> List Resolution -> FfiItems -merge (it ** _) [] = it -merge it@(_ ** _) ((Unresolved _) :: t) = merge it t -merge (it ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge nit t where - nit : (i : FfiItems ** UngroupedItems i) - nit = let base = { items $= (++ is) - , mods $= (++ ms) - , uses $= deleteBy f oid } it in (base ** _) where - f : UseTree -> UseTree -> Bool - f (Name id1) (Name id2) = id1 == id2 - f Glob Glob = True - f (Path id1 t1) (Path id2 t2) = id1 == id2 && f t1 t2 - f _ _ = False +merge (it ** _) [] = it +merge it@(_ ** _) ((Unresolved _) :: t) = merge it t +merge it@(_ ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge nit t + where nit : (i : FfiItems ** UngroupedItems i) + nit = let nus = (deleteBy f oid) . ex $ it in 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 From ab4484470c621d61b2da3f9fe3da23e0fab1ddd2 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:36:46 +0200 Subject: [PATCH 26/29] chore: wip --- notes/Demo.idr | 94 +++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 32c0dc3ef2606..5a321803e54f2 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -40,30 +40,6 @@ data UngroupedItems : FfiItems -> Type where empty : String -> FfiItems empty s = MkFfiItems s [] [] [] -normalize : FfiItems -> (i : FfiItems ** UngroupedItems i) -normalize it = let nl = (foldr f []) . uses $ it in fin 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 - -- [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. @@ -87,20 +63,38 @@ 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}) --- resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution --- resolveOne a@(it ** _) = -resolveOne : _ -> List Resolution -resolveOne _ = - [] where - -- join . (map $ \(u ** _) => resolveReexport u u it) . ex $ a where - data T : Type where Mod : FfiItems -> T - Item : String -> T - - f : String -> FfiItems -> Maybe T - f id (MkFfiItems _ is ms _) = find c ((map Item is) ++ (map Mod ms)) where - c : T -> Bool - c (Mod (MkFfiItems mid _ _ _)) = mid == id - c (Item s) = s == id +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) @@ -109,25 +103,25 @@ resolveOne _ = -> List Resolution resolveReexport oid (Name id) {prf = NameWitness} it@(MkFfiItems mid _ _ _) = case f id it of - Just (Mod m) => - [ Resolved oid ({ mods := [ m ] } . empty $ mid) ] - Just (Item i) => + Just (Left i) => [ Resolved oid ({ items := [ i ] } . empty $ mid) ] - Nothing => + Just (Right m) => + [ Resolved oid ({ mods := [ m ] } . empty $ mid) ] + Nothing => [ Unresolved oid ] resolveReexport oid Glob {prf = GlobWitness} it = [ Resolved oid it ] - resolveReexport oid (Path id t) {prf = PathWitness} it - = case f id it of Just (Mod m) => resolveReexport oid t m - Just _ => [ Unresolved oid ] - Nothing => [ Unresolved oid ] + 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 -> FfiItems -merge (it ** _) [] = it -merge it@(_ ** _) ((Unresolved _) :: t) = merge it t -merge it@(_ ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge nit t +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 re nus where + nit = let nus = (deleteBy f oid) . ex $ it in re nus iit where f : UseTree -> (u : UseTree ** Ungrouped u) -> Bool f (Name id1) (Name id2 ** _) = id1 == id2 From 2463ee818f0231ffefeed80c7f2f58aa1affeee9 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:19:26 +0200 Subject: [PATCH 27/29] chore: wip --- notes/Demo.idr | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 5a321803e54f2..2f7e58fa229b2 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -104,13 +104,16 @@ resolveOne a@(it ** _) = resolveReexport oid (Name id) {prf = NameWitness} it@(MkFfiItems mid _ _ _) = case f id it of Just (Left i) => - [ Resolved oid ({ items := [ i ] } . empty $ mid) ] + [ oid |> Resolved $ { items := [ i ] } . empty $ mid ] Just (Right m) => - [ Resolved oid ({ mods := [ m ] } . empty $ mid) ] + case uses m of + [] => [ oid |> Resolved $ { mods := [ m ] } . empty $ mid ] + _ => [ Unresolved oid ] Nothing => [ Unresolved oid ] resolveReexport oid Glob {prf = GlobWitness} it - = [ Resolved oid it ] + = case uses it of [] => [ Resolved oid it ] + _ => [ Unresolved oid ] 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 ] @@ -135,4 +138,6 @@ merge it@(iit ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge nit t covering resolve : FfiItems -> FfiItems resolve it = let nit = normalize . { mods $= map resolve } $ it in - (merge nit) . resolveOne $ nit + f . f $ nit where + f : FfiItems -> FfiItems + f it = (merge it) . resolveOne $ it From 87890d91cc4d831f82bacc4bfeb07d84bf49e1c7 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:53:02 +0200 Subject: [PATCH 28/29] chore: wip --- notes/Demo.idr | 58 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 2f7e58fa229b2..e1be649bb49a3 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -106,38 +106,58 @@ resolveOne a@(it ** _) = Just (Left i) => [ oid |> Resolved $ { items := [ i ] } . empty $ mid ] Just (Right m) => - case uses m of - [] => [ oid |> Resolved $ { mods := [ m ] } . empty $ mid ] - _ => [ Unresolved oid ] + [ oid |> Resolved $ { mods := [ m ] } . empty $ mid ] Nothing => [ Unresolved oid ] resolveReexport oid Glob {prf = GlobWitness} it - = case uses it of [] => [ Resolved oid it ] - _ => [ Unresolved oid ] + = [ 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 -> FfiItems -merge (it ** _) [] = it +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 re nus iit 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 + 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 . f $ nit where - f : FfiItems -> FfiItems + let (it ** _) = f . f $ nit in it where + f : (i : FfiItems ** UngroupedItems i) + -> (i : FfiItems ** UngroupedItems i) f it = (merge it) . resolveOne $ it + +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 $ "" + +test2 : FfiItems +test2 = + let g := [ Name "TypeId", Name "Any" ] + in { uses := [ Path "std" (Path "any" (Group g)) ] } . empty $ "" + +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 $ "" From 0c419dd601581bd557e48fe1a79e99ca1e3f32f4 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:59:49 +0200 Subject: [PATCH 29/29] chore: wip --- notes/Demo.idr | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index e1be649bb49a3..dc9193876d0cc 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -137,11 +137,12 @@ merge it@(iit ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge nit t covering resolve : FfiItems -> FfiItems -resolve it = let nit = normalize . { mods $= map resolve } $ it in - let (it ** _) = f . f $ nit in it where - f : (i : FfiItems ** UngroupedItems i) - -> (i : FfiItems ** UngroupedItems i) - f it = (merge it) . resolveOne $ it +resolve it = let nit := normalize . { mods $= map resolve } $ it + (it ** _) := f . f $ nit + in it where + f : (i : FfiItems ** UngroupedItems i) + -> (i : FfiItems ** UngroupedItems i) + f it = (merge it) . resolveOne $ it test1 : FfiItems test1 = let bar := { items := [ "Foo" ] } . empty $ "bar"