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 001/100] 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 002/100] 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 003/100] 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 004/100] 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 005/100] 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 006/100] 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 007/100] 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 008/100] 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 009/100] 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 010/100] 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 011/100] 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 012/100] 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 013/100] 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 014/100] 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 015/100] 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 016/100] 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 017/100] 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 018/100] 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 019/100] 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 020/100] 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 021/100] 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 022/100] 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 023/100] 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 024/100] 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 025/100] 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 026/100] 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 027/100] 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 028/100] 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 029/100] 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" From 4b05ed430baab22b9a95b312ba4474902e2218fc Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:05:20 +0200 Subject: [PATCH 030/100] chore: wip --- notes/Demo.idr | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index dc9193876d0cc..3acc9b1d35319 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -137,24 +137,33 @@ 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 - (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 + in f nit where + e : FfiItems -> Nat + e (MkFfiItems _ _ _ us) = length us + f : (i : FfiItems ** UngroupedItems i) -> FfiItems + f a@(it ** _) = let na@(nit ** _) := (merge a) . resolveOne $ a + in case e it == e nit of True => nit + False => f na + +-- [NOTE]: the following tests comprise only test data. To test it out, the +-- Idris REPL is required. + +-- [NOTE]: this test is for the resolution function `resolve`. test1 : FfiItems test1 = let bar := { items := [ "Foo" ] } . empty $ "bar" foo := { mods := [ bar ] } . empty $ "foo" in { mods := [ foo ] , uses := [ Path "bar" Glob, Path "foo" Glob ] } . empty $ "" +-- [NOTE]: this test is for the normalization function `normalize`. test2 : FfiItems test2 = let g := [ Name "TypeId", Name "Any" ] in { uses := [ Path "std" (Path "any" (Group g)) ] } . empty $ "" +-- [NOTE]: this test is for the normalization function `normalize`. test3 : FfiItems test3 = let g := [ Path "foo" (Path "bar" (Group [ Name "Ty" @@ -162,3 +171,22 @@ test3 = , Name "TypeId" , Name "Any" ] in { uses := [ Path "std" (Path "any" (Group g)) ] } . empty $ "" + +-- [NOTE]: this test is for the resolution function `resolve`. +test4 : FfiItems +test4 = + let uses := [ Path "std" (Path "os" (Path "raw" (Name "c_void"))) + , Path "level1" Glob ] + level1 := { items := [ "Foo", "bar", "Word" ] } . empty $ "level1" + mods := [ level1 ] + items := [ "Array", "baz", "malloc" ] + in { items := items, mods := mods, uses := uses } . empty $ "" + +-- [NOTE]: this test is for the resolution function `resolve`. +test5 : FfiItems +test5 = + let foobar := { items := [ "Foo" ] } . empty $ "foobar" + bar := { mods := [ foobar ] } . empty $ "bar" + foo := { mods := [ bar ] } . empty $ "foo" + in { uses := [ Path "foobar" Glob, Path "bar" Glob, Path "foo" (Name "bar") ] + , mods := [ foo ] } . empty $ "" From 8ae9aaa004bb46743a9d3dd76febd3a0fedc803d Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:21:43 +0200 Subject: [PATCH 031/100] chore: wip --- notes/Demo.idr | 126 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 44 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 3acc9b1d35319..7f3185048a58e 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -14,6 +14,10 @@ data UseTree : Type where Path : String -> UseTree -> UseTree -- (themselves solely identifiers.) record FfiItems where constructor MkFfiItems + -- [NOTE]: the path here is only used for the purposes of keeping around a + -- stable hash of the module that we know to be unique among any other other + -- modules in the crate's module tree. + path : String ident : String items : List String mods : List FfiItems @@ -22,6 +26,11 @@ record FfiItems where data Resolution : Type where Resolved : UseTree -> FfiItems -> Resolution Unresolved : UseTree -> Resolution +record StatefulItems where + constructor MkState + state : FfiItems + new : FfiItems + -- [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. @@ -32,22 +41,22 @@ data Ungrouped : UseTree -> Type where NameWitness : Ungrouped (Name _) -> Ungrouped (Path _ t) data UngroupedItems : FfiItems -> Type where - EmptyWitness : UngroupedItems (MkFfiItems _ _ _ []) - Witness : {auto prfs : UngroupedItems (MkFfiItems mid is ms ts)} + EmptyWitness : UngroupedItems (MkFfiItems _ _ _ _ []) + Witness : {auto prfs : UngroupedItems (MkFfiItems mp mid is ms ts)} -> {auto prf : Ungrouped t} - -> UngroupedItems (MkFfiItems mid is ms (t :: ts)) + -> UngroupedItems (MkFfiItems mp mid is ms (t :: ts)) -empty : String -> FfiItems -empty s = MkFfiItems s [] [] [] +empty : String -> String -> FfiItems +empty p id = MkFfiItems p id [] [] [] -- [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 ((MkFfiItems _ _ _ _ []) ** _) = [] -ex a@(MkFfiItems mid is ms (h :: t) ** Witness {prfs} {prf}) = - let na = ((MkFfiItems mid is ms t) ** prfs) in +ex a@(MkFfiItems mp mid is ms (h :: t) ** Witness {prfs} {prf}) = + let na = ((MkFfiItems mp 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 @@ -57,11 +66,11 @@ ex a@(MkFfiItems mid is ms (h :: t) ** Witness {prfs} {prf}) = re : List (u : UseTree ** Ungrouped u) -> FfiItems -> (i : FfiItems ** UngroupedItems i) -re [] (MkFfiItems mid is ms _) = - (MkFfiItems mid is ms [] ** EmptyWitness) +re [] (MkFfiItems mp mid is ms _) = + (MkFfiItems mp 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}) + let (MkFfiItems mp mid is ms us ** w) = re t i in + (MkFfiItems mp mid is ms (h :: us) ** Witness {prfs = w} {prf = hw}) normalize : FfiItems -> (i : FfiItems ** UngroupedItems i) normalize it = let nl = (foldr f []) . uses $ it in re nl it where @@ -87,26 +96,28 @@ normalize it = let nl = (foldr f []) . uses $ it in re nl it where f' o@(Group l) = map (\t => f' $ assert_smaller o t) l |> join -resolveOne : (i : FfiItems ** UngroupedItems i) -> List Resolution -resolveOne a@(it ** _) = +resolveOne : (i : FfiItems ** UngroupedItems i) -> FfiItems -> 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 + f id (MkFfiItems _ _ is ms _) = + find c ((map Left is) ++ (map Right ms)) where + c : Either String FfiItems -> Bool + c (Left s) = s == id + c (Right (MkFfiItems _ mid _ _ _)) = mid == id resolveReexport : UseTree -> (t : UseTree) -> {auto 0 prf : Ungrouped t} -> FfiItems -> List Resolution - resolveReexport oid (Name id) {prf = NameWitness} it@(MkFfiItems mid _ _ _) + resolveReexport oid (Name id) {prf = NameWitness} + it@(MkFfiItems mp mid _ _ _) = case f id it of Just (Left i) => - [ oid |> Resolved $ { items := [ i ] } . empty $ mid ] + [ oid |> Resolved $ { items := [ i ] } . (empty mp) $ mid ] Just (Right m) => - [ oid |> Resolved $ { mods := [ m ] } . empty $ mid ] + [ oid |> Resolved $ { mods := [ m ] } . (empty mp) $ mid ] Nothing => [ Unresolved oid ] resolveReexport oid Glob {prf = GlobWitness} it @@ -119,9 +130,9 @@ resolveOne a@(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 +merge it@(_ ** _) [] = it +merge it@(_ ** _) ((Unresolved _) :: t) = merge it t +merge it@(iit ** _) ((Resolved oid (MkFfiItems _ _ is ms _)) :: t) = merge nit t where nit : (i : FfiItems ** UngroupedItems i) nit = let nus = (deleteBy f oid) . ex $ it in { items $= (++ is), mods $= (++ ms) } iit |> re nus where @@ -136,32 +147,58 @@ merge it@(iit ** _) ((Resolved oid (MkFfiItems _ is ms _)) :: t) = merge nit t False covering -resolve : FfiItems -> FfiItems -resolve it = let nit := normalize . { mods $= map resolve } $ it - in f nit where - e : FfiItems -> Nat - e (MkFfiItems _ _ _ us) = length us +updateMod : FfiItems -> FfiItems -> FfiItems +updateMod nit@(MkFfiItems nmp _ _ _ _) st@(MkFfiItems mp _ _ _ _) = + case nmp == mp of True => nit + False => f nit st + where + f : FfiItems -> FfiItems -> FfiItems + f nit@(MkFfiItems nmp _ _ _ _) st = + { mods $= (foldr f' []) . (map (updateMod nit)) } $ st where + f' : FfiItems -> List FfiItems -> List FfiItems + f' it@(MkFfiItems mp _ _ _ _) l = case mp == nmp of True => nit :: l + False => it :: l + +covering +resolveDriver : StatefulItems -> StatefulItems +resolveDriver (MkState { state = st, new = it }) = + let (st, nms) := (foldr r (st, [])) . mods $ it + nit := normalize . { mods := nms } $ it + in f nit st where + r : FfiItems + -> (FfiItems, List FfiItems) + -> (FfiItems, List FfiItems) + r it (st, lits) = + let inp := MkState { state = st, new = it } + (MkState { state = nst, new = nit }) := resolveDriver inp + in (nst, nit :: lits) + + f : (i : FfiItems ** UngroupedItems i) -> FfiItems -> StatefulItems + f a@(it ** _) st = let na@(nit ** _) := (merge a) . (resolveOne a) $ st + st := updateMod nit st + in case (length . uses $ it) == (length . uses $ nit) of + True => MkState { state = st, new = nit } + False => f na st - f : (i : FfiItems ** UngroupedItems i) -> FfiItems - f a@(it ** _) = let na@(nit ** _) := (merge a) . resolveOne $ a - in case e it == e nit of True => nit - False => f na +covering +resolve : FfiItems -> FfiItems +resolve it = MkState it it |> resolveDriver |> new -- [NOTE]: the following tests comprise only test data. To test it out, the -- Idris REPL is required. -- [NOTE]: this test is for the resolution function `resolve`. test1 : FfiItems -test1 = let bar := { items := [ "Foo" ] } . empty $ "bar" - foo := { mods := [ bar ] } . empty $ "foo" +test1 = let bar := { items := [ "Foo" ] } . (empty "foo::bar") $ "bar" + foo := { mods := [ bar ] } . (empty "foo") $ "foo" in { mods := [ foo ] - , uses := [ Path "bar" Glob, Path "foo" Glob ] } . empty $ "" + , uses := [ Path "bar" Glob, Path "foo" Glob ] } . (empty "") $ "" -- [NOTE]: this test is for the normalization function `normalize`. test2 : FfiItems test2 = let g := [ Name "TypeId", Name "Any" ] - in { uses := [ Path "std" (Path "any" (Group g)) ] } . empty $ "" + in { uses := [ Path "std" (Path "any" (Group g)) ] } . (empty "") $ "" -- [NOTE]: this test is for the normalization function `normalize`. test3 : FfiItems @@ -170,23 +207,24 @@ test3 = , Path "test" (Name "foobar") ])) , Name "TypeId" , Name "Any" ] - in { uses := [ Path "std" (Path "any" (Group g)) ] } . empty $ "" + in { uses := [ Path "std" (Path "any" (Group g)) ] } . (empty "") $ "" -- [NOTE]: this test is for the resolution function `resolve`. test4 : FfiItems test4 = let uses := [ Path "std" (Path "os" (Path "raw" (Name "c_void"))) , Path "level1" Glob ] - level1 := { items := [ "Foo", "bar", "Word" ] } . empty $ "level1" + id := "level1" + level1 := { items := [ "Foo", "bar", "Word" ] } . (empty id) $ id mods := [ level1 ] items := [ "Array", "baz", "malloc" ] - in { items := items, mods := mods, uses := uses } . empty $ "" + in { items := items, mods := mods, uses := uses } . (empty "") $ "" -- [NOTE]: this test is for the resolution function `resolve`. test5 : FfiItems test5 = - let foobar := { items := [ "Foo" ] } . empty $ "foobar" - bar := { mods := [ foobar ] } . empty $ "bar" - foo := { mods := [ bar ] } . empty $ "foo" + let foobar := { items := [ "Foo" ] } . (empty "foo::bar::foobar") $ "foobar" + bar := { mods := [ foobar ] } . (empty "foo::bar") $ "bar" + foo := { mods := [ bar ] } . (empty "foo") $ "foo" in { uses := [ Path "foobar" Glob, Path "bar" Glob, Path "foo" (Name "bar") ] - , mods := [ foo ] } . empty $ "" + , mods := [ foo ] } . (empty "") $ "" From 1bce5b470068e4f6f4e20155f27502d3ab577e71 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:20:33 +0200 Subject: [PATCH 032/100] chore: wip --- notes/Demo.idr | 247 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 169 insertions(+), 78 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 7f3185048a58e..58a459f1737fc 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -1,27 +1,52 @@ module Demo import Data.List +import Data.List.Quantifiers +import Data.List1 %default total -data UseTree : Type where Path : String -> UseTree -> UseTree - Name : String -> UseTree - Glob : UseTree - Group : List UseTree -> UseTree +namespace UseTree + public export + data UseTree : Type where Path : String -> UseTree -> UseTree + Name : String -> UseTree + Glob : UseTree + Group : List UseTree -> UseTree + +namespace RootUseTree + public export + data RootUseTree : Type where + Path : (s : String) + -> { auto prf : Not (s = "super") } + -> RootUseTree + -> RootUseTree + Name : (s : String) -> { auto prf : Not (s = "super") } -> RootUseTree + Group : List RootUseTree -> RootUseTree + Glob : RootUseTree + +ModulePath : Type +ModulePath = List String + +data ModuleKind = Root | Node + +determineModulePath : ModuleKind -> Type +determineModulePath Root = Void +determineModulePath Node = List1 String + +determineModuleUseTree : ModuleKind -> Type +determineModuleUseTree Root = RootUseTree +determineModuleUseTree Node = 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 +record FfiItems (s : ModuleKind) where constructor MkFfiItems - -- [NOTE]: the path here is only used for the purposes of keeping around a - -- stable hash of the module that we know to be unique among any other other - -- modules in the crate's module tree. - path : String + path : determineModulePath s ident : String items : List String - mods : List FfiItems - uses : List UseTree + mods : List (FfiItems Node) + uses : List (determineModuleUseTree s) data Resolution : Type where Resolved : UseTree -> FfiItems -> Resolution Unresolved : UseTree -> Resolution @@ -31,22 +56,70 @@ record StatefulItems where state : FfiItems new : FfiItems --- [NOTE]: the next two types are only used as proof witnesses and will not get +-- [NOTE]: the next types are only used as proof witnesses and will not get -- ported over to Rust. Instead, a new, refined type will be used in Rust to -- express the constraints imposed by the below dependent types. -data Ungrouped : UseTree -> Type where NameWitness : Ungrouped (Name _) - GlobWitness : Ungrouped Glob - PathWitness : {auto prf : Ungrouped t} - -> Ungrouped (Path _ t) +namespace Ungrouped + public export + data Ungrouped : UseTree -> Type where Name : Ungrouped (Name _) + Glob : Ungrouped Glob + Path : {auto prf : Ungrouped t} + -> Ungrouped (Path _ t) + + public export + data UngroupedItems : FfiItems -> Type where + Empty : UngroupedItems (MkFfiItems _ _ _ _ []) + Witness : {auto prfs : UngroupedItems (MkFfiItems mp mid is ms ts)} + -> {auto prf : Ungrouped t} + -> UngroupedItems (MkFfiItems mp mid is ms (t :: ts)) -data UngroupedItems : FfiItems -> Type where - EmptyWitness : UngroupedItems (MkFfiItems _ _ _ _ []) - Witness : {auto prfs : UngroupedItems (MkFfiItems mp mid is ms ts)} - -> {auto prf : Ungrouped t} - -> UngroupedItems (MkFfiItems mp mid is ms (t :: ts)) +-- [NOTE]: this is just a forward definition of the proof that is given later on +-- to show that a module has a non-empty path. It is needed for the proof that a +-- given module is the root module, and using a mutual block often negatively +-- impacts the type-checker. +namespace NodeModule + public export + data NodeModule : FfiItems -> Type -empty : String -> String -> FfiItems +namespace RootModule + namespace Constraints + export + data IdUseTree : UseTree -> Type where Name : IdUseTree (Name _) + Path : IdUseTree (Path _ _) + + export + data NonIdUseTree : UseTree -> Type where Glob : NonIdUseTree Glob + Group : NonIdUseTree (Group _) + + public export + data RootModule : FfiItems -> Type where + Empty : RootModule (MkFfiItems [] "" _ [] []) + Module : { auto prfs : RootModule (MkFfiItems mp mid is ms us) } + -> { auto prf : NodeModule nm } + -> RootModule (MkFfiItems mp mid is (nm :: ms) us) + Name : { auto prfs : RootModule (MkFfiItems mp mid is ms us) } + -> { auto prf1 : Not (s = "super") } + -> { auto prf2 : IdUseTree (Name s) } + -> RootModule (MkFfiItems mp mid is ms ((Name s) :: us)) + Path : { auto prfs : RootModule (MkFfiItems mp mid is ms us) } + -> { auto prf1 : Not (s = "super") } + -> { auto prf2 : IdUseTree (Path s t) } + -> RootModule (MkFfiItems mp mid is ms ((Path s t) :: us)) + Other : { auto prfs : RootModule (MkFfiItems mp mid is ms us) } + -> { auto prf : NonIdUseTree o } + -> RootModule (MkFfiItems mp mid is ms (o :: us)) + +namespace NodeModule + public export + data NodeModule : FfiItems -> Type where + Empty : { auto prf : Not (s = "") } + -> NodeModule (MkFfiItems (_ :: _) s _ [] _) + Module : { auto prfs : NodeModule (MkFfiItems mp mid is ms us) } + -> { auto prf : NodeModule nm } + -> NodeModule (MkFfiItems mp mid is (nm :: ms) us) + +empty : ModulePath -> String -> FfiItems empty p id = MkFfiItems p id [] [] [] -- [NOTE]: this deconstructs a module that is proven to contain no group @@ -67,7 +140,7 @@ re : List (u : UseTree ** Ungrouped u) -> FfiItems -> (i : FfiItems ** UngroupedItems i) re [] (MkFfiItems mp mid is ms _) = - (MkFfiItems mp mid is ms [] ** EmptyWitness) + (MkFfiItems mp mid is ms [] ** Empty) re ((h ** hw) :: t) i = let (MkFfiItems mp mid is ms us ** w) = re t i in (MkFfiItems mp mid is ms (h :: us) ** Witness {prfs = w} {prf = hw}) @@ -88,17 +161,18 @@ normalize it = let nl = (foldr f []) . uses $ it in re nl it where f = (++) . f' where f' : UseTree -> List (u : UseTree ** Ungrouped u) f' (Name id) = - [ ((Name id) ** NameWitness) ] + [ ((Name id) ** Name) ] f' Glob = - [ (Glob ** GlobWitness) ] + [ (Glob ** Glob) ] f' (Path id t) = - map (\(t ** w) => (Path id t ** PathWitness {prf = w})) (f' t) + map (\(t ** w) => (Path id t ** Path {prf = w})) (f' t) f' o@(Group l) = map (\t => f' $ assert_smaller o t) l |> join resolveOne : (i : FfiItems ** UngroupedItems i) -> FfiItems -> List Resolution -resolveOne a@(it ** _) _ = - join . (map $ \(u ** prf) => resolveReexport u u {prf} it) . ex $ a where +resolveOne a@(it ** _) st = + let p := MkState { state = st, new = it } + in join . (map $ \(u ** prf) => resolveReexport u u {prf} p) . ex $ a where f : String -> FfiItems -> Maybe (Either String FfiItems) f id (MkFfiItems _ _ is ms _) = find c ((map Left is) ++ (map Right ms)) where @@ -106,13 +180,26 @@ resolveOne a@(it ** _) _ = c (Left s) = s == id c (Right (MkFfiItems _ mid _ _ _)) = mid == id + trimSuper : (t : UseTree ** Ungrouped t) + -> (Nat, (t : UseTree ** Ungrouped t)) + trimSuper t = f t Z where + f : (t : UseTree ** Ungrouped t) + -> Nat + -> (Nat, (t : UseTree ** Ungrouped t)) + f (Path "super" nt ** Path {prf}) s = S s |> f (nt ** prf) + f t@(_ ** _) s = (s, t) + + findSuper : FfiItems -> ModulePath -> FfiItems + -- findSuper it p = let np := reverse . tail . reverse $ p + -- in ?rhs + resolveReexport : UseTree -> (t : UseTree) -> {auto 0 prf : Ungrouped t} - -> FfiItems + -> StatefulItems -> List Resolution - resolveReexport oid (Name id) {prf = NameWitness} - it@(MkFfiItems mp mid _ _ _) + resolveReexport oid (Name id) {prf = Name} + (MkState { state = _, new = it@(MkFfiItems mp mid _ _ _) }) = case f id it of Just (Left i) => [ oid |> Resolved $ { items := [ i ] } . (empty mp) $ mid ] @@ -120,10 +207,14 @@ resolveOne a@(it ** _) _ = [ oid |> Resolved $ { mods := [ m ] } . (empty mp) $ mid ] Nothing => [ Unresolved oid ] - resolveReexport oid Glob {prf = GlobWitness} it + resolveReexport oid Glob {prf = Glob} + (MkState { state = _, new = it }) = [ Resolved oid it ] - resolveReexport oid (Path id t) {prf = PathWitness {prf}} it - = case f id it of Just (Right m) => resolveReexport oid t {prf} m + resolveReexport oid (Path id t) {prf = Path {prf}} + (MkState { state = st, new = it }) + = case f id it of Just (Right m) => + let np := MkState { state = st, new = m } + in resolveReexport oid t {prf} np Just _ => [ Unresolved oid ] Nothing => [ Unresolved oid ] @@ -141,7 +232,7 @@ merge it@(iit ** _) ((Resolved oid (MkFfiItems _ _ is ms _)) :: t) = merge nit t id1 == id2 f Glob (Glob ** _) = True - f (Path id1 t1) (Path id2 t2 ** PathWitness {prf}) = + f (Path id1 t1) (Path id2 t2 ** Path {prf}) = id1 == id2 && f t1 (t2 ** prf) f _ _ = False @@ -162,23 +253,23 @@ updateMod nit@(MkFfiItems nmp _ _ _ _) st@(MkFfiItems mp _ _ _ _) = covering resolveDriver : StatefulItems -> StatefulItems resolveDriver (MkState { state = st, new = it }) = - let (st, nms) := (foldr r (st, [])) . mods $ it - nit := normalize . { mods := nms } $ it - in f nit st where + let (nst, nms) := (foldr r (st, [])) . mods $ it + nit := normalize . { mods := nms } $ it + in f nit nst where r : FfiItems -> (FfiItems, List FfiItems) -> (FfiItems, List FfiItems) - r it (st, lits) = - let inp := MkState { state = st, new = it } - (MkState { state = nst, new = nit }) := resolveDriver inp - in (nst, nit :: lits) + r it (st, its) = + let (MkState { state = nst, new = nit }) := + resolveDriver (MkState { state = st , new = it }) + in (nst, nit :: its) f : (i : FfiItems ** UngroupedItems i) -> FfiItems -> StatefulItems - f a@(it ** _) st = let na@(nit ** _) := (merge a) . (resolveOne a) $ st - st := updateMod nit st + f a@(it ** _) st = let na@(nit ** _) := st |> resolveOne a |> merge a + nst := updateMod nit st in case (length . uses $ it) == (length . uses $ nit) of - True => MkState { state = st, new = nit } - False => f na st + True => MkState { state = nst, new = nit } + False => f na nst covering resolve : FfiItems -> FfiItems @@ -188,43 +279,43 @@ resolve it = MkState it it |> resolveDriver |> new -- Idris REPL is required. -- [NOTE]: this test is for the resolution function `resolve`. -test1 : FfiItems -test1 = let bar := { items := [ "Foo" ] } . (empty "foo::bar") $ "bar" - foo := { mods := [ bar ] } . (empty "foo") $ "foo" - in { mods := [ foo ] - , uses := [ Path "bar" Glob, Path "foo" Glob ] } . (empty "") $ "" +-- test1 : FfiItems +-- test1 = let bar := { items := [ "Foo" ] } . (empty "foo::bar") $ "bar" +-- foo := { mods := [ bar ] } . (empty "foo") $ "foo" +-- in { mods := [ foo ] +-- , uses := [ Path "bar" Glob, Path "foo" Glob ] } . (empty "") $ "" -- [NOTE]: this test is for the normalization function `normalize`. -test2 : FfiItems -test2 = - let g := [ Name "TypeId", Name "Any" ] - in { uses := [ Path "std" (Path "any" (Group g)) ] } . (empty "") $ "" +-- test2 : FfiItems +-- test2 = +-- let g := [ Name "TypeId", Name "Any" ] +-- in { uses := [ Path "std" (Path "any" (Group g)) ] } . (empty "") $ "" -- [NOTE]: this test is for the normalization function `normalize`. -test3 : FfiItems -test3 = - let g := [ Path "foo" (Path "bar" (Group [ Name "Ty" - , Path "test" (Name "foobar") ])) - , Name "TypeId" - , Name "Any" ] - in { uses := [ Path "std" (Path "any" (Group g)) ] } . (empty "") $ "" +-- test3 : FfiItems +-- test3 = +-- let g := [ Path "foo" (Path "bar" (Group [ Name "Ty" +-- , Path "test" (Name "foobar") ])) +-- , Name "TypeId" +-- , Name "Any" ] +-- in { uses := [ Path "std" (Path "any" (Group g)) ] } . (empty "") $ "" -- [NOTE]: this test is for the resolution function `resolve`. -test4 : FfiItems -test4 = - let uses := [ Path "std" (Path "os" (Path "raw" (Name "c_void"))) - , Path "level1" Glob ] - id := "level1" - level1 := { items := [ "Foo", "bar", "Word" ] } . (empty id) $ id - mods := [ level1 ] - items := [ "Array", "baz", "malloc" ] - in { items := items, mods := mods, uses := uses } . (empty "") $ "" +-- test4 : FfiItems +-- test4 = +-- let uses := [ Path "std" (Path "os" (Path "raw" (Name "c_void"))) +-- , Path "level1" Glob ] +-- id := "level1" +-- level1 := { items := [ "Foo", "bar", "Word" ] } . (empty id) $ id +-- mods := [ level1 ] +-- items := [ "Array", "baz", "malloc" ] +-- in { items := items, mods := mods, uses := uses } . (empty "") $ "" -- [NOTE]: this test is for the resolution function `resolve`. -test5 : FfiItems -test5 = - let foobar := { items := [ "Foo" ] } . (empty "foo::bar::foobar") $ "foobar" - bar := { mods := [ foobar ] } . (empty "foo::bar") $ "bar" - foo := { mods := [ bar ] } . (empty "foo") $ "foo" - in { uses := [ Path "foobar" Glob, Path "bar" Glob, Path "foo" (Name "bar") ] - , mods := [ foo ] } . (empty "") $ "" +-- test5 : FfiItems +-- test5 = +-- let foobar := { items := [ "Foo" ] } . (empty "foo::bar::foobar") $ "foobar" +-- bar := { mods := [ foobar ] } . (empty "foo::bar") $ "bar" +-- foo := { mods := [ bar ] } . (empty "foo") $ "foo" +-- in { uses := [ Path "foobar" Glob, Path "bar" Glob, Path "foo" (Name "bar") ] +-- , mods := [ foo ] } . (empty "") $ "" From c9507dd0058b9a545833b68077cf61a2ce5e157a Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:59:50 +0200 Subject: [PATCH 033/100] chore: wip --- ctest/src/ffi_items.rs | 83 ++++++++-- notes/Demo.idr | 349 +++++++++++++++++++---------------------- 2 files changed, 231 insertions(+), 201 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 43e652c428524..5db26d23b10e1 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -39,6 +39,7 @@ pub(crate) struct FfiItems { pub(crate) constants: Vec, pub(crate) foreign_functions: Vec, pub(crate) foreign_statics: Vec, + pub(crate) uses: Vec, pub(crate) modules: Vec, /// This is used while recursing through parsed modules to gather absolute @@ -106,6 +107,7 @@ impl Default for FfiItems { foreign_functions: Default::default(), foreign_statics: Default::default(), modules: Default::default(), + uses: Default::default(), current_module: syn::Path { leading_colon: None, segments: Default::default(), @@ -336,19 +338,74 @@ 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); + let syn::ItemMod { vis, ident, content: Some((_, mod_items)), .. } = i else { + unreachable!("this runs post cargo-expand, which inlines all modules"); + }; + let uses: Vec<_> = mod_items + .iter() + .cloned() + .filter_map(|it| { + if let syn::Item::Use(it) = it { + Some(it) + } else { + None + } + }) + .collect(); + + // [NOTE]: if the module is known to keep be the virtual module that we + // create to process the items in the crate root, then we require not + // creating a new module that will become a child to the current one, + // but rather make all items in the module become the items of the + // current module (the crate root.) + if ident == "__ctest_root_mod" { + mod_items.iter().for_each(|it| { + match it { + Type(t) => visit::visit_item_type(self, &t) + Struct(s) => visit::visit_item_struct(self, &s), + Union(u) => visit::visit_item_union(self, &u), + Const(c) => visit::visit_item_const(self, &c), + ForeginMod(m) => visit::visit_item_foreign_mod(self, &md), + } + }); + self.uses = uses; + } else { + 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, + }); + } + } +} - self.modules.push(Module { - public, - ident, - path, - items, - }); +#[test] +fn tmp() { + let source = r#" + use std::*; + + fn main() { + use std::any::*; } + "#; + let mut items = FfiItems::default(); + let syn::File { attrs, items: mod_items, .. } = syn::parse_file(source).unwrap(); + let file_mod: syn::ItemMod = syn::ItemMod { + attrs: attrs, + vis: syn::parse_quote! { pub }, + unsafety: None, + mod_token: syn::token::Mod::default(), + ident: syn::Ident::new("__ctest_root_mod", proc_macro2::Span::call_site()), + content: Some((syn::token::Brace::default(), mod_items)), + semi: None, + }; + items.visit_item_mod(&file_mod); + println!("{:#?}", items.modules.last().unwrap()); } diff --git a/notes/Demo.idr b/notes/Demo.idr index 58a459f1737fc..90df9e66638d4 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -1,8 +1,7 @@ module Demo import Data.List -import Data.List.Quantifiers -import Data.List1 +import Data.Vect %default total @@ -13,53 +12,29 @@ namespace UseTree Glob : UseTree Group : List UseTree -> UseTree -namespace RootUseTree - public export - data RootUseTree : Type where - Path : (s : String) - -> { auto prf : Not (s = "super") } - -> RootUseTree - -> RootUseTree - Name : (s : String) -> { auto prf : Not (s = "super") } -> RootUseTree - Group : List RootUseTree -> RootUseTree - Glob : RootUseTree - -ModulePath : Type -ModulePath = List String - -data ModuleKind = Root | Node - -determineModulePath : ModuleKind -> Type -determineModulePath Root = Void -determineModulePath Node = List1 String - -determineModuleUseTree : ModuleKind -> Type -determineModuleUseTree Root = RootUseTree -determineModuleUseTree Node = 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 (s : ModuleKind) where - constructor MkFfiItems - path : determineModulePath s - ident : String - items : List String - mods : List (FfiItems Node) - uses : List (determineModuleUseTree s) - -data Resolution : Type where Resolved : UseTree -> FfiItems -> Resolution +ModulePath : Nat -> Type +ModulePath n = Vect n String + +namespace FfiItems + record FfiItems (n : Nat) where + constructor MkFfiItems + path : Vect n String + ident : String + items : List String + mods : List (FfiItems (S n)) + uses : List UseTree + +data Resolution : Type where Resolved : UseTree -> FfiItems _ -> Resolution Unresolved : UseTree -> Resolution -record StatefulItems where +record StatefulItems (n : Nat) where constructor MkState - state : FfiItems - new : FfiItems - --- [NOTE]: the next 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. + state : FfiItems Z + current : FfiItems n +-- [NOTE]: these 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. namespace Ungrouped public export data Ungrouped : UseTree -> Type where Name : Ungrouped (Name _) @@ -68,64 +43,19 @@ namespace Ungrouped -> Ungrouped (Path _ t) public export - data UngroupedItems : FfiItems -> Type where + data UngroupedItems : FfiItems _ -> Type where Empty : UngroupedItems (MkFfiItems _ _ _ _ []) Witness : {auto prfs : UngroupedItems (MkFfiItems mp mid is ms ts)} -> {auto prf : Ungrouped t} -> UngroupedItems (MkFfiItems mp mid is ms (t :: ts)) --- [NOTE]: this is just a forward definition of the proof that is given later on --- to show that a module has a non-empty path. It is needed for the proof that a --- given module is the root module, and using a mutual block often negatively --- impacts the type-checker. -namespace NodeModule - public export - data NodeModule : FfiItems -> Type - -namespace RootModule - namespace Constraints - export - data IdUseTree : UseTree -> Type where Name : IdUseTree (Name _) - Path : IdUseTree (Path _ _) - - export - data NonIdUseTree : UseTree -> Type where Glob : NonIdUseTree Glob - Group : NonIdUseTree (Group _) - - public export - data RootModule : FfiItems -> Type where - Empty : RootModule (MkFfiItems [] "" _ [] []) - Module : { auto prfs : RootModule (MkFfiItems mp mid is ms us) } - -> { auto prf : NodeModule nm } - -> RootModule (MkFfiItems mp mid is (nm :: ms) us) - Name : { auto prfs : RootModule (MkFfiItems mp mid is ms us) } - -> { auto prf1 : Not (s = "super") } - -> { auto prf2 : IdUseTree (Name s) } - -> RootModule (MkFfiItems mp mid is ms ((Name s) :: us)) - Path : { auto prfs : RootModule (MkFfiItems mp mid is ms us) } - -> { auto prf1 : Not (s = "super") } - -> { auto prf2 : IdUseTree (Path s t) } - -> RootModule (MkFfiItems mp mid is ms ((Path s t) :: us)) - Other : { auto prfs : RootModule (MkFfiItems mp mid is ms us) } - -> { auto prf : NonIdUseTree o } - -> RootModule (MkFfiItems mp mid is ms (o :: us)) - -namespace NodeModule - public export - data NodeModule : FfiItems -> Type where - Empty : { auto prf : Not (s = "") } - -> NodeModule (MkFfiItems (_ :: _) s _ [] _) - Module : { auto prfs : NodeModule (MkFfiItems mp mid is ms us) } - -> { auto prf : NodeModule nm } - -> NodeModule (MkFfiItems mp mid is (nm :: ms) us) - -empty : ModulePath -> String -> FfiItems +empty : ModulePath n -> String -> FfiItems n empty p id = MkFfiItems p id [] [] [] -- [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 : (i : FfiItems _ ** UngroupedItems i) -> List (u : UseTree ** Ungrouped u) ex ((MkFfiItems _ _ _ _ []) ** _) = [] ex a@(MkFfiItems mp mid is ms (h :: t) ** Witness {prfs} {prf}) = @@ -133,23 +63,22 @@ ex a@(MkFfiItems mp mid is ms (h :: t) ** Witness {prfs} {prf}) = (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 +-- 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) + -> FfiItems n + -> (i : FfiItems n ** UngroupedItems i) re [] (MkFfiItems mp mid is ms _) = (MkFfiItems mp mid is ms [] ** Empty) re ((h ** hw) :: t) i = let (MkFfiItems mp mid is ms us ** w) = re t i in (MkFfiItems mp mid is ms (h :: us) ** Witness {prfs = w} {prf = hw}) -normalize : FfiItems -> (i : FfiItems ** UngroupedItems i) +normalize : FfiItems n -> (i : FfiItems n ** 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 + -- trees ro(t : Nat)oted 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 @@ -169,111 +98,155 @@ normalize it = let nl = (foldr f []) . uses $ it in re nl it where f' o@(Group l) = map (\t => f' $ assert_smaller o t) l |> join -resolveOne : (i : FfiItems ** UngroupedItems i) -> FfiItems -> List Resolution -resolveOne a@(it ** _) st = - let p := MkState { state = st, new = it } - in join . (map $ \(u ** prf) => resolveReexport u u {prf} p) . 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 - - trimSuper : (t : UseTree ** Ungrouped t) - -> (Nat, (t : UseTree ** Ungrouped t)) - trimSuper t = f t Z where - f : (t : UseTree ** Ungrouped t) - -> Nat - -> (Nat, (t : UseTree ** Ungrouped t)) - f (Path "super" nt ** Path {prf}) s = S s |> f (nt ** prf) - f t@(_ ** _) s = (s, t) - - findSuper : FfiItems -> ModulePath -> FfiItems - -- findSuper it p = let np := reverse . tail . reverse $ p - -- in ?rhs - - resolveReexport : UseTree - -> (t : UseTree) - -> {auto 0 prf : Ungrouped t} - -> StatefulItems - -> List Resolution - resolveReexport oid (Name id) {prf = Name} - (MkState { state = _, new = it@(MkFfiItems mp mid _ _ _) }) - = case f id it of - Just (Left i) => - [ oid |> Resolved $ { items := [ i ] } . (empty mp) $ mid ] - Just (Right m) => - [ oid |> Resolved $ { mods := [ m ] } . (empty mp) $ mid ] - Nothing => - [ Unresolved oid ] - resolveReexport oid Glob {prf = Glob} - (MkState { state = _, new = it }) - = [ Resolved oid it ] - resolveReexport oid (Path id t) {prf = Path {prf}} - (MkState { state = st, new = it }) - = case f id it of Just (Right m) => - let np := MkState { state = st, new = m } - in resolveReexport oid t {prf} np - Just _ => [ Unresolved oid ] - Nothing => [ Unresolved oid ] - -merge : (i : FfiItems ** UngroupedItems i) +-- [TODO]: reimplement this. +f : String -> FfiItems n -> Maybe (Either String (FfiItems (S n))) + +samePath : ModulePath _ -> ModulePath _ -> Bool +samePath [] [] = True +samePath (h1 :: t1) (h2 :: t2) = h1 == h2 && samePath t1 t2 +samePath _ _ = False + +covering +findParent : StatefulItems (S n) -> Maybe (FfiItems n) +findParent (MkState { state = st, current = (MkFfiItems cmp _ _ _ _) }) = + f st where r : FfiItems _ -> Maybe (FfiItems n) -> Maybe (FfiItems n) + f : FfiItems _ -> Maybe (FfiItems n) + r it Nothing = f it + r _ p = p + f it@(MkFfiItems mp _ _ ms _) = + case samePath (reverse . tail . reverse $ cmp) mp of + True => believe_me $ Just it + False => foldr r Nothing ms + +namespace ResolveNode + public export partial + resolveReexport : UseTree + -> (t : UseTree) + -> {auto 0 prf : Ungrouped t} + -> StatefulItems (S _) + -> List Resolution + resolveReexport oid (Name id) {prf = Name} + st@(MkState { state = _ + , current = it@(MkFfiItems mp mid _ _ _) }) + = case id == "super" of + True => case findParent st of + Just m => + [ oid |> Resolved $ + { mods := [ m ] } . (empty mp) $ mid ] + Nothing => idris_crash "[TODO]" + False => case f id it of + Just (Left i) => + [ oid |> Resolved $ + { items := [ i ] } . (empty mp) $ mid ] + Just (Right m) => + [ oid |> Resolved $ + { mods := [ m ] } . (empty mp) $ mid ] + Nothing => + [ Unresolved oid ] + resolveReexport oid Glob {prf = Glob} (MkState { state = _, current = it }) + = [ Resolved oid it ] + resolveReexport oid (Path id t) {prf = Path {prf}} + ist@(MkState { state = st, current = it }) + = case id == "super" of + True => case findParent ist of + Just m => ?rhspath + Nothing => idris_crash "[TODO]" + False => case f id it of + Just (Right m) => + let np := MkState { state = st, current = m } + in ResolveNode.resolveReexport oid t {prf} np + Just _ => [ Unresolved oid ] + Nothing => [ Unresolved oid ] + +namespace ResolveRoot + public export + resolveReexport : UseTree + -> (t : UseTree) + -> {auto 0 prf : Ungrouped t} + -> StatefulItems _ + -> List Resolution + resolveReexport oid (Name id) {prf = Name} + (MkState { state = _, current = it@(MkFfiItems mp mid _ _ _) }) + = case f id it of + Just (Left i) => + [ oid |> Resolved $ { items := [ i ] } . (empty mp) $ mid ] + Just (Right m) => + [ oid |> Resolved $ { mods := [ m ] } . (empty mp) $ mid ] + Nothing => + [ Unresolved oid ] + resolveReexport oid Glob {prf = Glob} (MkState { state = _, current = it }) + = [ Resolved oid it ] + resolveReexport oid (Path id t) {prf = Path {prf}} + (MkState { state = st, current = it }) + = case f id it of Just (Right m) => + let np := MkState { state = st, current = m } + in ResolveRoot.resolveReexport oid t {prf} np + Just _ => [ Unresolved oid ] + Nothing => [ Unresolved oid ] + + +partial +resolveOne : { n : _ } + -> (i : FfiItems n ** UngroupedItems i) + -> FfiItems Z + -> List Resolution +resolveOne a@(it ** _) st = join . (map $ f) . ex $ a where + p : StatefulItems n + p = MkState st it + + f : (u : UseTree ** Ungrouped u) -> List Resolution + f (u ** prf) = case n of + Z => ResolveRoot.resolveReexport u u {prf} p + S _ => ResolveNode.resolveReexport u u {prf} p + + +merge : (i : FfiItems n ** UngroupedItems i) -> List Resolution - -> (i : FfiItems ** UngroupedItems i) + -> (i : FfiItems n ** UngroupedItems i) merge it@(_ ** _) [] = it merge it@(_ ** _) ((Unresolved _) :: t) = merge it t merge it@(iit ** _) ((Resolved oid (MkFfiItems _ _ is ms _)) :: t) = merge nit t - where nit : (i : FfiItems ** UngroupedItems i) - nit = let nus = (deleteBy f oid) . ex $ it - in { items $= (++ is), mods $= (++ ms) } iit |> re nus where - f : UseTree -> (u : UseTree ** Ungrouped u) -> Bool - f (Name id1) (Name id2 ** _) = - id1 == id2 - f Glob (Glob ** _) = - True - f (Path id1 t1) (Path id2 t2 ** Path {prf}) = - id1 == id2 && f t1 (t2 ** prf) - f _ _ = - False - + where nit : (i : FfiItems n ** UngroupedItems i) +-- nit = let nus = (deleteBy f oid) . ex $ it +-- in { items $= (++ is), mods $= (++ ms) } iit |> re nus where +-- f : UseTree -> (u : UseTree ** Ungrouped u) -> Bool +-- f (Name id1) (Name id2 ** _) = +-- id1 == id2 +-- f Glob (Glob ** _) = +-- True +-- f (Path id1 t1) (Path id2 t2 ** Path {prf}) = +-- id1 == id2 && f t1 (t2 ** prf) +-- f _ _ = +-- False + +-- [TODO]: implement this function correctly. covering -updateMod : FfiItems -> FfiItems -> FfiItems -updateMod nit@(MkFfiItems nmp _ _ _ _) st@(MkFfiItems mp _ _ _ _) = - case nmp == mp of True => nit - False => f nit st - where - f : FfiItems -> FfiItems -> FfiItems - f nit@(MkFfiItems nmp _ _ _ _) st = - { mods $= (foldr f' []) . (map (updateMod nit)) } $ st where - f' : FfiItems -> List FfiItems -> List FfiItems - f' it@(MkFfiItems mp _ _ _ _) l = case mp == nmp of True => nit :: l - False => it :: l +updateMod : FfiItems n -> FfiItems Z -> FfiItems Z -covering -resolveDriver : StatefulItems -> StatefulItems -resolveDriver (MkState { state = st, new = it }) = +partial +resolveDriver : { n : _ } -> StatefulItems n -> StatefulItems n +resolveDriver (MkState { state = st, current = it }) = let (nst, nms) := (foldr r (st, [])) . mods $ it nit := normalize . { mods := nms } $ it in f nit nst where - r : FfiItems - -> (FfiItems, List FfiItems) - -> (FfiItems, List FfiItems) + r : FfiItems (S n) + -> (FfiItems Z, List (FfiItems (S n))) + -> (FfiItems Z, List (FfiItems (S n))) r it (st, its) = - let (MkState { state = nst, new = nit }) := - resolveDriver (MkState { state = st , new = it }) + let (MkState { state = nst, current = nit }) := + resolveDriver (MkState { state = st , current = it }) in (nst, nit :: its) - f : (i : FfiItems ** UngroupedItems i) -> FfiItems -> StatefulItems + f : (i : FfiItems n ** UngroupedItems i) -> FfiItems Z -> StatefulItems n f a@(it ** _) st = let na@(nit ** _) := st |> resolveOne a |> merge a nst := updateMod nit st in case (length . uses $ it) == (length . uses $ nit) of - True => MkState { state = nst, new = nit } + True => MkState { state = nst, current = nit } False => f na nst -covering -resolve : FfiItems -> FfiItems -resolve it = MkState it it |> resolveDriver |> new +partial +resolve : FfiItems Z -> FfiItems Z +resolve it = MkState it it |> resolveDriver |> current -- [NOTE]: the following tests comprise only test data. To test it out, the -- Idris REPL is required. From 3c4553d29b75c3d47b78b450b122d6809dcb577b Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:46:47 +0200 Subject: [PATCH 034/100] chore: wip --- notes/Demo.idr | 88 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/notes/Demo.idr b/notes/Demo.idr index 90df9e66638d4..2cb98bc4dc037 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -24,8 +24,15 @@ namespace FfiItems mods : List (FfiItems (S n)) uses : List UseTree -data Resolution : Type where Resolved : UseTree -> FfiItems _ -> Resolution - Unresolved : UseTree -> Resolution +record ResolvedItem where + constructor MkResolved + items : List String + mods : List (FfiItems depth) + uses : List UseTree + +data Resolution : Type where + Resolved : UseTree -> ResolvedItem -> Resolution + Unresolved : UseTree -> Resolution record StatefulItems (n : Nat) where constructor MkState @@ -132,19 +139,28 @@ namespace ResolveNode True => case findParent st of Just m => [ oid |> Resolved $ - { mods := [ m ] } . (empty mp) $ mid ] + MkResolved { items = [ ] + , mods = [ m ] + , uses = [ ] } ] Nothing => idris_crash "[TODO]" False => case f id it of Just (Left i) => [ oid |> Resolved $ - { items := [ i ] } . (empty mp) $ mid ] + MkResolved { items = [ i ] + , mods = [ ] + , uses = [ ] + , depth = Z } ] Just (Right m) => [ oid |> Resolved $ - { mods := [ m ] } . (empty mp) $ mid ] + MkResolved { items = [ ] + , mods = [ m ] + , uses = [ ] } ] Nothing => [ Unresolved oid ] resolveReexport oid Glob {prf = Glob} (MkState { state = _, current = it }) - = [ Resolved oid it ] + = [ Resolved oid $ MkResolved { items = items it + , mods = mods it + , uses = uses it } ] resolveReexport oid (Path id t) {prf = Path {prf}} ist@(MkState { state = st, current = it }) = case id == "super" of @@ -169,13 +185,20 @@ namespace ResolveRoot (MkState { state = _, current = it@(MkFfiItems mp mid _ _ _) }) = case f id it of Just (Left i) => - [ oid |> Resolved $ { items := [ i ] } . (empty mp) $ mid ] + [ oid |> Resolved $ MkResolved { items = [ i ] + , mods = [ ] + , uses = [ ] + , depth = Z } ] Just (Right m) => - [ oid |> Resolved $ { mods := [ m ] } . (empty mp) $ mid ] + [ oid |> Resolved $ MkResolved { items = [ ] + , mods = [ m ] + , uses = [ ] } ] Nothing => [ Unresolved oid ] resolveReexport oid Glob {prf = Glob} (MkState { state = _, current = it }) - = [ Resolved oid it ] + = [ Resolved oid $ MkResolved { items = items it + , mods = mods it + , uses = uses it } ] resolveReexport oid (Path id t) {prf = Path {prf}} (MkState { state = st, current = it }) = case f id it of Just (Right m) => @@ -203,21 +226,27 @@ resolveOne a@(it ** _) st = join . (map $ f) . ex $ a where merge : (i : FfiItems n ** UngroupedItems i) -> List Resolution -> (i : FfiItems n ** 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 n ** UngroupedItems i) --- nit = let nus = (deleteBy f oid) . ex $ it --- in { items $= (++ is), mods $= (++ ms) } iit |> re nus where --- f : UseTree -> (u : UseTree ** Ungrouped u) -> Bool --- f (Name id1) (Name id2 ** _) = --- id1 == id2 --- f Glob (Glob ** _) = --- True --- f (Path id1 t1) (Path id2 t2 ** Path {prf}) = --- id1 == id2 && f t1 (t2 ** prf) --- f _ _ = --- False +merge it@(_ ** _) [] = it +merge it@(_ ** _) ((Unresolved _) :: t) = merge it t +merge it@(iit ** _) ((Resolved oid (MkResolved is ms _)) :: t) = merge nit t + where + transform : FfiItems _ + -> (FfiItems m, List (FfiItems m)) + -> FfiItems (S m) + transform it (st, its) = (++ [ ident it ]) . path $ st |> empty $ ?transformrest + + nit : (i : FfiItems n ** UngroupedItems i) + nit = let nus = (deleteBy f oid) . ex $ it + in { items $= (++ is), mods $= (++ ms) } iit |> re nus where + f : UseTree -> (u : UseTree ** Ungrouped u) -> Bool + f (Name id1) (Name id2 ** _) = + id1 == id2 + f Glob (Glob ** _) = + True + f (Path id1 t1) (Path id2 t2 ** Path {prf}) = + id1 == id2 && f t1 (t2 ** prf) + f _ _ = + False -- [TODO]: implement this function correctly. covering @@ -238,11 +267,12 @@ resolveDriver (MkState { state = st, current = it }) = in (nst, nit :: its) f : (i : FfiItems n ** UngroupedItems i) -> FfiItems Z -> StatefulItems n - f a@(it ** _) st = let na@(nit ** _) := st |> resolveOne a |> merge a - nst := updateMod nit st - in case (length . uses $ it) == (length . uses $ nit) of - True => MkState { state = nst, current = nit } - False => f na nst + f a@(it ** _) st = + let na@(nit ** _) := st |> resolveOne a |> merge a + nst := updateMod nit st + in case (List.length . uses $ it) == (List.length . uses $ nit) of + True => MkState { state = nst, current = nit } + False => f na nst partial resolve : FfiItems Z -> FfiItems Z From a9b1dd6b6e38af8f6832d596240dc1549730fb71 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 25 Sep 2026 15:54:03 +0200 Subject: [PATCH 035/100] chore: wip --- ctest/src/ffi_items.rs | 47 +++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 5db26d23b10e1..b02cb069ea0ca 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -95,6 +95,26 @@ impl FfiItems { pub(crate) fn foreign_statics(&self) -> &Vec { &self.foreign_statics } + + /// Entry point to parse a [`syn::File`]. + /// + /// This will also resolve `use`-trees such that the resulting `FfiItems` + /// instance is always left with no reexports that are not sourced from + /// third-party crates. + pub(crate) fn visit_file(&mut self, file: &syn::File) { + let syn::File { attrs, items: mod_items, .. } = file; + let file_mod = syn::ItemMod { + attrs: attrs, + vis: syn::parse_quote! { pub }, + unsafety: None, + mod_token: syn::token::Mod::default(), + ident: syn::Ident::new("__ctest_root_mod", proc_macro2::Span::call_site()), + content: Some((syn::token::Brace::default(), mod_items)), + semi: None, + }; + items.visit_item_mod(&file_mod); + *self = resolve_use_trees(self.clone()); + } } impl Default for FfiItems { @@ -116,6 +136,10 @@ impl Default for FfiItems { } } +fn resolve_use_trees(module: FfiItems) -> FfiItems { + todo!(); +} + /// Appends a new module-local item to an absolute path that does *not* start /// with `crate`. /// @@ -359,15 +383,7 @@ impl<'ast> Visit<'ast> for FfiItems { // but rather make all items in the module become the items of the // current module (the crate root.) if ident == "__ctest_root_mod" { - mod_items.iter().for_each(|it| { - match it { - Type(t) => visit::visit_item_type(self, &t) - Struct(s) => visit::visit_item_struct(self, &s), - Union(u) => visit::visit_item_union(self, &u), - Const(c) => visit::visit_item_const(self, &c), - ForeginMod(m) => visit::visit_item_foreign_mod(self, &md), - } - }); + visit::visit_item_mod(self, i); self.uses = uses; } else { let public = matches!(vis, Visibility::Public(_)); @@ -376,6 +392,7 @@ impl<'ast> Visit<'ast> for FfiItems { let mut items = FfiItems::new(); items.current_module = path.clone(); visit::visit_item_mod(&mut items, i); + items.uses = uses; self.modules.push(Module { public, ident, @@ -389,11 +406,13 @@ impl<'ast> Visit<'ast> for FfiItems { #[test] fn tmp() { let source = r#" - use std::*; +use std::*; - fn main() { - use std::any::*; - } +mod test { use std::any::*; pub struct Foo; } + +fn main() { + use std::any::*; +} "#; let mut items = FfiItems::default(); let syn::File { attrs, items: mod_items, .. } = syn::parse_file(source).unwrap(); @@ -407,5 +426,5 @@ fn tmp() { semi: None, }; items.visit_item_mod(&file_mod); - println!("{:#?}", items.modules.last().unwrap()); + println!("{:#?}", items); } From 9a865032b72340b6eb8490496c156f73589815ec Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sat, 26 Sep 2026 10:25:43 +0200 Subject: [PATCH 036/100] chore: wip --- ctest/src/ffi_items.rs | 65 +++++++++++++-- notes/Demo.idr | 182 +++++++++++++++++++++-------------------- 2 files changed, 153 insertions(+), 94 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index b02cb069ea0ca..6fb66bfc2fdea 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -6,6 +6,7 @@ use std::ops::Deref; use quote::ToTokens; use syn::Visibility; use syn::punctuated::Punctuated; +use syn::UseTree; use syn::visit::{ self, Visit, @@ -39,7 +40,7 @@ pub(crate) struct FfiItems { pub(crate) constants: Vec, pub(crate) foreign_functions: Vec, pub(crate) foreign_statics: Vec, - pub(crate) uses: Vec, + pub(crate) uses: Vec, pub(crate) modules: Vec, /// This is used while recursing through parsed modules to gather absolute @@ -104,15 +105,15 @@ impl FfiItems { pub(crate) fn visit_file(&mut self, file: &syn::File) { let syn::File { attrs, items: mod_items, .. } = file; let file_mod = syn::ItemMod { - attrs: attrs, + attrs: attrs.clone(), vis: syn::parse_quote! { pub }, unsafety: None, mod_token: syn::token::Mod::default(), ident: syn::Ident::new("__ctest_root_mod", proc_macro2::Span::call_site()), - content: Some((syn::token::Brace::default(), mod_items)), + content: Some((syn::token::Brace::default(), mod_items.clone())), semi: None, }; - items.visit_item_mod(&file_mod); + self.visit_item_mod(&file_mod); *self = resolve_use_trees(self.clone()); } } @@ -140,6 +141,51 @@ fn resolve_use_trees(module: FfiItems) -> FfiItems { todo!(); } +#[derive(Clone, Debug)] +pub(crate) struct RefinedUsePath { + ident: syn::Ident, + tree: Box, +} + +#[derive(Clone, Debug)] +pub(crate) enum RefinedUseTree { + Path(RefinedUsePath), + Name(syn::UseName), + Rename(syn::UseRename), + Glob(syn::UseGlob), +} + +#[derive(Clone, Debug)] +pub(crate) struct RefinedUse { + is_public: bool, + tree: RefinedUseTree, +} + +/// Gets rid of group reexports in a `use` statement by flattenning them into a +/// set of individual reexports. +fn normalize_path(path: syn::UseTree) -> Vec { + match path { + UseTree::Name(n) => vec![RefinedUseTree::Name(n)], + UseTree::Rename(r) => vec![RefinedUseTree::Rename(r)], + UseTree::Glob(g) => vec![RefinedUseTree::Glob(g)], + + UseTree::Path(syn::UsePath { ident, tree, .. }) => { + normalize_path(*tree) + .into_iter() + .map(Box::new) + .map(|tree| RefinedUseTree::Path(RefinedUsePath { + ident: ident.clone(), + tree + })) + .collect() + } + + UseTree::Group(syn::UseGroup { items, .. }) => { + items.into_iter().map(normalize_path).flatten().collect() + } + } +} + /// Appends a new module-local item to an absolute path that does *not* start /// with `crate`. /// @@ -369,12 +415,19 @@ impl<'ast> Visit<'ast> for FfiItems { .iter() .cloned() .filter_map(|it| { - if let syn::Item::Use(it) = it { - Some(it) + if let syn::Item::Use(syn::ItemUse { vis, tree, .. }) = it { + normalize_path(tree) + .into_iter() + .map(move |tree| RefinedUse { + is_public: matches!(vis, syn::Visibility::Public(_)), + tree, + }) + .into() } else { None } }) + .flatten() .collect(); // [NOTE]: if the module is known to keep be the virtual module that we diff --git a/notes/Demo.idr b/notes/Demo.idr index 2cb98bc4dc037..7b0ab57d2b49b 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -16,6 +16,7 @@ ModulePath : Nat -> Type ModulePath n = Vect n String namespace FfiItems + public export record FfiItems (n : Nat) where constructor MkFfiItems path : Vect n String @@ -83,9 +84,10 @@ re ((h ** hw) :: t) i = (MkFfiItems mp mid is ms (h :: us) ** Witness {prfs = w} {prf = hw}) normalize : FfiItems n -> (i : FfiItems n ** UngroupedItems i) -normalize it = let nl = (foldr f []) . uses $ it in re nl it where +normalize it@(MkFfiItems { uses = us, _ }) = foldr f [] us |> re <| it where -- [NOTE]: this function requires asserting to the totality checker that the - -- trees ro(t : Nat)oted 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 + -- 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 @@ -96,32 +98,29 @@ normalize it = let nl = (foldr f []) . uses $ it in re nl it where -> List (u : UseTree ** Ungrouped u) f = (++) . f' where f' : UseTree -> List (u : UseTree ** Ungrouped u) - f' (Name id) = - [ ((Name id) ** Name) ] - f' Glob = - [ (Glob ** Glob) ] - f' (Path id t) = - map (\(t ** w) => (Path id t ** Path {prf = w})) (f' t) - f' o@(Group l) = - map (\t => f' $ assert_smaller o t) l |> join + f' (Name id) = [ (Name id ** Name) ] + f' Glob = [ (Glob ** Glob) ] + f' (Path id t) = f' t |> flip map <| \(t ** _) => ( Path id t + ** Path) + f' o@(Group l) = join <| l |> flip map <| \t => f' $ assert_smaller o t -- [TODO]: reimplement this. -f : String -> FfiItems n -> Maybe (Either String (FfiItems (S n))) +findItem : String -> FfiItems n -> Maybe (Either String (FfiItems (S n))) samePath : ModulePath _ -> ModulePath _ -> Bool -samePath [] [] = True +samePath [] [] = True samePath (h1 :: t1) (h2 :: t2) = h1 == h2 && samePath t1 t2 -samePath _ _ = False +samePath _ _ = False covering findParent : StatefulItems (S n) -> Maybe (FfiItems n) findParent (MkState { state = st, current = (MkFfiItems cmp _ _ _ _) }) = f st where r : FfiItems _ -> Maybe (FfiItems n) -> Maybe (FfiItems n) f : FfiItems _ -> Maybe (FfiItems n) - r it Nothing = f it - r _ p = p + r it Nothing = f it + r _ p = p f it@(MkFfiItems mp _ _ ms _) = - case samePath (reverse . tail . reverse $ cmp) mp of + case cmp |> samePath . reverse . tail . reverse <| mp of True => believe_me $ Just it False => foldr r Nothing ms @@ -132,45 +131,43 @@ namespace ResolveNode -> {auto 0 prf : Ungrouped t} -> StatefulItems (S _) -> List Resolution - resolveReexport oid (Name id) {prf = Name} - st@(MkState { state = _ - , current = it@(MkFfiItems mp mid _ _ _) }) + resolveReexport oid (Name id) st@(MkState { current = it, _ }) = case id == "super" of True => case findParent st of Just m => - [ oid |> Resolved $ - MkResolved { items = [ ] - , mods = [ m ] - , uses = [ ] } ] - Nothing => idris_crash "[TODO]" - False => case f id it of + [ oid |> Resolved <| MkResolved { items = [ ] + , mods = [ m ] + , uses = [ ] } ] + Nothing => + idris_crash "[TODO]" + False => case findItem id it of Just (Left i) => - [ oid |> Resolved $ - MkResolved { items = [ i ] - , mods = [ ] - , uses = [ ] - , depth = Z } ] + [ oid |> Resolved <| MkResolved { items = [ i ] + , mods = [ ] + , uses = [ ] + , depth = Z } ] Just (Right m) => - [ oid |> Resolved $ - MkResolved { items = [ ] - , mods = [ m ] - , uses = [ ] } ] + [ oid |> Resolved <| MkResolved { items = [ ] + , mods = [ m ] + , uses = [ ] } ] Nothing => [ Unresolved oid ] - resolveReexport oid Glob {prf = Glob} (MkState { state = _, current = it }) - = [ Resolved oid $ MkResolved { items = items it - , mods = mods it - , uses = uses it } ] + resolveReexport oid Glob (MkState { current = (MkFfiItems { items = is + , mods = ms + , uses = us + , _ }) + , _ }) + = [ Resolved oid $ MkResolved { items = is, mods = ms, uses = us } ] resolveReexport oid (Path id t) {prf = Path {prf}} ist@(MkState { state = st, current = it }) = case id == "super" of True => case findParent ist of Just m => ?rhspath Nothing => idris_crash "[TODO]" - False => case f id it of - Just (Right m) => - let np := MkState { state = st, current = m } - in ResolveNode.resolveReexport oid t {prf} np + False => case findItem id it of + Just (Right m) => ResolveNode.resolveReexport oid t $ + MkState { state = st + , current = m } Just _ => [ Unresolved oid ] Nothing => [ Unresolved oid ] @@ -181,32 +178,32 @@ namespace ResolveRoot -> {auto 0 prf : Ungrouped t} -> StatefulItems _ -> List Resolution - resolveReexport oid (Name id) {prf = Name} - (MkState { state = _, current = it@(MkFfiItems mp mid _ _ _) }) - = case f id it of + resolveReexport oid (Name id) (MkState { current = it, _ }) + = case findItem id it of Just (Left i) => - [ oid |> Resolved $ MkResolved { items = [ i ] - , mods = [ ] - , uses = [ ] - , depth = Z } ] + [ oid |> Resolved <| MkResolved { items = [ i ] + , mods = [ ] + , uses = [ ] + , depth = Z } ] Just (Right m) => - [ oid |> Resolved $ MkResolved { items = [ ] - , mods = [ m ] - , uses = [ ] } ] + [ oid |> Resolved <| MkResolved { items = [ ] + , mods = [ m ] + , uses = [ ] } ] Nothing => [ Unresolved oid ] - resolveReexport oid Glob {prf = Glob} (MkState { state = _, current = it }) - = [ Resolved oid $ MkResolved { items = items it - , mods = mods it - , uses = uses it } ] - resolveReexport oid (Path id t) {prf = Path {prf}} - (MkState { state = st, current = it }) - = case f id it of Just (Right m) => - let np := MkState { state = st, current = m } - in ResolveRoot.resolveReexport oid t {prf} np - Just _ => [ Unresolved oid ] - Nothing => [ Unresolved oid ] - + resolveReexport oid Glob (MkState { current = (MkFfiItems { items = is + , mods = ms + , uses = us + , _ }) + , _ }) + = [ Resolved oid $ MkResolved { items = is, mods = ms, uses = us } ] + resolveReexport oid (Path id t) {prf = Path {prf}} (MkState { state = st + , current = it }) + = case findItem id it of + Just (Right m) => ResolveRoot.resolveReexport oid t $ + MkState { state = st, current = m } + Just _ => [ Unresolved oid ] + Nothing => [ Unresolved oid ] partial resolveOne : { n : _ } @@ -215,40 +212,49 @@ resolveOne : { n : _ } -> List Resolution resolveOne a@(it ** _) st = join . (map $ f) . ex $ a where p : StatefulItems n - p = MkState st it - f : (u : UseTree ** Ungrouped u) -> List Resolution - f (u ** prf) = case n of - Z => ResolveRoot.resolveReexport u u {prf} p - S _ => ResolveNode.resolveReexport u u {prf} p + p = MkState st it + f (u ** _) = case n of Z => ResolveRoot.resolveReexport u u p + S _ => ResolveNode.resolveReexport u u p + +covering merge : (i : FfiItems n ** UngroupedItems i) -> List Resolution -> (i : FfiItems n ** UngroupedItems i) -merge it@(_ ** _) [] = it -merge it@(_ ** _) ((Unresolved _) :: t) = merge it t +merge it@(_ ** _) [] = it +merge it@(_ ** _) ((Unresolved _) :: t) = merge it t merge it@(iit ** _) ((Resolved oid (MkResolved is ms _)) :: t) = merge nit t where + covering transform : FfiItems _ - -> (FfiItems m, List (FfiItems m)) - -> FfiItems (S m) - transform it (st, its) = (++ [ ident it ]) . path $ st |> empty $ ?transformrest - + -> (FfiItems m, List (FfiItems (S m))) + -> (FfiItems m, List (FfiItems (S m))) + transform it (st, its) = let np := st |> snoc . path <| ident it in + ( st + , { path := np + , mods := mods it |> + foldr transform (empty np $ ident it, []) |> snd } it :: its) + + covering nit : (i : FfiItems n ** UngroupedItems i) - nit = let nus = (deleteBy f oid) . ex $ it - in { items $= (++ is), mods $= (++ ms) } iit |> re nus where - f : UseTree -> (u : UseTree ** Ungrouped u) -> Bool - f (Name id1) (Name id2 ** _) = - id1 == id2 - f Glob (Glob ** _) = - True - f (Path id1 t1) (Path id2 t2 ** Path {prf}) = - id1 == id2 && f t1 (t2 ** prf) - f _ _ = - False - --- [TODO]: implement this function correctly. + nit = + let nus = (deleteBy f oid) . ex $ it + in { items $= (++ is) + , mods := foldr transform (iit, []) ms |> snd } 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 ** Path {prf}) = + id1 == id2 && f t1 (t2 ** prf) + f _ _ = + False + +-- [TODO]: implement this function correctly. Potentially use `StatefulItems`. covering updateMod : FfiItems n -> FfiItems Z -> FfiItems Z From 1b1e58e606f104c983e91ef44024abbd1160d178 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:38:22 +0200 Subject: [PATCH 037/100] chore: wip --- ctest/src/ffi_items.rs | 75 ++++++++++++++++++++++------- notes/Demo.idr | 106 ++++++++++++++++++++++++----------------- 2 files changed, 121 insertions(+), 60 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 6fb66bfc2fdea..a457d6d6c8b1b 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -2,6 +2,8 @@ use std::borrow::Borrow; use std::ops::Deref; +use std::iter; +use std::ops::ControlFlow; use quote::ToTokens; use syn::Visibility; @@ -46,7 +48,7 @@ pub(crate) struct FfiItems { /// 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, + pub(crate) current_module: syn::Path, } impl FfiItems { @@ -114,7 +116,16 @@ impl FfiItems { semi: None, }; self.visit_item_mod(&file_mod); - *self = resolve_use_trees(self.clone()); + let root_module = Module { + public: bool::default(), + ident: String::new().into_boxed_str(), + path: syn::Path { + leading_colon: None, + segments: Punctuated::new(), + }, + items: self.clone(), + }; + *self = resolve_use_trees(root_module).items; } } @@ -137,10 +148,6 @@ impl Default for FfiItems { } } -fn resolve_use_trees(module: FfiItems) -> FfiItems { - todo!(); -} - #[derive(Clone, Debug)] pub(crate) struct RefinedUsePath { ident: syn::Ident, @@ -186,6 +193,49 @@ fn normalize_path(path: syn::UseTree) -> Vec { } } +enum Resolution { + Resolved { original_use: RefinedUse, module: FfiItems }, + Unresolved { original_use: RefinedUse } +} + +fn resolve_use_trees(root: Module) -> Module { + let resolved_children = root.items.modules.into_iter().map(resolve_use_trees).collect(); + let root = { + let items = FfiItems { + modules: resolved_children, + ..root.items + }; + Module { items, ..root } + }; + let (ControlFlow::Continue(root) | ControlFlow::Break(root)) = + iter::repeat(()).try_fold(root, |root, _| { + let resolved_uses = resolve_one(root.clone()); + let new_root = merge_module(root.clone(), resolved_uses); + match root.items.uses.len() == new_root.items.uses.len() { + true => ControlFlow::Break(new_root), + false => ControlFlow::Continue(new_root) + } + }); + root +} + +fn resolve_one(src: Module) -> Vec { + src.items.uses + .clone() + .into_iter() + .map(|u| (u, src.clone())) + .map(|(u, m)| resolve_use(u, m)) + .collect() +} + +fn resolve_use(r#use: RefinedUse, state: Module) -> Resolution { + todo!(); +} + +fn merge_module(dst: Module, src: Vec) -> Module { + todo!(); +} + /// Appends a new module-local item to an absolute path that does *not* start /// with `crate`. /// @@ -468,16 +518,7 @@ fn main() { } "#; let mut items = FfiItems::default(); - let syn::File { attrs, items: mod_items, .. } = syn::parse_file(source).unwrap(); - let file_mod: syn::ItemMod = syn::ItemMod { - attrs: attrs, - vis: syn::parse_quote! { pub }, - unsafety: None, - mod_token: syn::token::Mod::default(), - ident: syn::Ident::new("__ctest_root_mod", proc_macro2::Span::call_site()), - content: Some((syn::token::Brace::default(), mod_items)), - semi: None, - }; - items.visit_item_mod(&file_mod); + let file = syn::parse_file(source).unwrap(); + items.visit_file(&file); println!("{:#?}", items); } diff --git a/notes/Demo.idr b/notes/Demo.idr index 7b0ab57d2b49b..a138a9c290c9e 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -102,10 +102,15 @@ normalize it@(MkFfiItems { uses = us, _ }) = foldr f [] us |> re <| it where f' Glob = [ (Glob ** Glob) ] f' (Path id t) = f' t |> flip map <| \(t ** _) => ( Path id t ** Path) - f' o@(Group l) = join <| l |> flip map <| \t => f' $ assert_smaller o t + f' o@(Group l) = join <| l |> flip map <| \t => f' $ + assert_smaller o t --- [TODO]: reimplement this. findItem : String -> FfiItems n -> Maybe (Either String (FfiItems (S n))) +findItem id (MkFfiItems { items = is, mods = ms, _ }) = + find c $ map Left is ++ map Right ms where + c : Either String (FfiItems (S n)) -> Bool + c (Left s) = s == id + c (Right (MkFfiItems { ident = s, _ })) = s == id samePath : ModulePath _ -> ModulePath _ -> Bool samePath [] [] = True @@ -114,16 +119,50 @@ samePath _ _ = False covering findParent : StatefulItems (S n) -> Maybe (FfiItems n) -findParent (MkState { state = st, current = (MkFfiItems cmp _ _ _ _) }) = +findParent (MkState { state = st, current = MkFfiItems { path = cmp, _ } }) = f st where r : FfiItems _ -> Maybe (FfiItems n) -> Maybe (FfiItems n) f : FfiItems _ -> Maybe (FfiItems n) - r it Nothing = f it - r _ p = p - f it@(MkFfiItems mp _ _ ms _) = + r it Nothing = f it + r _ p = p + f it@(MkFfiItems mp _ _ ms _) = case cmp |> samePath . reverse . tail . reverse <| mp of True => believe_me $ Just it False => foldr r Nothing ms +namespace ResolveRoot + public export + resolveReexport : UseTree + -> (t : UseTree) + -> {auto 0 prf : Ungrouped t} + -> StatefulItems _ + -> List Resolution + resolveReexport oid (Name id) (MkState { current = it, _ }) + = case findItem id it of + Just (Left i) => + [ oid |> Resolved <| MkResolved { items = [ i ] + , mods = [ ] + , uses = [ ] + , depth = Z } ] + Just (Right m) => + [ oid |> Resolved <| MkResolved { items = [ ] + , mods = [ m ] + , uses = [ ] } ] + Nothing => + [ Unresolved oid ] + resolveReexport oid Glob (MkState { current = (MkFfiItems { items = is + , mods = ms + , uses = us + , _ }) + , _ }) + = [ Resolved oid $ MkResolved { items = is, mods = ms, uses = us } ] + resolveReexport oid (Path id t) {prf = Path {prf}} (MkState { state = st + , current = it }) + = case findItem id it of + Just (Right m) => ResolveRoot.resolveReexport oid t $ + MkState { state = st, current = m } + Just _ => [ Unresolved oid ] + Nothing => [ Unresolved oid ] + namespace ResolveNode public export partial resolveReexport : UseTree @@ -162,7 +201,16 @@ namespace ResolveNode ist@(MkState { state = st, current = it }) = case id == "super" of True => case findParent ist of - Just m => ?rhspath + Just m => + case length . path $ m of + Z => + ResolveRoot.resolveReexport oid t $ + MkState { state = st, current = m } + o@(S _) => + let nst : StatefulItems o + nst = believe_me $ MkState { state = st + , current = m } + in ResolveNode.resolveReexport oid t nst Nothing => idris_crash "[TODO]" False => case findItem id it of Just (Right m) => ResolveNode.resolveReexport oid t $ @@ -171,40 +219,6 @@ namespace ResolveNode Just _ => [ Unresolved oid ] Nothing => [ Unresolved oid ] -namespace ResolveRoot - public export - resolveReexport : UseTree - -> (t : UseTree) - -> {auto 0 prf : Ungrouped t} - -> StatefulItems _ - -> List Resolution - resolveReexport oid (Name id) (MkState { current = it, _ }) - = case findItem id it of - Just (Left i) => - [ oid |> Resolved <| MkResolved { items = [ i ] - , mods = [ ] - , uses = [ ] - , depth = Z } ] - Just (Right m) => - [ oid |> Resolved <| MkResolved { items = [ ] - , mods = [ m ] - , uses = [ ] } ] - Nothing => - [ Unresolved oid ] - resolveReexport oid Glob (MkState { current = (MkFfiItems { items = is - , mods = ms - , uses = us - , _ }) - , _ }) - = [ Resolved oid $ MkResolved { items = is, mods = ms, uses = us } ] - resolveReexport oid (Path id t) {prf = Path {prf}} (MkState { state = st - , current = it }) - = case findItem id it of - Just (Right m) => ResolveRoot.resolveReexport oid t $ - MkState { state = st, current = m } - Just _ => [ Unresolved oid ] - Nothing => [ Unresolved oid ] - partial resolveOne : { n : _ } -> (i : FfiItems n ** UngroupedItems i) @@ -235,7 +249,7 @@ merge it@(iit ** _) ((Resolved oid (MkResolved is ms _)) :: t) = merge nit t ( st , { path := np , mods := mods it |> - foldr transform (empty np $ ident it, []) |> snd } it :: its) + foldr transform (empty np $ ident it, []) |> snd } it :: its ) covering nit : (i : FfiItems n ** UngroupedItems i) @@ -254,9 +268,15 @@ merge it@(iit ** _) ((Resolved oid (MkResolved is ms _)) :: t) = merge nit t f _ _ = False --- [TODO]: implement this function correctly. Potentially use `StatefulItems`. covering updateMod : FfiItems n -> FfiItems Z -> FfiItems Z +updateMod it@(MkFfiItems { path = itp, _ }) st@(MkFfiItems { path = stp, _ }) = + case samePath stp itp of True => believe_me it + False => { mods $= foldr f [] } st + where f : FfiItems m -> List (FfiItems m) -> List (FfiItems m) + f fit@(MkFfiItems { path = fitp, _ }) fits = + case samePath fitp itp of True => believe_me it :: fits + False => { mods $= foldr f [] } fit :: fits partial resolveDriver : { n : _ } -> StatefulItems n -> StatefulItems n From dbccfb97f4ed249ac3528153fcd3655aaadb48ea Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:55:15 +0200 Subject: [PATCH 038/100] chore: wip --- ctest/src/ffi_items.rs | 108 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 6 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index a457d6d6c8b1b..4d448d2f07e5b 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -28,6 +28,16 @@ use crate::{ Union, }; +enum GenericItem { + Type(Type), + Struct(Struct), + Union(Union), + Const(Const), + Fn(Fn), + Static(Static), + Module(Module), +} + /// Represents a collected set of top-level Rust items relevant to FFI /// generation or analysis. /// @@ -127,6 +137,45 @@ impl FfiItems { }; *self = resolve_use_trees(root_module).items; } + + /// Searches for an item in the container, and returns an owned instance of + /// that item. + /// + /// The generic item may be pattern-matched to find out specifically which + /// item was found. + fn search(&self, ident: syn::Ident) -> Option { + let aliases = self.aliases.clone().into_iter().map(GenericItem::Type); + let structs = self.structs.clone().into_iter().map(GenericItem::Struct); + let unions = self.unions.clone().into_iter().map(GenericItem::Union); + let constants = self.constants.clone().into_iter().map(GenericItem::Const); + let foreign_functions = self.foreign_functions.clone().into_iter().map(GenericItem::Fn); + let foreign_statics = self.foreign_statics.clone().into_iter().map(GenericItem::Static); + let mut items: Vec<_> = self.modules + .clone() + .into_iter() + .map(GenericItem::Module) + .chain(foreign_statics) + .chain(foreign_functions) + .chain(constants) + .chain(unions) + .chain(structs) + .chain(aliases) + .collect(); + let key_fn = |it: &GenericItem| match it { + GenericItem::Type(t) => t.ident(), + GenericItem::Struct(s) => s.ident(), + GenericItem::Union(u) => u.ident(), + GenericItem::Const(c) => c.ident(), + GenericItem::Fn(f) => f.ident(), + GenericItem::Static(s) => s.ident(), + GenericItem::Module(m) => m.ident(), + }; + items.sort_unstable_by_key(key_fn); + let Ok(idx) = items.binary_search_by_key(&ident.to_string(), key_fn) else { + return None; + }; + items.swap_remove(idx).into() + } } impl Default for FfiItems { @@ -194,8 +243,8 @@ fn normalize_path(path: syn::UseTree) -> Vec { } enum Resolution { - Resolved { original_use: RefinedUse, module: FfiItems }, - Unresolved { original_use: RefinedUse } + Resolved { original_use: RefinedUse, items: FfiItems }, + Unresolved, } fn resolve_use_trees(root: Module) -> Module { @@ -223,13 +272,60 @@ fn resolve_one(src: Module) -> Vec { src.items.uses .clone() .into_iter() - .map(|u| (u, src.clone())) - .map(|(u, m)| resolve_use(u, m)) + .map(|u| (u.clone(), u, src.clone())) + .map(|(ou, u, m)| resolve_use(ou, u, m)) .collect() } -fn resolve_use(r#use: RefinedUse, state: Module) -> Resolution { - todo!(); +fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Resolution { + match &r#use.tree { + RefinedUseTree::Name(syn::UseName { ident }) => { + macro_rules! single_item { + ($field:ident: $it:ident) => {{ + let items = FfiItems { + $field: vec![$it], + ..Default::default() + }; + Resolution::Resolved { + original_use, + items, + } + }}; + } + + match state.items.search(ident.clone()) { + Some(GenericItem::Type(t)) => single_item!(aliases: t), + Some(GenericItem::Struct(s)) => single_item!(structs: s), + Some(GenericItem::Union(u)) => single_item!(unions: u), + Some(GenericItem::Const(c)) => single_item!(constants: c), + Some(GenericItem::Fn(f)) => single_item!(foreign_functions: f), + Some(GenericItem::Static(s)) => single_item!(foreign_statics: s), + Some(GenericItem::Module(m)) => single_item!(modules: m), + None => Resolution::Unresolved, + } + } + RefinedUseTree::Glob(_) => Resolution::Resolved { original_use, items: state.items }, + RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => todo!(), + + RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { + let new_use = RefinedUse { + is_public: r#use.is_public, + tree: *tree.clone(), + }; + let new_state = match state.items.search(ident.clone()) { + Some(GenericItem::Module(m)) => m, + None => return Resolution::Unresolved, + Some(_) => unreachable!( + "paths we parse in ctest include only modules, and not \ + enum variants; update this in the future if we start \ + supporting enum variants and thus start having imports \ + that can span multiple segments of a span without \ + strictly being nested modules" + ), + }; + resolve_use(original_use, new_use, new_state) + } + } } fn merge_module(dst: Module, src: Vec) -> Module { From 366231bb6148e2ffdfc7c1de2bf5d02070169cd3 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:59:19 +0200 Subject: [PATCH 039/100] chore: wip --- ctest/src/ffi_items.rs | 49 +++++- notes/Demo.idr | 2 +- notes/main.typ | 391 ++++++++++------------------------------- 3 files changed, 136 insertions(+), 306 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 4d448d2f07e5b..063087d2e840f 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1,7 +1,7 @@ //! Conversion of Rust code to a simplified abstract syntax tree. use std::borrow::Borrow; -use std::ops::Deref; +use std::ops::{self, Deref}; use std::iter; use std::ops::ControlFlow; @@ -319,7 +319,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re "paths we parse in ctest include only modules, and not \ enum variants; update this in the future if we start \ supporting enum variants and thus start having imports \ - that can span multiple segments of a span without \ + that can span multiple segments of a path without \ strictly being nested modules" ), }; @@ -328,6 +328,51 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } } +fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module) -> Module { + let new_root_path = f(root.path.clone()); + let new_root_cache = path_to_string(&new_root_path); + macro_rules! map_items { + ($field:ident: $ty:tt) => {{ + root.items.$field + .clone() + .into_iter() + .map(|it| { + let new_path = f(it.path); + $ty { path: new_path, ..it } + }) + .collect::>() + }}; + } + let new_aliases = map_items!(aliases: Type); + let new_structs = map_items!(structs: Struct); + let new_unions = map_items!(unions: Union); + let new_constants = map_items!(constants: Const); + let new_foreign_functions = map_items!(foreign_functions: Fn); + let new_foreign_statics = map_items!(foreign_statics: Static); + let new_modules: Vec<_> = root.items.modules + .clone() + .into_iter() + .map(|m| (f.clone(), m)) + .map(|(f, m)| manipulate_path(f, m)) + .collect(); + let new_items = FfiItems { + aliases: new_aliases, + structs: new_structs, + unions: new_unions, + constants: new_constants, + foreign_functions: new_foreign_functions, + foreign_statics: new_foreign_statics, + modules: new_modules, + ..Default::default() + }; + Module { + public: root.public, + ident: new_root_cache, + path: new_root_path, + items: new_items, + } +} + fn merge_module(dst: Module, src: Vec) -> Module { todo!(); } diff --git a/notes/Demo.idr b/notes/Demo.idr index a138a9c290c9e..e27c57684eaf0 100644 --- a/notes/Demo.idr +++ b/notes/Demo.idr @@ -289,7 +289,7 @@ resolveDriver (MkState { state = st, current = it }) = -> (FfiItems Z, List (FfiItems (S n))) r it (st, its) = let (MkState { state = nst, current = nit }) := - resolveDriver (MkState { state = st , current = it }) + resolveDriver $ MkState { state = st, current = it } in (nst, nit :: its) f : (i : FfiItems n ** UngroupedItems i) -> FfiItems Z -> StatefulItems n diff --git a/notes/main.typ b/notes/main.typ index 4fe769f5166ca..9d62546599b7b 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -4,306 +4,91 @@ #title() -Extending `ctest` requires keeping track of the ```rust use``` statements while -parsing to get right the item visibility across modules. We could very well be -producing tests for an item without it being used in its "source" module. - -This, though, shouldn't matter much. The module from which we parsed the item -should be enough to test it; The problem comes when filtering. Folks expect to -filter items that are potentially reexported. - -Suppose somebody sets up a skip for a record `Foo`, and trusts that there will -be an item path `bar::Foo` that will match this item. The item is defined in -module `crate::bar::foo::Foo`, but reexported in module `bar`. - -While going through the skips in the `ctest` internal logic, we find that this -item will never get a hit. The path associated with it is `bar::foo::Foo`, and -not `bar::Foo`. - -Solving this particular need could go through parsing as well reexports, and -scanning them afterwards. This second pass would add "synonyms" to the item -paths of each parsed item. Skips would run against each of those paths. - -This is assuming our only needs are concerned with filtering. This is not -necessarily the case. Or maybe it is. `ctest` parses all items in a crate. Then -it generates tests that refer to items on the Rust side of things. - -There is one more usecase for ```rust use```-statement parsing. Suppose somebody -sets up a record that is not public under path `crate::foo::Bar`. Then they -reexport it under path `crate::Bar`. - -Then suppose they call ```rust TestGenerator::skip_private```. We are again -screwed big time if the Rust tests refer to the item through the private item -path. Or are we, now? How does that option work when not set? - -Apparently, I do not have to worry about item resolution. We can refer to both -public and private items in the generated tests. This means the only thing that -needs to work with reexports are filters. - -This means that function ```rust TranslateHelper::filter_ffi_items``` may be the -only thing that needs changing. That function filters out all items that will be -tested, so we can also recursively filter items there. - -The simplest approach that comes to mind to have multiple identifiers assigned -to a given symbol is to perform multiple passes. Once all symbols are parsed -into `FfiItems`, we make one full traversal per parsed item. - -Each full traversal of the same `FfiItems` ensures for each given item, we keep -all paths that could be used to refer to that item in scope. Once `Ffiitems` is -parsed, it will not change anymore until we filter out elements. - -This is not the best approach when it comes to performance, but it will do for -an initial implementation. In terms of memory storage, to avoid funny allocation -issues, we can clone `FfiItems` initially. - -The cloned `FfiItems` is then used for the passes; The original `FfiItems` gets -its parsed items modified with alternative paths. Seems fair enough. We fully -own the items within it at filter-time, so the plan seems feasible. - -The problem comes when you think about item resolution in those secondary -passes. It is non-trivial to implement because at module -```rust crate::bar::foo``` you can have the following code: - -```rust -pub struct Bar; -``` - -Then back at module ```rust crate::bar```, you can have the following code. - -```rust -mod foo; - -use foo::*; -``` - -But you could also have this: - -```rust -mod foo; - -use self::foo::*; -``` - -And for that matter, you could have instead module -```rust crate::bar::barfoo::foo```, with the following layout: - -/ Module ```rust crate::bar::barfoo::foo```: - ```rust - pub struct Bar; - ``` - -/ Module ```rust crate::bar::barfoo```: - ```rust - mod foo; - mod test; - use foo::*; - ``` - -/ Module ```rust crate::bar```: - ```rust - use foo::*; // `foo` comes from `barfoo`'s reexport below - mod barfoo; - use barfoo::*; - ``` - -The above situation is one of a number of potentially complex item resolution -scenarios that I would have to deal with. Maybe there is some library that does -this for me, or maybe I can use some library straight from `rustc`. - -It seems like `rustc_resolve` does just this, but it is not made for use outside -the compiler, so it is not available in `crates.io`. The next best thing would -be to inline each of these, and perform multiple passes per module. - -Inlining is not enitrely clear to me right now, but the multi-passes would work -a lot like Typst's convergence algorithm. One pass yields modules we know are -child modules to the current one, and modules we don't know about. - -Then we can assume all imports that have not been resolved yet need special -attention in subsequent passes. After the first pass, we have expanded the -"importable" items, so we should be capable of resolving all "delayed" imports. - -Following up from the prior example, we can say that the first pass would yield -the following information: - -```rust -// module `crate::bar` -use foo::*; // [UNRESOLVED] -mod barfoo; -use barfoo::*; // [RESOLVED] -``` - -The resulting ```rust FfiItems``` for module ```rust crate::bar``` would contain -one module, and all items from that one child module. - -That would leave us with an ```rust FfiItems``` for module ```rust crate::bar``` -consisting of two modules; Module `barfoo` and module `barfoo::foo`. But -resolving solely modules this way is useless. - -Instead, the algorithm could be something along the lines of (a bit like -Bellman-Ford except without proof of correctness:) - -/ Algorithm 1: \ - Inputs: - - - An ```rust FfiItems``` instance with the parsed contents of a full crate. - The parsed contents must contain both currently parsed items and import - statements (so just items.) - - Outputs: - - - *Pending*. - - Steps: - - + Match on the list of child modules to the input ```rust FfiItems```. - - - If there are no child modules, return unity (*Pending*.) - - - If there are any modules, extract the next module. - - + Match on the list of reexports in the extracted module. - - - If there are no reexports, proceed as follows. - - + Run algorithm 1. Set its input to be a new ```rust FfiItems``` with - its modules as the tail list of the current list of modules, barring - the extracted module. - - - If there are any reexports, extract the next reexport. - - + Run algorithm 2. Set the input import statement to be the extracted - reexport. Set the input ```rust FfiItems``` to be the current - input's ```rust FfiItems```. - -/ Algorithm 2: \ - Inputs: - - - An import ```rust use``` statement. - - - A base ```rust FfiItems``` corresponding to the module where the above - import statement lives at. - - Outputs: - - - A list of a coproduct type. The type considers two data constructors; One - for _resolved_ modules, and another for _unresolved_ modules. The former - takes a single parameter of type ```rust FfiItems```. - - This returns a list instead of a single ```rust FfiItems``` instance because - a given ```rust use``` statement could refer to a group in its tail segment. - Each element of the group could itself expand to an arbitrary reexport. - - Steps: - - + Match against the type of input import. - - - If the import is a path, proceed as follows. - - + Match against the list of modules in the input ```rust FfiItems```, - searching for the leftmost extracted segment of the input import's path. - - - If the list of modules contains a match against the path, proceed as - follows. - - + Run algorithm 2. Set the input ```rust use``` statement to be the - rhs of the current import statement. Set the input - ```rust FfiItems``` to be the match module. - - + Return the result of step 1.a.1.a.1. - - - If the list of modules does not contain a match, proceed as follows. - - + Return a singleton list. Its one element should consist of the value - returned from calling the _unresolved_ data constructor. - - - If the import is an identifier or a renamed identifier, proceed as - follows. - - + Match against the input ```rust FfiItems```'s list of items. - - - If a match is found for the identifier or original identifier (in the - case of a rename,) proceed as follows. - - + Return a singleton list. Its one element should consist of a new - ```rust FfiItems``` instance containing solely the found item, - wrapped by a _resolved_ data constructor. - - - If no match is found, proceed as follows. - - + Return a singleton list. The element should consist of the value - returned from calling the _unresolved_ data constructor. - - - If the import is a glob, proceed as follows. - - + Return a single-element list. Its one element should wrap the input - ```rust FfiItems``` instance with a _resolved_ data constructor. - - - If the import is a group, proceed as follows. - - + Run algorithm 3. Set the input list to be the matched group. Set the - input ```rust FfiItems``` to be the current input's ```rust FfiItems```. - - + Return the result of step 1.d.1. - -/ Algorithm 3: \ - Inputs: - - - A list of grouped elements in the tail of an import statement. - - - An ```rust FfiItems``` instance where the reexport from which the above - input is sourced (i.e. the instance containing the whole ```rust use``` - statement.) - - Outputs: - - - A list of ```rust FfiItems``` wrapped with the same sum type as outlined in - the outputs of algorithm 2. - - Steps: - - + Match against the input list. - - - If there are no elements left in the group, return the empty list. - - - If there are any elements left, extract the next element and proceed as - follows. - - + Run algorithm 2. Set the input import statement to be the extracted - element. Set the input ```rust FfiItems``` to be the current input - ```rust FfiItems```. - - + Run algorithm 3. Set te input list to be the tail list of elements after - extracting the above element. Set the ```rust FfiItems``` instance to be - the same input instance as we currently have as input. - - + Return the resulting list from appending the lists from step 1.b.1 to - the lists from step 1.b.2. - -/ Algorithm 4: \ - Inputs: - - - An instance of ```rust FfiItems```. - - Outputs: - - - A list of all reexports in the input ```rust FfiItems``` mapped to the type - outlined in the outputs of algorithm 2. - - Steps: - - + Match against the list of reexports in the input ```rust FfiItems```. - - - If there are no reexports, return the empty list. - - - If there are any reexports left, proceed as follows. - - + Run algorithm 2. Set the input import to be the matched reexport. Set - the input ```rust FfiItems``` to be the current input's - ```rust FfiItems```. - - + Run algorithm 4. Set the input ```rust FfiItems``` to be a new - ```rust FfiItems``` instance whose modules contain the current input's - tail list of modules, barring the extracted reexport. - - + Return the result of appending the resulting list from step 1.b.1 to the - resulting list from step 1.b.2. += On the design and implementation of the rename proposition +The function is known to be needed both in the merging function and in the +rename case of the single-reexport resolution proposition. The proposition can +assume true a closure, and imply another proposition from a module to a module. + +The closure would be passed the path to some item, and it would be expected to +return another path. The new path would then be used to replace that of the item +while performing a traversal of the assumed module tree. + +Both values may be passed by value to the proposition, and the closure may take +as well a fully-owned path, and return a fully-owned path. The proof would be +shaped as a single-run of the closure over the root, and a map of the children. + +The path of the module would first be passed to the closure. Then a new module +would be produced, where the path would correspond with that returned by the +clsoure. The map over the child items would repeat the same things. + +Note the traversal would be required to go through all items in the subtree +rooted at the passed module. This implies that there ought be multiple maps for +each of the containers used for each type of item. + +Recusion would take place when the container being handled would correspond with +that of child modules to the currently traversed module. For each of those, the +same logic would follow, but additionally the mapping would take place anew. + +This hints at the possibility to abstract away the module-pass in a single inner +function that would take the module and the closure, and yield the updated +module tree rooted at the passed module. + +This is exactly what the whole renaming proposition would do. This implies that +the logic should only consist of performing the update over the module path, +then repeating the same thing over the child items, recursing with the modules. + +The only detail of the proof that remains to be specified is the behavior post +recursing with each of the child modules. The returned module would be assigned +instead to the prior module. This means there is no need to rename the child. + +The renaming of the module's path will already take place within the next call +stack frame. There is one thing that remains to be specified; The module path +exists not only in the corresponding field, but also in a cached form. + +The cached form would also need updating after the path were changed. This would +seem to be readily possible with the proposition that is also used at parse-time +to yield the same paths serving as a stringified cache of the data structure. + +It would seem the proof is complete; There is something odd with it, though. Let +there be a review of the path manipulation strategy that would be used when +handling renames in the single-reexport resolution proposition. + +Renaming the last segment of all paths appearing in a given module subtree would +need to change the last segment of that tree's root, followed by a change of all +subsequent child items' path segments corresponding to the ancestor's old ident. + +This could be accomplished by capturing in the closure the original path of the +ancestor module. Then whatever followed from the path passed to the clsoure post +extracting this path would be reappended to the modified ancestor's path. + +So to picture an example; Consider path foo::bar. The module tree rooted at +module bar should be renamed to baz. This would mean that all items whose paths +started with foo::baz in that tree would first extract the path after this. + +For example, if there is some item with path foo::bar::foobar::Foo, the closure +would extract the path after the original ancestor's foo::bar path. This would +correspond with foobar::Foo. + +Then this would be appended to the result of modifying the root module's path to +foo::baz. The end result would be path foo::baz::foobar::Foo. Of course, it +could be that there is no path after the root path, like the root module itself. + +In those cases, the append opeeration would yield only the modified root path. +Notably, the Rust runtime has no way of knowing that the path being extracted +from the path passed to the closure is prepended by the root module's path. + +This will require some potential unreachable! calls, but that should be +acceptable. Once the proposition for handing use-trees with renames in them is +discussed, the details of the proof will be provided. + +Something that remains to be discussed from the proofing of the path +manipulation proposition is that there exists a potential issue with the way it +handles the traits implemented by the passed closure. + +The trat implementation of the passed closure is also required to be Clone +because otherwise there are issues with using it in the item list mappings +closures. This constraint seems not feasible lest closures implement Clone. + +It would seem closures do implement Clone. Constraining the closure trait any +further than with an immutable and recallable trait seems neither feasible. The +FnMut trait is not required, and FnOnce is only known to be a one-off thing. From 4135ca62b499a9f8cf81dd2744dbf394b208084d Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:00:08 +0200 Subject: [PATCH 040/100] chore: wip --- ctest/src/ffi_items.rs | 38 +++++++++-- notes/main.typ | 148 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 177 insertions(+), 9 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 063087d2e840f..eaa17bf31ff38 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -208,7 +208,7 @@ pub(crate) enum RefinedUseTree { Path(RefinedUsePath), Name(syn::UseName), Rename(syn::UseRename), - Glob(syn::UseGlob), + Glob, } #[derive(Clone, Debug)] @@ -223,7 +223,7 @@ fn normalize_path(path: syn::UseTree) -> Vec { match path { UseTree::Name(n) => vec![RefinedUseTree::Name(n)], UseTree::Rename(r) => vec![RefinedUseTree::Rename(r)], - UseTree::Glob(g) => vec![RefinedUseTree::Glob(g)], + UseTree::Glob(g) => vec![RefinedUseTree::Glob], UseTree::Path(syn::UsePath { ident, tree, .. }) => { normalize_path(*tree) @@ -292,7 +292,6 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } }}; } - match state.items.search(ident.clone()) { Some(GenericItem::Type(t)) => single_item!(aliases: t), Some(GenericItem::Struct(s)) => single_item!(structs: s), @@ -304,8 +303,35 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re None => Resolution::Unresolved, } } - RefinedUseTree::Glob(_) => Resolution::Resolved { original_use, items: state.items }, - RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => todo!(), + RefinedUseTree::Glob => Resolution::Resolved { original_use, items: state.items }, + RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => { + macro_rules! single_item { + ($field:ident , $it:tt : $ty:tt) => {{ + let mut new_path = $it.path; + new_path.pop(); + new_path.push(rename); + let new_item = $ty { + ident: path_to_string(&new_path), + path: same_path, + ..$id, + }; + Resolution::Resolved { + original_use, + items: FfiItems { $field: new_item, ..Default::default() }, + } + }}; + } + match state.items.search(ident.clone()) { + Some(GenericItem::Module(m)) => todo!(), + Some(GenericItem::Type(t)) => single_item!(aliases, t: Type), + Some(GenericItem::Struct(s)) => single_item!(structs, s: Struct), + Some(GenericItem::Union(u)) => single_item!(unions, u: Union), + Some(GenericItem::Const(c)) => single_item!(constants, c: Const), + Some(GenericItem::Fn(f)) => single_item!(foreign_functions, f: Fn), + Some(GenericItem::Static(s)) => single_item!(foreign_statics, s: Static), + None => Resolution::Unresolved + } + } RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { let new_use = RefinedUse { @@ -366,10 +392,10 @@ fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module ..Default::default() }; Module { - public: root.public, ident: new_root_cache, path: new_root_path, items: new_items, + ..root } } diff --git a/notes/main.typ b/notes/main.typ index 9d62546599b7b..4c228d82e8a6a 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -86,9 +86,151 @@ manipulation proposition is that there exists a potential issue with the way it handles the traits implemented by the passed closure. The trat implementation of the passed closure is also required to be Clone -because otherwise there are issues with using it in the item list mappings +because otherwise there are issues with using it in the item-list-mapping closures. This constraint seems not feasible lest closures implement Clone. It would seem closures do implement Clone. Constraining the closure trait any -further than with an immutable and recallable trait seems neither feasible. The -FnMut trait is not required, and FnOnce is only known to be a one-off thing. +further with an immutable and recallable trait seems neither feasible. The FnMut +trait is not required, and FnOnce can only be assumed to be a one-off thing. + +The only worrying thing about the path manipulation strategy, as well as all +other propositions whose proofs use recursiveness, is that the Rust runtime +ensures not unboundedness of recursion in most cases. + +This consideration will be left for later. Current endeavors are rather focused +on the renaming function to complete case handling during single-reexport +resolution, and on the merging function. These are the only two things left. + +The renaming case may now more easily proceed by using the path manipulation +proposition. Looking back on it, the refined type for use statements seems like +it could benefit from making the glob injection be a const functor. + +Collapsing the contents of a glob seems feasible because it contains no +information of use. Now back to the rename case. In this instance, it is known +that the rename case holds both a source identifier and a rename. + +The first thing to do here would be a lookup of the source identifier. If found, +then the returned item (if not a module) would suffer in-place modification of +its path by creating a new (non-module) item. + +If the returned item were to be a module, then the renamed identifier alongside +the yield module would have to be used with the path manipulation proposition. +Let the proof be written up to this point. + +Further discussion is merited on the side of renaming non-module items. The path +of the item should be changed; One can be sure that post-parsing there are no +items whose paths are empty. + +Beyond that, the last segment of the path can be readily removed from the +original path segments. Then the renamed identifier can be pushed anew into the +punctuated list of segments. + +This process is mechanical enough for each item type that it could potentially +be automated by means of an MBE. The path field is always available, no matter +the item; All but the final operation is then item-independent. + +The last required action is to reconstruct the item with the updated path, and +the updated cache for the path. This should be fairly simple to accomplish. + += Idris community tutorial +Before starting with the Idris community tutorial, it would be best to finish +the attempt at providing a proof tha the vector splitting proposition is +correct. This was part of an initial attempt during early hours with Idris. + +The proposition requires there being a vector that implies another proposition. +This other proposition assumes a natural and implies a pair of a vector with +some TBD length, and some vector with some other TBD length. + +The splitting logic would dictate that the splitting index must fall between 0 +and the length of the vector. Constraining that type is likely possible through +finite sets, but it may prove more instructive to do so by hand. + +The proposition constraining some natural number to exist within a range should +be indexed by a natural. If one assumes an inductive definition for the type, +there is one vacuously true case; Natural zero is always in bounds. + +Thus, one case of the proposition constructs a finite set from the zeroth +natural. One other case follows then; Provided an existing finite set, there +exists another finite set representing the successor to the existing finite set. + +This would dictate that some finite set indexed by 2 can be deconstructed into a +finite set that was constructed from a finite set indexed by 1, and that was +itself constructed from a finite indexed by 2. + +Would this proposition be enough to reflect that there exists no natural, that +upon being dependently paired with a finite set, would not surpass its index? +This can be readily tested by producing a dependent natural with a finite set. + +It would seem the above design for finite sets constrains the natural to be a +specific number, and not to fall within a specific range. Consider the case of a +natural 2, which should fall within the finite set bounded from above at 3. + +The construction of the finite set follows the same construction as that of a +natural. The successor to some existing finite set builds from the prior finite +set. This allows not one to retrieve the finite set that would limit a range. + +Construction of the proof requires being capable of constructing the type of the +finite set. For some natural, if a finite set represents an upper bound, the +building of the finite set must allow representing the natural. + +Except it may very well be that the test case itself is flawed. The type of the +dependent pair specifically uses the dependant natural as the index to the +dependee finite set. This implies the natural must be the upper bound itself. + +A better test case may be found if instead the finite set is used to constrain a +natural that exists in another context. An ideal example would seem to be a +length-indexed vector whose implicit for the length were bounded by the set. + +It would seem a more precise test case may go through the proofing of an +indexing propostion for a length-indexed vector. This would consider a finite +set that represented some non-zero natural, and a vector with that length. + +The finite set would have be indexed by the successor to some natural, and the +vector length would have to also be the successor the same natural. This would +ensure the finite set represents at least numbers greater than zero. + +The proofing of this may not be as simple, though. The basecase would seem to +correspond with a proposition with a finite set for the successor of zero. This +would yield the only element in the vector. + +This could be assumed to be the inductive hypothesis. The only other case would +be for the finite set to represent the successor of some other finite set, and +thus for the vector to hold more than one element. + +In the proof, this would pattern match with the finite set as mentioned above, +and with the vector as a destructuring of its head from its tail. The only issue +to this is that this would always fetch the first element of the vector. + +The only possible cases to consider would be those. It seems the finite set as +conceived represents not a bounded natural, but the idea of a bounded natural. +One can not destructure the set into any one natural in the bounded range. + +For the indexing to work, the natural indicating the index of the element in the +vector would have to be bounded. The natural would thus require existing as a +dependent pair alongside a proposition indexed by the limit and natural. + +This proposition to limit a natural knowing the upper bound seems expressed by +the finite set. The problem with the finite set is that the natural is not +accessbile; Only the limit is known. + +A better proposition would have to build on both ideas; It would have to be +indexed by a limit and a natural. The latter would indicate the natural in +question to bound, while the former would indicate the limit of the natural. + +This seems a lot like a less-than-or-equal binary relation. It is indexed by two +naturals, and indicates the lhs is strictly smaller than or equal to the rhs. +This idea seems exactly like what vector indexing needs. + +One would wonder then what is the point of finite sets, then. It would seem +finite sets alone do not provide much value; std implements a bunch of utilities +for roundtripping between a finite set and a natural. + +These utilites deal with integer overloads between naturals and integers, and +then between naturals and finite sets. This sometimes implies use of type system +subversion through belive_me. This is unrelated to the topic at hand. + +It would seem providing a finite set then, so long as there existed a way of +converting from a natural to a finite set, is acceptable. This would mean that +the finite set exists within the limits of the vector. + +Let there be a pause to go back to Rust matters. From 0114011681466c00798f30d1b03d4703e3d47c69 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:49:10 +0200 Subject: [PATCH 041/100] chore: wip --- ctest/src/ffi_items.rs | 43 +++++++++++++++++++++++++++++++++++------- notes/main.typ | 43 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index eaa17bf31ff38..f2115aa17ad09 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -307,22 +307,51 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => { macro_rules! single_item { ($field:ident , $it:tt : $ty:tt) => {{ - let mut new_path = $it.path; - new_path.pop(); - new_path.push(rename); + let mut new_segments = $it.path.segments; + new_segments.pop(); + new_segments.push(syn::PathSegment { + ident: rename.clone(), + arguments: syn::PathArguments::None, + }); + let new_path = syn::Path { segments: new_segments, ..$it.path }; let new_item = $ty { ident: path_to_string(&new_path), - path: same_path, - ..$id, + path: new_path, + ..$it }; Resolution::Resolved { original_use, - items: FfiItems { $field: new_item, ..Default::default() }, + items: FfiItems { $field: vec![new_item], ..Default::default() }, } }}; } match state.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => todo!(), + Some(GenericItem::Module(m)) => { + let new_path = { + let mut base_path_segments = m.path.segments.clone(); + base_path_segments.pop(); + base_path_segments.push(syn::PathSegment { + ident: rename.clone(), + arguments: syn::PathArguments::None, + }); + syn::Path { segments: base_path_segments, ..m.path } + }; + let new_cache = path_to_string(&new_path); + let new_path_len = new_path.segments.len(); + let new_module = manipulate_path(|p| { + let new_path = p.segments + .clone() + .into_iter() + .skip(new_path_len) + .fold(new_path.clone(), |mut p, s| { + p.segments.push(s); + p + }); + syn::Path { leading_colon: p.leading_colon, ..new_path } + }, m); + let new_items = FfiItems { modules: vec![new_module], ..Default::default() }; + Resolution::Resolved { original_use, items: new_items } + } Some(GenericItem::Type(t)) => single_item!(aliases, t: Type), Some(GenericItem::Struct(s)) => single_item!(structs, s: Struct), Some(GenericItem::Union(u)) => single_item!(unions, u: Union), diff --git a/notes/main.typ b/notes/main.typ index 4c228d82e8a6a..eca4f855265d3 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -130,7 +130,48 @@ be automated by means of an MBE. The path field is always available, no matter the item; All but the final operation is then item-independent. The last required action is to reconstruct the item with the updated path, and -the updated cache for the path. This should be fairly simple to accomplish. +the updated cache for the path. This should be fairly simple to accomplish. The +only case left to address is that of modules. + +When matching against a module in the rename case, the proof should instead use +the path manipulation proposition. The closure assumed true in that proposition +should capture the original path of the subtree root module. + +Then for each path passed to the closure, it should extract whatever is left of +the path after the root set of segments, and prepend to that the updated path to +the root module. This goes through potentially caching the updated path. + +This idea is still fuzzy; How does one find the base path of the passed root +module in the path of each item? This can be accomplished by first taking the +length of the base path, and then removing that suffix from the path segments. + +The length can be obtained from the ancestor module outside of the closure. The +type in syn for representing path segments does not allow removal from the +front. One simple alternative is to perform random-access insertion. + +Firstly, the length of the ancestor module's path gets stored outside the +closure. Then an iterator from this length to the length of the path passed in +the closure fetches each segment pertaining only to the item's path. + +Then this gets gathered into a vector, the elements of which are iterated over +to be inserted into the updated and cached base path of the root module living +outside the closure. + +An even better approach may be to get first the difference of the length of the +item's path with respect to the module's path, and then pop from the back as +many times as that difference indicates. + +Then this can be gathered in a vector, to be reversed prior to being iterated +over and added to a new copy of the cached and updated version of the root +module's path (again, living outside the closure.) + +Or a simpler idea would be to iterate over all segments of the item's path, +skipping first through the length of the ancestor module's path length. Then the +remaining segments can be gathered into a vector. + +The vector would then be folded with a seed value consisting of a copy of the +cached new ancestor path, to which all previoulsy collected, item-specific path +segments would be appended. = Idris community tutorial Before starting with the Idris community tutorial, it would be best to finish From 6c58b3c32b8fb0b6bfd1da777cd479dfca9d2a18 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:05:33 +0200 Subject: [PATCH 042/100] chore: wip --- ctest/src/ffi_items.rs | 286 ++++++++++++++++++++++++++++++++++------- ctest/src/generator.rs | 1 - notes/main.typ | 53 +++++++- 3 files changed, 288 insertions(+), 52 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index f2115aa17ad09..dc6996221cf03 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1,18 +1,23 @@ //! Conversion of Rust code to a simplified abstract syntax tree. use std::borrow::Borrow; -use std::ops::{self, Deref}; use std::iter; -use std::ops::ControlFlow; +use std::ops::{ + self, + ControlFlow, + Deref, +}; use quote::ToTokens; -use syn::Visibility; use syn::punctuated::Punctuated; -use syn::UseTree; use syn::visit::{ self, Visit, }; +use syn::{ + UseTree, + Visibility, +}; use crate::{ Abi, @@ -115,7 +120,11 @@ impl FfiItems { /// instance is always left with no reexports that are not sourced from /// third-party crates. pub(crate) fn visit_file(&mut self, file: &syn::File) { - let syn::File { attrs, items: mod_items, .. } = file; + let syn::File { + attrs, + items: mod_items, + .. + } = file; let file_mod = syn::ItemMod { attrs: attrs.clone(), vis: syn::parse_quote! { pub }, @@ -148,9 +157,18 @@ impl FfiItems { let structs = self.structs.clone().into_iter().map(GenericItem::Struct); let unions = self.unions.clone().into_iter().map(GenericItem::Union); let constants = self.constants.clone().into_iter().map(GenericItem::Const); - let foreign_functions = self.foreign_functions.clone().into_iter().map(GenericItem::Fn); - let foreign_statics = self.foreign_statics.clone().into_iter().map(GenericItem::Static); - let mut items: Vec<_> = self.modules + let foreign_functions = self + .foreign_functions + .clone() + .into_iter() + .map(GenericItem::Fn); + let foreign_statics = self + .foreign_statics + .clone() + .into_iter() + .map(GenericItem::Static); + let mut items: Vec<_> = self + .modules .clone() .into_iter() .map(GenericItem::Module) @@ -197,13 +215,13 @@ impl Default for FfiItems { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct RefinedUsePath { ident: syn::Ident, tree: Box, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) enum RefinedUseTree { Path(RefinedUsePath), Name(syn::UseName), @@ -211,7 +229,7 @@ pub(crate) enum RefinedUseTree { Glob, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct RefinedUse { is_public: bool, tree: RefinedUseTree, @@ -223,18 +241,18 @@ fn normalize_path(path: syn::UseTree) -> Vec { match path { UseTree::Name(n) => vec![RefinedUseTree::Name(n)], UseTree::Rename(r) => vec![RefinedUseTree::Rename(r)], - UseTree::Glob(g) => vec![RefinedUseTree::Glob], + UseTree::Glob(_) => vec![RefinedUseTree::Glob], - UseTree::Path(syn::UsePath { ident, tree, .. }) => { - normalize_path(*tree) - .into_iter() - .map(Box::new) - .map(|tree| RefinedUseTree::Path(RefinedUsePath { + UseTree::Path(syn::UsePath { ident, tree, .. }) => normalize_path(*tree) + .into_iter() + .map(Box::new) + .map(|tree| { + RefinedUseTree::Path(RefinedUsePath { ident: ident.clone(), - tree - })) - .collect() - } + tree, + }) + }) + .collect(), UseTree::Group(syn::UseGroup { items, .. }) => { items.into_iter().map(normalize_path).flatten().collect() @@ -243,12 +261,20 @@ fn normalize_path(path: syn::UseTree) -> Vec { } enum Resolution { - Resolved { original_use: RefinedUse, items: FfiItems }, + Resolved { + original_use: RefinedUse, + items: FfiItems, + }, Unresolved, } fn resolve_use_trees(root: Module) -> Module { - let resolved_children = root.items.modules.into_iter().map(resolve_use_trees).collect(); + let resolved_children = root + .items + .modules + .into_iter() + .map(resolve_use_trees) + .collect(); let root = { let items = FfiItems { modules: resolved_children, @@ -262,14 +288,15 @@ fn resolve_use_trees(root: Module) -> Module { let new_root = merge_module(root.clone(), resolved_uses); match root.items.uses.len() == new_root.items.uses.len() { true => ControlFlow::Break(new_root), - false => ControlFlow::Continue(new_root) + false => ControlFlow::Continue(new_root), } }); root } fn resolve_one(src: Module) -> Vec { - src.items.uses + src.items + .uses .clone() .into_iter() .map(|u| (u.clone(), u, src.clone())) @@ -303,7 +330,10 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re None => Resolution::Unresolved, } } - RefinedUseTree::Glob => Resolution::Resolved { original_use, items: state.items }, + RefinedUseTree::Glob => Resolution::Resolved { + original_use, + items: state.items, + }, RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => { macro_rules! single_item { ($field:ident , $it:tt : $ty:tt) => {{ @@ -313,7 +343,10 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re ident: rename.clone(), arguments: syn::PathArguments::None, }); - let new_path = syn::Path { segments: new_segments, ..$it.path }; + let new_path = syn::Path { + segments: new_segments, + ..$it.path + }; let new_item = $ty { ident: path_to_string(&new_path), path: new_path, @@ -321,7 +354,10 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re }; Resolution::Resolved { original_use, - items: FfiItems { $field: vec![new_item], ..Default::default() }, + items: FfiItems { + $field: vec![new_item], + ..Default::default() + }, } }}; } @@ -334,23 +370,36 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re ident: rename.clone(), arguments: syn::PathArguments::None, }); - syn::Path { segments: base_path_segments, ..m.path } + syn::Path { + segments: base_path_segments, + ..m.path + } }; - let new_cache = path_to_string(&new_path); let new_path_len = new_path.segments.len(); - let new_module = manipulate_path(|p| { - let new_path = p.segments - .clone() - .into_iter() - .skip(new_path_len) - .fold(new_path.clone(), |mut p, s| { - p.segments.push(s); - p - }); - syn::Path { leading_colon: p.leading_colon, ..new_path } - }, m); - let new_items = FfiItems { modules: vec![new_module], ..Default::default() }; - Resolution::Resolved { original_use, items: new_items } + let new_module = manipulate_path( + |p| { + let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( + new_path.clone(), + |mut p, s| { + p.segments.push(s); + p + }, + ); + syn::Path { + leading_colon: p.leading_colon, + ..new_path + } + }, + m, + ); + let new_items = FfiItems { + modules: vec![new_module], + ..Default::default() + }; + Resolution::Resolved { + original_use, + items: new_items, + } } Some(GenericItem::Type(t)) => single_item!(aliases, t: Type), Some(GenericItem::Struct(s)) => single_item!(structs, s: Struct), @@ -358,7 +407,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re Some(GenericItem::Const(c)) => single_item!(constants, c: Const), Some(GenericItem::Fn(f)) => single_item!(foreign_functions, f: Fn), Some(GenericItem::Static(s)) => single_item!(foreign_statics, s: Static), - None => Resolution::Unresolved + None => Resolution::Unresolved, } } @@ -388,12 +437,16 @@ fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module let new_root_cache = path_to_string(&new_root_path); macro_rules! map_items { ($field:ident: $ty:tt) => {{ - root.items.$field + root.items + .$field .clone() .into_iter() .map(|it| { let new_path = f(it.path); - $ty { path: new_path, ..it } + $ty { + path: new_path, + ..it + } }) .collect::>() }}; @@ -404,7 +457,9 @@ fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module let new_constants = map_items!(constants: Const); let new_foreign_functions = map_items!(foreign_functions: Fn); let new_foreign_statics = map_items!(foreign_statics: Static); - let new_modules: Vec<_> = root.items.modules + let new_modules: Vec<_> = root + .items + .modules .clone() .into_iter() .map(|m| (f.clone(), m)) @@ -429,7 +484,134 @@ fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module } fn merge_module(dst: Module, src: Vec) -> Module { - todo!(); + let new_path = dst.path.clone(); + src.into_iter() + .filter_map(|r| { + if let Resolution::Resolved { + original_use, + items, + } = r + { + Some((original_use, items)) + } else { + None + } + }) + .map( + |( + ou, + FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + + uses, + current_module, + }, + )| { + macro_rules! single_item { + ($field:ident: $ty:tt) => {{ + $field + .into_iter() + .map(|it| { + let Some(ident) = it.path.segments.last().cloned() else { + unreachable!( + "all parsed items have at least one \ + segment in their paths" + ); + }; + let mut new_path = new_path.clone(); + new_path.segments.push(ident); + $ty { + path: new_path, + ..it + } + }) + .collect::>() + }}; + } + let new_aliases = single_item!(aliases: Type); + let new_structs = single_item!(structs: Struct); + let new_unions = single_item!(unions: Union); + let new_constants = single_item!(constants: Const); + let new_foreign_functions = single_item!(foreign_functions: Fn); + let new_foreign_statics = single_item!(foreign_statics: Static); + let new_modules = modules + .into_iter() + .map(|m| { + manipulate_path( + |p| { + let Some(ident) = p.segments.last().cloned() else { + unreachable!( + "all parsed items have at least one \ + segment in their paths" + ) + }; + let mut new_path = new_path.clone(); + new_path.segments.push(ident); + new_path + }, + m, + ) + }) + .collect(); + ( + ou, + FfiItems { + aliases: new_aliases, + structs: new_structs, + unions: new_unions, + constants: new_constants, + foreign_functions: new_foreign_functions, + foreign_statics: new_foreign_statics, + modules: new_modules, + uses, + current_module, + }, + ) + }, + ) + .fold( + dst, + |mut dst, + ( + ou, + FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + .. + }, + )| { + let Some(idx) = dst.items.uses.iter().position(|u| *u == ou) else { + unreachable!( + "the use statement was sourced from the module which \ + this fold is using as seed value" + ) + }; + dst.items.uses.swap_remove(idx); + dst.items.aliases.extend_from_slice(&aliases); + dst.items.structs.extend_from_slice(&structs); + dst.items.unions.extend_from_slice(&unions); + dst.items.constants.extend_from_slice(&constants); + dst.items + .foreign_functions + .extend_from_slice(&foreign_functions); + dst.items + .foreign_statics + .extend_from_slice(&foreign_statics); + dst.items.modules.extend_from_slice(&modules); + dst + }, + ) } /// Appends a new module-local item to an absolute path that does *not* start @@ -654,7 +836,13 @@ impl<'ast> Visit<'ast> for FfiItems { } fn visit_item_mod(&mut self, i: &'ast syn::ItemMod) { - let syn::ItemMod { vis, ident, content: Some((_, mod_items)), .. } = i else { + let syn::ItemMod { + vis, + ident, + content: Some((_, mod_items)), + .. + } = i + else { unreachable!("this runs post cargo-expand, which inlines all modules"); }; let uses: Vec<_> = mod_items diff --git a/ctest/src/generator.rs b/ctest/src/generator.rs index d1a914e35c566..6faf4182b23e9 100644 --- a/ctest/src/generator.rs +++ b/ctest/src/generator.rs @@ -10,7 +10,6 @@ use std::path::{ use std::rc::Rc; use askama::Template; -use syn::visit::Visit; use thiserror::Error; use crate::ffi_items::FfiItems; diff --git a/notes/main.typ b/notes/main.typ index eca4f855265d3..36e4d0c261730 100644 --- a/notes/main.typ +++ b/notes/main.typ @@ -171,7 +171,25 @@ remaining segments can be gathered into a vector. The vector would then be folded with a seed value consisting of a copy of the cached new ancestor path, to which all previoulsy collected, item-specific path -segments would be appended. +segments would be appended. It would seem the proposition for handling +single-use-statement resolution has had its proof finished. The only thing left +is to provide proof for the merging proposition. In this case, this is known to +iterate through the resolution items, ignoring those that are marked unresolved. +For each resolved item, the associated use statement should be removed from the +module. Beyond that, merging should integrate the items in the container carried +for this one injection of the resolution coproduct, into the destination module. +This, as mentioned during elaboration of the path manipulation function, +requires renaming all paths in both items and module subtrees rooted at the +items to merge, such that everything but their last path segment is replaced +with the destination's module path segment. In other words, merging of resolved +items requires considering the case for non-module items, and the case for +modules. Something similar was already proofed in the rename case of the +single-use-statement resolution proposition. Maybe this is worth abstracting, or +maybe not. Either way, it seems clear items should have everything but their +trailing path segment replaced with the path of the module into which to merge. +Modules need to be fed into the path manipulation proposition, with a closure +that, again, extracts the last segment of the path, and prepends to it the path +of the module into which to merge. = Idris community tutorial Before starting with the Idris community tutorial, it would be best to finish @@ -274,4 +292,35 @@ It would seem providing a finite set then, so long as there existed a way of converting from a natural to a finite set, is acceptable. This would mean that the finite set exists within the limits of the vector. -Let there be a pause to go back to Rust matters. +The role of finite sets is then only to provide some bound in the type signature +of a proposition. The actual work of transforming the integer into a natural in +the range denoted by the finite set is delegated to some other function. + +This would mean that the finite set is always useless outside of the guarantees +it provides at the type level. There's always need for a supporting +infrastructure to hold up the transformation between a natural and the set. + +This then means that a custom finite set is not enough for none of the indexing +nor the vector splitting routine. There seems to be an interface for converting +between integers and some other type, which is used by std's finite sets. + +Implementing integer to finite set conversion seems not to be a simple endeavor. +The integer cannot be merely any integer, and neither can it be cast first into +a natural, and pattern-matched to be used in constructing a finite set. + +The finite set is already defined in the propositions for the Num interface as +providing a bound for some natural. This means integer overloading could fail. +Most notably, a negative integer would not be good enough. + +One should only be capable of constructing a finite set off of an unsigned +integer literal. One could introduce such a constraint prior to the propositions +in the Num interface, and use the constrained integer for overloading. + +It would seem setting up constraints prior to introducing the interface +constraint is possible. With that in mind, it then becomes possible to establish +that provided a certain integer n, this integer will only become a finite set if +another proposition determines that this is possible. This other proposition +would imply a type that would serve as proposition requirement and would either +have to be proof-searched or otherwise manually provided. In theory, a proof +search seems feasible because this exists for the sole purpose of handling +overloaded literals. From b376b8dfb8971e5abf016b400884a8548bc1ec0a Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:44:21 +0200 Subject: [PATCH 043/100] chore: wip --- ctest/src/ast/constant.rs | 4 +- ctest/src/ast/function.rs | 4 +- ctest/src/ast/module.rs | 4 +- ctest/src/ast/static_variable.rs | 4 +- ctest/src/ast/structure.rs | 4 +- ctest/src/ast/type_alias.rs | 4 +- ctest/src/ast/union.rs | 4 +- ctest/src/ffi_items.rs | 156 +++++++++++++++++-------------- 8 files changed, 99 insertions(+), 85 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index 4b78b6efbe6bc..84d8d0ac44f05 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -4,7 +4,7 @@ use crate::BoxStr; #[derive(Debug, Clone)] pub struct Const { pub(crate) public: bool, - pub(crate) ident: BoxStr, + pub(crate) cached_path: BoxStr, pub(crate) path: syn::Path, pub(crate) ty: syn::Type, } @@ -17,7 +17,7 @@ impl Const { /// `crate::bar`, the returned string will be `bar::foo`, and not /// `crate::bar::foo`. pub fn path(&self) -> &str { - &self.ident + &self.cached_path } /// Returns the last path of the identifier, from the absolute path returned diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index 60ee882ba8c1c..ce1d70cd9a6a3 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -12,7 +12,7 @@ pub struct Fn { pub(crate) public: bool, #[expect(unused)] pub(crate) abi: Abi, - pub(crate) ident: BoxStr, + pub(crate) cached_path: BoxStr, pub(crate) path: syn::Path, pub(crate) link_name: Option, #[expect(unused)] @@ -29,7 +29,7 @@ impl Fn { /// `crate::bar`, the returned string will be `bar::foo`, and not /// `crate::bar::foo`. pub fn path(&self) -> &str { - &self.ident + &self.cached_path } /// Returns the last path of the identifier, from the absolute path returned diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index faedb33c50137..c624086a784a9 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -6,7 +6,7 @@ use crate::ffi_items::FfiItems; #[derive(Debug, Clone)] pub struct Module { pub(crate) public: bool, - pub(crate) ident: BoxStr, + pub(crate) cached_path: BoxStr, pub(crate) path: syn::Path, pub(crate) items: FfiItems, } @@ -19,7 +19,7 @@ impl Module { /// `crate::bar`, the returned string will be `bar::foo`, and not /// `crate::bar::foo`. pub fn path(&self) -> &str { - &self.ident + &self.cached_path } /// Returns the last path of the identifier, from the absolute path returned diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index ab1797174b933..d6115c437250b 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -12,7 +12,7 @@ pub struct Static { pub(crate) public: bool, #[expect(unused)] pub(crate) abi: Abi, - pub(crate) ident: BoxStr, + pub(crate) cached_path: BoxStr, pub(crate) path: syn::Path, pub(crate) link_name: Option, pub(crate) ty: syn::Type, @@ -26,7 +26,7 @@ impl Static { /// `crate::bar`, the returned string will be `bar::foo`, and not /// `crate::bar::foo`. pub fn path(&self) -> &str { - &self.ident + &self.cached_path } /// Returns the last path of the identifier, from the absolute path returned diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index dc47731c44773..11d8edaba3929 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -7,7 +7,7 @@ use crate::{ #[derive(Debug, Clone)] pub struct Struct { pub(crate) public: bool, - pub(crate) ident: BoxStr, + pub(crate) cached_path: BoxStr, pub(crate) path: syn::Path, pub(crate) fields: Vec, } @@ -20,7 +20,7 @@ impl Struct { /// `crate::bar`, the returned string will be `bar::foo`, and not /// `crate::bar::foo`. pub fn path(&self) -> &str { - &self.ident + &self.cached_path } /// Returns the last path of the identifier, from the absolute path returned diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index 0d5c79a021938..72d4b75401265 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -4,7 +4,7 @@ use crate::BoxStr; #[derive(Debug, Clone)] pub struct Type { pub(crate) public: bool, - pub(crate) ident: BoxStr, + pub(crate) cached_path: BoxStr, pub(crate) path: syn::Path, pub(crate) ty: syn::Type, } @@ -17,7 +17,7 @@ impl Type { /// `crate::bar`, the returned string will be `bar::foo`, and not /// `crate::bar::foo`. pub fn path(&self) -> &str { - &self.ident + &self.cached_path } /// Returns the last path of the identifier, from the absolute path returned diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index a49278d0de737..6f1fa2975f856 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -7,7 +7,7 @@ use crate::{ #[derive(Debug, Clone)] pub struct Union { pub(crate) public: bool, - pub(crate) ident: BoxStr, + pub(crate) cached_path: BoxStr, pub(crate) path: syn::Path, pub(crate) fields: Vec, } @@ -20,7 +20,7 @@ impl Union { /// `crate::bar`, the returned string will be `bar::foo`, and not /// `crate::bar::foo`. pub fn path(&self) -> &str { - &self.ident + &self.cached_path } /// Returns the last path of the identifier, from the absolute path returned diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index dc6996221cf03..02485f2e422b3 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -137,7 +137,7 @@ impl FfiItems { self.visit_item_mod(&file_mod); let root_module = Module { public: bool::default(), - ident: String::new().into_boxed_str(), + cached_path: String::new().into_boxed_str(), path: syn::Path { leading_colon: None, segments: Punctuated::new(), @@ -348,7 +348,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re ..$it.path }; let new_item = $ty { - ident: path_to_string(&new_path), + cached_path: path_to_string(&new_path), path: new_path, ..$it }; @@ -443,7 +443,9 @@ fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module .into_iter() .map(|it| { let new_path = f(it.path); + let new_cache = path_to_string(&new_path); $ty { + cached_path: new_cache, path: new_path, ..it } @@ -476,7 +478,7 @@ fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module ..Default::default() }; Module { - ident: new_root_cache, + cached_path: new_root_cache, path: new_root_path, items: new_items, ..root @@ -508,52 +510,73 @@ fn merge_module(dst: Module, src: Vec) -> Module { foreign_functions, foreign_statics, modules, - - uses, - current_module, + .. }, )| { - macro_rules! single_item { - ($field:ident: $ty:tt) => {{ - $field - .into_iter() - .map(|it| { - let Some(ident) = it.path.segments.last().cloned() else { - unreachable!( - "all parsed items have at least one \ - segment in their paths" - ); - }; - let mut new_path = new_path.clone(); - new_path.segments.push(ident); - $ty { - path: new_path, - ..it - } - }) - .collect::>() - }}; - } - let new_aliases = single_item!(aliases: Type); - let new_structs = single_item!(structs: Struct); - let new_unions = single_item!(unions: Union); - let new_constants = single_item!(constants: Const); - let new_foreign_functions = single_item!(foreign_functions: Fn); - let new_foreign_statics = single_item!(foreign_statics: Static); - let new_modules = modules + // [NOTE]: this sequence must have at least one segment, because + // the invariant assumed in the closure for path manipulation of + // the items to merge assuems so. This is true for all items to + // merge, but this module wrapping all those items is only + // virtual in nature; The containig items (post-path-renaming) + // will be extracted, and the module trashed. But the module, + // even though a dummy, will also have the clsoure further down + // below applied to its path, so it needs to be non-empty. + let mut dummy_segment = syn::punctuated::Punctuated::new(); + dummy_segment.push(syn::PathSegment { + ident: syn::Ident::new("dummy", proc_macro2::Span::call_site()), + arguments: syn::PathArguments::None, + }); + let items_without_modules = FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + ..Default::default() + }; + let module = Module { + public: false, + cached_path: String::new().into_boxed_str(), + path: syn::Path { + leading_colon: None, + segments: dummy_segment, + }, + items: items_without_modules, + }; + let new_module = manipulate_path( + |p| { + let Some(ident) = p.segments.last().cloned() else { + unreachable!( + "all parsed items have at least one segment in \ + their paths" + ) + }; + let mut new_path = new_path.clone(); + new_path.segments.push(ident); + new_path + }, + module, + ); + let new_nested_modules: Vec<_> = modules .into_iter() .map(|m| { + let Some(base_path) = m.path.segments.last().cloned() else { + unreachable!( + "all items post-parsing have at least one \ + segment in their paths" + ); + }; manipulate_path( |p| { - let Some(ident) = p.segments.last().cloned() else { - unreachable!( - "all parsed items have at least one \ - segment in their paths" - ) - }; - let mut new_path = new_path.clone(); - new_path.segments.push(ident); - new_path + p.segments + .clone() + .into_iter() + .skip_while(|s| *s != base_path) + .fold(new_path.clone(), |mut p, s| { + p.segments.push(s); + p + }) }, m, ) @@ -562,15 +585,8 @@ fn merge_module(dst: Module, src: Vec) -> Module { ( ou, FfiItems { - aliases: new_aliases, - structs: new_structs, - unions: new_unions, - constants: new_constants, - foreign_functions: new_foreign_functions, - foreign_statics: new_foreign_statics, - modules: new_modules, - uses, - current_module, + modules: new_nested_modules, + ..new_module.items }, ) }, @@ -694,7 +710,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi let public = is_visible(&i.vis); let abi = abi.clone(); let path = append_path(&table.current_module, &i.sig.ident); - let ident = path_to_string(&path); + let cached_path = path_to_string(&path); let parameters = i .sig .inputs @@ -726,7 +742,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi table.foreign_functions.push(Fn { public, abi, - ident, + cached_path, path, link_name, parameters, @@ -738,14 +754,14 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a let public = is_visible(&i.vis); let abi = abi.clone(); let path = append_path(&table.current_module, &i.ident); - let ident = path_to_string(&path); + let cached_path = path_to_string(&path); let ty = i.ty.deref().clone(); let link_name = extract_single_link_name(&i.attrs); table.foreign_statics.push(Static { public, abi, - ident, + cached_path, path, link_name, ty, @@ -756,12 +772,12 @@ 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 cached_path = path_to_string(&path); let ty = i.ty.deref().clone(); self.aliases.push(Type { public, - ident, + cached_path, path, ty, }); @@ -770,7 +786,7 @@ impl<'ast> Visit<'ast> for FfiItems { fn visit_item_struct(&mut self, i: &'ast syn::ItemStruct) { let public = is_visible(&i.vis); let path = append_path(&self.current_module, &i.ident); - let ident = path_to_string(&path); + let cached_path = 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), @@ -779,7 +795,7 @@ impl<'ast> Visit<'ast> for FfiItems { self.structs.push(Struct { public, - ident, + cached_path, path, fields, }); @@ -788,12 +804,12 @@ impl<'ast> Visit<'ast> for FfiItems { fn visit_item_union(&mut self, i: &'ast syn::ItemUnion) { let public = is_visible(&i.vis); let path = append_path(&self.current_module, &i.ident); - let ident = path_to_string(&path); + let cached_path = path_to_string(&path); let fields = collect_fields(&i.fields.named); self.unions.push(Union { public, - ident, + cached_path, path, fields, }); @@ -802,12 +818,12 @@ impl<'ast> Visit<'ast> for FfiItems { fn visit_item_const(&mut self, i: &'ast syn::ItemConst) { let public = is_visible(&i.vis); let path = append_path(&self.current_module, &i.ident); - let ident = path_to_string(&path); + let cached_path = path_to_string(&path); let ty = i.ty.deref().clone(); self.constants.push(Const { public, - ident, + cached_path, path, ty, }); @@ -875,14 +891,14 @@ impl<'ast> Visit<'ast> for FfiItems { } else { let public = matches!(vis, Visibility::Public(_)); let path = append_path(&self.current_module, ident); - let ident = path_to_string(&path); + let cached_path = path_to_string(&path); let mut items = FfiItems::new(); items.current_module = path.clone(); visit::visit_item_mod(&mut items, i); items.uses = uses; self.modules.push(Module { public, - ident, + cached_path, path, items, }); @@ -893,13 +909,11 @@ impl<'ast> Visit<'ast> for FfiItems { #[test] fn tmp() { let source = r#" -use std::*; +use test2::*; -mod test { use std::any::*; pub struct Foo; } +use test::*; -fn main() { - use std::any::*; -} +mod test { mod test2 { pub struct Foo; } } "#; let mut items = FfiItems::default(); let file = syn::parse_file(source).unwrap(); From 68f45b5f85c3f6600eed9a609e9937e5e73ba3ce Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:17:32 +0200 Subject: [PATCH 044/100] chore: wip --- ctest/src/ffi_items.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 02485f2e422b3..10ab3e0bb64b2 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -194,6 +194,10 @@ impl FfiItems { }; items.swap_remove(idx).into() } + + fn dedup(&mut self) { + todo!(); + } } impl Default for FfiItems { @@ -515,11 +519,11 @@ fn merge_module(dst: Module, src: Vec) -> Module { )| { // [NOTE]: this sequence must have at least one segment, because // the invariant assumed in the closure for path manipulation of - // the items to merge assuems so. This is true for all items to + // the items to merge assumes so. This is true for all items to // merge, but this module wrapping all those items is only // virtual in nature; The containig items (post-path-renaming) // will be extracted, and the module trashed. But the module, - // even though a dummy, will also have the clsoure further down + // even though a dummy, will also have the closure further down // below applied to its path, so it needs to be non-empty. let mut dummy_segment = syn::punctuated::Punctuated::new(); dummy_segment.push(syn::PathSegment { From a37e9be10fef99a23ee80609cba76c3b1d224898 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:01:40 +0200 Subject: [PATCH 045/100] chore: wip --- ctest/src/ffi_items.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 10ab3e0bb64b2..c6cae83bd0449 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -195,8 +195,26 @@ impl FfiItems { items.swap_remove(idx).into() } - fn dedup(&mut self) { - todo!(); + fn dedup(mut self) -> FfiItems { + macro_rules! key_fn { + () => { + |it| it.cached_path.clone() + }; + } + macro_rules! dedup { + ($it:ident) => {{ + self.$it.sort_by_key(key_fn!()); + self.$it.dedup_by_key(key_fn!()); + }}; + } + dedup!(aliases); + dedup!(structs); + dedup!(unions); + dedup!(constants); + dedup!(foreign_functions); + dedup!(foreign_statics); + dedup!(modules); + self } } @@ -290,6 +308,10 @@ fn resolve_use_trees(root: Module) -> Module { iter::repeat(()).try_fold(root, |root, _| { let resolved_uses = resolve_one(root.clone()); let new_root = merge_module(root.clone(), resolved_uses); + let new_root = Module { + items: new_root.items.dedup(), + ..new_root + }; match root.items.uses.len() == new_root.items.uses.len() { true => ControlFlow::Break(new_root), false => ControlFlow::Continue(new_root), @@ -917,7 +939,7 @@ use test2::*; use test::*; -mod test { mod test2 { pub struct Foo; } } +mod test { use test2::*; mod test2 { pub struct Foo; } } "#; let mut items = FfiItems::default(); let file = syn::parse_file(source).unwrap(); From 4b7454b47763f983a71416b9f596784c1a5ae69f Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:08:45 +0200 Subject: [PATCH 046/100] chore: wip --- ctest/src/ffi_items.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index c6cae83bd0449..9a4593c704f54 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -935,10 +935,10 @@ impl<'ast> Visit<'ast> for FfiItems { #[test] fn tmp() { let source = r#" -use test2::*; - use test::*; +use test::test2::*; + mod test { use test2::*; mod test2 { pub struct Foo; } } "#; let mut items = FfiItems::default(); From bb138375b76391bf9b7a3f0daed6212a685a5618 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:36:41 +0200 Subject: [PATCH 047/100] chore: wip --- ctest/src/ffi_items.rs | 215 ++++++++++++++++++++++++----------------- 1 file changed, 128 insertions(+), 87 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 9a4593c704f54..6eb4fe69f58c0 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -755,7 +755,10 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi ty: arg.ty.deref().clone(), }, syn::FnArg::Receiver(_) => { - unreachable!("Foreign functions can't have self/receiver parameters.") + unreachable!( + "Foreign functions can't have self/receiver \ + parameters." + ) } }) .collect::>(); @@ -794,89 +797,89 @@ 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 cached_path = path_to_string(&path); - let ty = i.ty.deref().clone(); - - self.aliases.push(Type { - public, - cached_path, - path, - ty, - }); - } +fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { + let public = is_visible(&i.vis); + let path = append_path(&table.current_module, &i.ident); + let cached_path = path_to_string(&path); + let ty = i.ty.deref().clone(); - fn visit_item_struct(&mut self, i: &'ast syn::ItemStruct) { - let public = is_visible(&i.vis); - let path = append_path(&self.current_module, &i.ident); - let cached_path = 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), - syn::Fields::Unit => Vec::new(), - }; + table.aliases.push(Type { + public, + cached_path, + path, + ty, + }); +} - self.structs.push(Struct { - public, - cached_path, - path, - fields, - }); - } +fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { + let public = is_visible(&i.vis); + let path = append_path(&table.current_module, &i.ident); + let cached_path = 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), + syn::Fields::Unit => Vec::new(), + }; - fn visit_item_union(&mut self, i: &'ast syn::ItemUnion) { - let public = is_visible(&i.vis); - let path = append_path(&self.current_module, &i.ident); - let cached_path = path_to_string(&path); - let fields = collect_fields(&i.fields.named); - - self.unions.push(Union { - public, - cached_path, - path, - fields, - }); - } + table.structs.push(Struct { + public, + cached_path, + path, + fields, + }); +} - fn visit_item_const(&mut self, i: &'ast syn::ItemConst) { - let public = is_visible(&i.vis); - let path = append_path(&self.current_module, &i.ident); - let cached_path = path_to_string(&path); - let ty = i.ty.deref().clone(); - - self.constants.push(Const { - public, - cached_path, - path, - ty, - }); - } +fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { + let public = is_visible(&i.vis); + let path = append_path(&table.current_module, &i.ident); + let cached_path = path_to_string(&path); + let fields = collect_fields(&i.fields.named); - fn visit_item_foreign_mod(&mut self, i: &'ast syn::ItemForeignMod) { - // Because we need to store the ABI we can't directly visit the foreign - // functions/statics. + table.unions.push(Union { + public, + cached_path, + path, + fields, + }); +} - // Since this is an extern block, assume extern "C" by default. - let abi = i - .abi - .name - .clone() - .map_or(Abi::C, |s| Abi::from(s.value().as_str())); +fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { + let public = is_visible(&i.vis); + let path = append_path(&table.current_module, &i.ident); + let cached_path = path_to_string(&path); + let ty = i.ty.deref().clone(); - for item in &i.items { - match item { - syn::ForeignItem::Fn(function) => visit_foreign_item_fn(self, function, &abi), - syn::ForeignItem::Static(static_variable) => { - visit_foreign_item_static(self, static_variable, &abi) - } - _ => (), + table.constants.push(Const { + public, + cached_path, + path, + ty, + }); +} + +fn visit_item_foreign_mod(table: &mut FfiItems, i: &syn::ItemForeignMod) { + // Because we need to store the ABI we can't directly visit the foreign + // functions/statics. + + // Since this is an extern block, assume extern "C" by default. + let abi = i + .abi + .name + .clone() + .map_or(Abi::C, |s| Abi::from(s.value().as_str())); + + for item in &i.items { + match item { + syn::ForeignItem::Fn(function) => visit_foreign_item_fn(table, function, &abi), + syn::ForeignItem::Static(static_variable) => { + visit_foreign_item_static(table, static_variable, &abi) } + _ => (), } } +} +impl<'ast> Visit<'ast> for FfiItems { fn visit_item_mod(&mut self, i: &'ast syn::ItemMod) { let syn::ItemMod { vis, @@ -885,23 +888,43 @@ impl<'ast> Visit<'ast> for FfiItems { .. } = i else { - unreachable!("this runs post cargo-expand, which inlines all modules"); + unreachable!( + "this runs post cargo-expand, which inlines all \ + modules" + ); }; let uses: Vec<_> = mod_items .iter() .cloned() - .filter_map(|it| { - if let syn::Item::Use(syn::ItemUse { vis, tree, .. }) = it { - normalize_path(tree) - .into_iter() - .map(move |tree| RefinedUse { - is_public: matches!(vis, syn::Visibility::Public(_)), - tree, - }) - .into() - } else { + .filter_map(|it| match it { + syn::Item::Type(t) => { + visit_item_type(self, &t); None } + syn::Item::Struct(s) => { + visit_item_struct(self, &s); + None + } + syn::Item::Union(u) => { + visit_item_union(self, &u); + None + } + syn::Item::Const(c) => { + visit_item_const(self, &c); + None + } + syn::Item::ForeignMod(m) => { + visit_item_foreign_mod(self, &m); + None + } + syn::Item::Use(syn::ItemUse { vis, tree, .. }) => normalize_path(tree) + .into_iter() + .map(move |tree| RefinedUse { + is_public: matches!(vis, syn::Visibility::Public(_)), + tree, + }) + .into(), + _ => None, }) .flatten() .collect(); @@ -935,11 +958,29 @@ impl<'ast> Visit<'ast> for FfiItems { #[test] fn tmp() { let source = r#" -use test::*; +use test2::test3; -use test::test2::*; +use test1::*; -mod test { use test2::*; mod test2 { pub struct Foo; } } +use test1::test2::test3::*; + +mod test1 { + use test2::{*, test3::*}; + + mod test2 { + use test3::*; + + mod test3 { + pub struct Foo; + + fn failure() { + extern "C" { + fn ctime(); + } + } + } + } +} "#; let mut items = FfiItems::default(); let file = syn::parse_file(source).unwrap(); From f3032ebef9b521673381e6372e709733d9a9c410 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:36:44 +0200 Subject: [PATCH 048/100] chore: wip --- ctest/src/ffi_items.rs | 52 +++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 6eb4fe69f58c0..19cd55095b521 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -893,28 +893,40 @@ impl<'ast> Visit<'ast> for FfiItems { modules" ); }; + let is_crate_root = ident == "__ctest_root_mod"; + let mut running_items = if is_crate_root { + self.clone() + } else { + let path = append_path(&self.current_module, ident); + let cached_path = path_to_string(&path); + let out = FfiItems { + current_module: path.clone(), + ..Default::default() + }; + out + }; let uses: Vec<_> = mod_items .iter() .cloned() .filter_map(|it| match it { syn::Item::Type(t) => { - visit_item_type(self, &t); + visit_item_type(&mut running_items, &t); None } syn::Item::Struct(s) => { - visit_item_struct(self, &s); + visit_item_struct(&mut running_items, &s); None } syn::Item::Union(u) => { - visit_item_union(self, &u); + visit_item_union(&mut running_items, &u); None } syn::Item::Const(c) => { - visit_item_const(self, &c); + visit_item_const(&mut running_items, &c); None } syn::Item::ForeignMod(m) => { - visit_item_foreign_mod(self, &m); + visit_item_foreign_mod(&mut running_items, &m); None } syn::Item::Use(syn::ItemUse { vis, tree, .. }) => normalize_path(tree) @@ -929,27 +941,19 @@ impl<'ast> Visit<'ast> for FfiItems { .flatten() .collect(); - // [NOTE]: if the module is known to keep be the virtual module that we - // create to process the items in the crate root, then we require not - // creating a new module that will become a child to the current one, - // but rather make all items in the module become the items of the - // current module (the crate root.) - if ident == "__ctest_root_mod" { - visit::visit_item_mod(self, i); - self.uses = uses; + visit::visit_item_mod(&mut running_items, i); + running_items.uses = uses; + + if is_crate_root { + *self = running_items; } else { - let public = matches!(vis, Visibility::Public(_)); let path = append_path(&self.current_module, ident); let cached_path = path_to_string(&path); - let mut items = FfiItems::new(); - items.current_module = path.clone(); - visit::visit_item_mod(&mut items, i); - items.uses = uses; self.modules.push(Module { - public, + public: matches!(vis, Visibility::Public(_)), cached_path, path, - items, + items: running_items, }); } } @@ -960,9 +964,7 @@ fn tmp() { let source = r#" use test2::test3; -use test1::*; - -use test1::test2::test3::*; +use test1::{*, test2::test3::*}; mod test1 { use test2::{*, test3::*}; @@ -970,6 +972,10 @@ mod test1 { mod test2 { use test3::*; + extern "C" { + fn time(); + } + mod test3 { pub struct Foo; From 68cb6e17f754d6063d175f90d6fd8f28a7fa527f Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:28:15 +0200 Subject: [PATCH 049/100] chore: wip --- ctest/src/ffi_items.rs | 10 +- notes/pr-write-up.md | 241 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 notes/pr-write-up.md diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 19cd55095b521..c442ae9a4bf60 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -897,10 +897,8 @@ impl<'ast> Visit<'ast> for FfiItems { let mut running_items = if is_crate_root { self.clone() } else { - let path = append_path(&self.current_module, ident); - let cached_path = path_to_string(&path); let out = FfiItems { - current_module: path.clone(), + current_module: append_path(&self.current_module, ident), ..Default::default() }; out @@ -947,12 +945,10 @@ impl<'ast> Visit<'ast> for FfiItems { if is_crate_root { *self = running_items; } else { - let path = append_path(&self.current_module, ident); - let cached_path = path_to_string(&path); self.modules.push(Module { public: matches!(vis, Visibility::Public(_)), - cached_path, - path, + cached_path: path_to_string(&running_items.current_module), + path: running_items.current_module.clone(), items: running_items, }); } diff --git a/notes/pr-write-up.md b/notes/pr-write-up.md new file mode 100644 index 0000000000000..a2ca39a834412 --- /dev/null +++ b/notes/pr-write-up.md @@ -0,0 +1,241 @@ +## Quick update + +This is an update on the last few weeks' worth of work. Time was spent +on a module resolution algorithm. This is necessary for correct item +paths. An example of this need is libc itself. + +The current module tree shape has reexports at the crate root. This +means items should be "referrable" by that path (i.e. their identifiers +alone.) But the current strategy (in this patch) gets their source path. + +This does not apply to upstream ctest. ctest currently assumes items are +reexported at the crate root. It only picks up on the item identifiers +during parsing. But module support needs import resolution. + +Following a summary of related changes in this patchset is included. +This does not currently include all changes in this PR. + +### Changes to FfiItems parsing + +The FfiItems type needed to change its use of syn's visitor hooks. These +are not completely useful with the addition of modules. This also +uncovered what is believed to be a bug in upstream ctest. + +The main concern here are non-module scope items. The syn visitor hooks +will visit every single matching item. This means items that only exist +within a function are also parsed. + +Such an item can not be used outside of the function. Other non-module +scopes also have this limitation (e.g. module-level const blocks.) +Upstream ctest will thus attempt to generate tsets for these as well. + +One could argue downstream users can set up skips for these. But that is +only a workaround. This also applies to use-statement scanning. A use +statement can exist within a function. But those should not be resolved. + +Bottom line: The syn visitor hooks had to be removed. Though it would be +more accurate to say that they were repurposed. Those trait methods now +live as free functions. + +The solution went through the module hooks. These are the only ones we +can safely use. A module may not be declared within a function scope. +And ctest requires only the items that appear within modules. + +The current approach wraps the crate root in a module. This is because +the crate root is not parsed as a module. So the hook for modules is not +run in those cases. Then the actual parsing is done in the hook. + +Each of the items ctest needs are "manually" parsed. This calls into the +previous hooks. These were repurposed into free functions. This is where +they are now used. This deserves further explanation. + +The crate root is wrapped in a virtual module. This is to trigger the +module visitor hook. The virtual module needs to have a special +identifier. This is detected during parsing. + +The crate root gets all parsed items added to it. Any other module gets +a new child module with all parsed items added to it. This builds up the +module tree hierarchy as one would expect. + +But all of this needs to exist within a wrapper. Filling in an FfiItems +is no more about calling into syn's file hook. It needs to first wrap +the items in the file in a module of their own. + +This is what eventually gets parsed. The item container is then +unwrapped from the virtual module. Note the module is a syn module type. +This needs another transformation for the resolution algorithm. + +There is one last thing to note about parsing. Use-statements may come +as groups (paths wrapped in curly brackets.) This is not convenient to +handle afterward. These get "normalized" during parsing. + +Normalization does one thing alone. It munches through a use-statement. +This may eventually stumble upon a group. That gets flattened into a set +of individual use-statements. A single use-statement then yields a list. + +One note about terminology must be made here. Use-statements may have +arbitrary paths as their associated objects. They may also have a single +name. These are all referred to from here on as paths. + +### The resolution algorithm + +The algorithm for resolution will now be explained. This follows a +fairly straightforward process. There is a single entrypoint routine. It +takes a module and yields another module. + +This is meant to be called from the afore mentioned wrapper. It is +called with another virtual module for the root. Recall the crate root +is initially only an item container. + +The new virtual module uses ctest's own Module type. This is used to +keep track of the path to the module. This will be needed during +use-statement resolution. + +Resolution starts by bottomming out on the list of child modules. This +is to ensure it takes place in a depth-first manner. Then it starts an +adaptive pass system over a single module. + +The current approach requires resolution to be depth-first. This is +because of the current limitations the algorithm has. The current +resolution strategy can only resolve "forward" use-statements. + +A "forward" use-statement is defined as a use-statement that brings into +scope items from a descendant module. Its dual brings into scope items +from some ascendant module. + +These observations are made with respect to the module in which the +use-statement appears. + +Note this does not exclude absolute use statements. A use-statement is +determined as one of the above based off of the target item. If the +target item is a descendant then that is a "forward" use-statement. + +Current work is focused on support for bidirectional imports. The rest +of the explanation will focus on the initial revision of the algorithm. +This supports only use-statements that refer to descendant items. + +This justifies the need for a depth-first approach. A leaf module in the +crate's module tree will have no resolvable use-statements. This is also +a good time to define "resolvable" and "unresolvable" use-statements. + +A "resolvable" use-statement is said to be resolvable in some finite +number of resolution passes. Its dual is impossible to resolve in any +number of resolution passes. + +One example of the latter are use-statements from third-party crates. +These will never resolve. The algorithm could attempt to inject the +crates into the module tree. But a simpler solution was found. + +That shall be explained soon. + +The pass system explanation will now be resumed. A single resolution +pass is given by two steps. The first one is the production of a list of +resolution items. The second one merges these into the module at hand. + +The former resolves all use-statements within a given module. This +yields a list of resolution items. Each resolution item corresponds with +one use-statement. It can be "resolved" or "unresolved." + +A use-statement is said to be "resolved" if the path it denotes has +yield a concrete item. Its dual refers to an item path that was not +found with respect to the module where the use-statement appears at. + +Consider the following example. + +use foo::*; + +use bar::*; + +use std::any::*; + +mod bar { + mod foo { + pub Foo; + } +} + +Resolution of the first use-statement depends on the second. The first +use-statement will yiled an unresolved item in the first pass. Only upon +the second use-statement resolving will the former also resolve. + +Though note both statements in the above example are said to be +resolvable. A definition for this is given further above. The third use +statement is said to be unresolvable. + +A single pass maps the list of use-statements. It uses the use-statement +resolution routine as map transform. This routine munches through a +given use-statement's path. + +Each path of the segment consists of an identifier. This routine carries +with it a module. This serves as state. This state is initially seeded +by the module where the use-statement appears at. + +Each identifier in the path gets looked up in this stateful module. A +match means the routine may continue munching. A mismatch means the +routine must yield an unresolved item. + +A munch that reaches the basecase denotes a resolved item. This is +produced much like an unresolved item. This bubbles up to the single +module resolution routine. That one mapped the list of use-statements. + +This eventually yields a potentially non-empty list of resolution items. +These now need to be merged into the top-level module. This is the +module where the use-statements that sourced them lived at. + +This now enters the second stage of a given resolution pass. Merging +iterates through the list of resolution items. Resolved items pack the +item the use-statement referred to. This deserves further explanation. + +A use-statement can refer to one of a non-glob or a glob. Non-globs may +be further specified as non-modules or modules. A glob packs in the +resolved item an item container with all items from the parent module. + +A non-glob packs in the resolved item an item container with itself only +as the contents. Notice how a non-module is small. A module carries with +it the subtree rooted at it in the crate's module tree. + +A resolved glob has a very similar topology to a module. It also packs +the subtree rooted at it in the crate's module tree. But it contains no +information on the "subroot" where that subtree was sourced from. + +Now back to the merging routine. Each resolved item also carries with it +the source use-statement. This is looked up in the module into which to +merge. Then this is removed from the module into which to merge. + +The last step is to extend the module into which to merge. This is the +actual "merging." The packed item container is deconstructed into its +component items. These then extend the lists of items in the target. + +That finishes a single resolution pass. The next thing it does is go +back to the top-level resolution routine. That now determines whether +another pass is in order. + +This is determined by diffing the number of use-statements. The operands +are the pre-pass module and the post-pass module. Passes over a single +module terminate if this comparison yields no differences. + +This is also what allows unresolvable use-statements not to break the +algorithm. These will not change once resolvable use-statements are +fully resolved. That is the sign that the pass system catches on. + +This is then repeated depth-first across all modules. The root module is +processed last. This one has at that point the entire subtree resolved. +It may need more than one pass. But it will surely find the items. + +### Extra notes + +A few details were glossed over the above explanation. These are not +essential to understanding the algorithm. Chief among them is the path +manipulation routine and the rename case of use-statement resolution. + +The latter will be explained first. A use-statement in Rust can have a +trailing renamed identifier. This identifier needs to also be reflected +in ctest's view of the crate's module tree. + +An item's path is often straightforward to rename. Modification of the +last segment of a path is all it takes to rename a non-module. But a +module is also a subtree for other items and modules. + + + +A module thus needs to recursively rename all items within the tree. From 35051cca15d758377eff5df81046ec023ecdb8c6 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:09:30 +0200 Subject: [PATCH 050/100] chore: wip --- ctest/src/ffi_items.rs | 8 +- notes/pr-answer.md | 30 ++++ notes/pr-revised.md | 382 +++++++++++++++++++++++++++++++++++++++++ notes/pr-write-up.md | 234 +++++++++++++++---------- 4 files changed, 561 insertions(+), 93 deletions(-) create mode 100644 notes/pr-answer.md create mode 100644 notes/pr-revised.md diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index c442ae9a4bf60..9cc0382b1f23e 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -438,10 +438,6 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { - let new_use = RefinedUse { - is_public: r#use.is_public, - tree: *tree.clone(), - }; let new_state = match state.items.search(ident.clone()) { Some(GenericItem::Module(m)) => m, None => return Resolution::Unresolved, @@ -453,6 +449,10 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re strictly being nested modules" ), }; + let new_use = RefinedUse { + is_public: r#use.is_public, + tree: *tree.clone(), + }; resolve_use(original_use, new_use, new_state) } } diff --git a/notes/pr-answer.md b/notes/pr-answer.md new file mode 100644 index 0000000000000..033bb67d007a0 --- /dev/null +++ b/notes/pr-answer.md @@ -0,0 +1,30 @@ +>This PR broke by removing PowerPC +>support, which has been in `libc` since 2020 when it was added and +>documented as being used for the Wii in +> +> +>Why was that done in this PR and how was it concluded that PowerPC +>wasn't in use? + +The reason is in the patch message. But "Rust target" may have been too +broad a term. Sorry for that. The change meant no in-tree Rust target in +upstream `rust-lang/rust`. + +The PR merged after the new usage guidelines merged. These indicate in +[^1] the current support policy. That mentions `rust-lang/libc` support +depends on Rust support in `rust-lang/rust`. + +libc takes the liberty to remove other kinds of targets. This change was +accepted in the PR. Whether libc also reserves the right to maintain +support is unknown. Though @tgross35 or @JohnTitor may think otherwise. + +There's also another argument to be made for out-of-tree targets. The +new usage guidelines point out in [^2] the API breakage for in-tree +tiered targets. Almost anything goes for tier 3. + +There's tier 3 targets where `core` fails to build. It would seem like +truly _anything_ goes for out-of-tree targets. Full breakage seems like +a feasible expectation. + +[^1]: +[^2]: diff --git a/notes/pr-revised.md b/notes/pr-revised.md new file mode 100644 index 0000000000000..75991311bbaad --- /dev/null +++ b/notes/pr-revised.md @@ -0,0 +1,382 @@ +## Description + +Add support for (currently) parsing and filtering Rust modules found in +the target crate to test. This is supposed to provide scaffolding for +multiple open feature requests that depend on ctest having support for +modules. + +I decided to open this before finishing up work on generating proper +tests on both C and Rust sides, because I am not sure if I should just +let the user deal with including a cohesive set of modules that do not +cause item resolution conflicts, or if I should instead generate +separate tests for each parsed module. + +**Edit**: the above paragraph really goes to say that I am not sure +whether I should generate multiple test templates while recursing +through the modules in `TestGenerator::generate_files`, or if I should +instead recurse through them in `TestTemplate::new` (with the passed +`FfiItems`) and return a collection of test templates for each parsed +module that has not been skipped. + +**Edit 2**: I decided to go for generating multiple test templates for +each parsed module. + +**Edit 3**: I completely forgot about `use` statements, so most of this +is currently useless. + +**Edit 4**: `use` statements have now an initial solution. This still +has limitations. Relevant changes are documented in Section Update 3. A +better algorithm is in the works. + +cc @tgross35 @mbyx + +### TODO + +- [ ] Look into parsing `use` statements, as those are the one things + missing from getting modules to work. + +- [ ] Update the docs to the public API of `TestGenerator` to mention + that the name remapping ought return the C identifiers without + worrying about potential item resolution conflicts. + +- [x] Look through the `populate_roundtrip_tests` function, as that one + I skipped to go first to `populate_field_ptr_tests` because of the + order these tests appear in the test template files. + +- [ ] Look into properly resolving paths to the types of type aliases, + constants and statics. These are handled poorly now. An example is + the check for arrays that is made in + `template::TestTemplate::populate_roundtrip_tests`, which does not + consider whether the type of the alias is itself an alias to be + resolved recursively (and potentially be an array.) + +### Update 1 + +I have changed the way we parse item identifiers, and more specifically, +their absolute paths without `crate` at the start (e.g. +`foo::bar::ctime()` instead of `crate::foo::bar::ctime()`.) + +This should make filtering continue working as-is right now for items, +but allow filtering on items in nested submodules by using an +appropriate string matching something like the above example. + +### Update 2 + +I think the initial set of changes is done. I have gone down the route +of potentially generating multiple templates for each parsed module, +such that more than a single test is generated. I have not yet +implemented the multi-template generation stuff, but that should be +fairly straightforward. What I believe to be done is the logic for +generating test to the right Rust paths while also keeping the single +segment paths on the C side of things. + +I am currently working through the tests, to see what is missing and +broken from the prior `ctest` interface. At this point, I am only trying +to work through the prior tests to see if they continue working and +generating the same set of tests (barring nested modules.) + +### Update 3 + +This is an update on the last few weeks' worth of work. Time was spent +on a module resolution algorithm. This is necessary for correct item +paths. An example of this need is `libc` itself. + +The current module tree shape has reexports at the crate root. This +means items should be "referrable" by that path (i.e. their identifiers +alone.) But the current strategy (in this patch) gets their source path. + +This does not apply to upstream `ctest`. `ctest` currently assumes items +are reexported at the crate root. It only picks up on item identifiers +during parsing. But module support needs import resolution. + +A summary of related changes in this patchset follows. This does not +currently include all changes in this PR. + +#### Changes to `FfiItems` parsing + +The `FfiItems` type needed to change its use of `syn`'s visitor hooks. +These are not completely useful with the addition of modules. This also +uncovered what is believed to be a bug in upstream `ctest`. + +The main concern here are non-module scope items. The `syn` visitor +hooks will visit every single matching item. This means items that only +exist within a function are also parsed. + +Such an item can not be used outside of the function. Other non-module +scopes also have this limitation (e.g. module-level `const` blocks.) +Upstream `ctest` will thus attempt to generate tsets for these as well. + +One could argue downstream users can set up skips for these. But that is +only a workaround. This also applies to `use`-statement scanning. A +`use` statement can exist within a function. But those should not be +resolved. + +Bottom line: The `syn` visitor hooks had to be removed. Though it would +be more accurate to say that they were repurposed. Those trait methods +now live as free functions. + +The solution went through use of the module hooks. These are the only +ones we can safely use. A module may not be declared within a function +scope. And `ctest` requires only the items that appear within modules. + +The current approach wraps the crate root in a module. This is because +the crate root is not parsed as a module. So the hook for modules is not +run in those cases. Then the actual parsing is done in the hook. + +Each of the items `ctest` needs are "manually" parsed. This calls into +the previous hooks. These are now free functions. This is where they are +now used. This deserves further explanation. + +The crate root is wrapped in a virtual module. This is to trigger the +module visitor hook. The virtual module needs to have a special +identifier. This is detected during parsing. + +The crate root gets all parsed items added to it. Any other module gets +a new child module with all parsed items added to it. This builds up the +module tree hierarchy as one would expect. + +But all of this needs to exist within a wrapper. Filling in an +`FfiItems` is no more about calling into `syn`'s file hook. It needs to +first wrap the items in the file in a module of their own. + +This is what eventually gets parsed. The item container is then +unwrapped from the virtual module. Note the module is a `syn` module +type. This needs another transformation for the resolution algorithm. + +There is one last thing to note about parsing. `use` statements may come +as groups (paths wrapped in curly brackets.) This is not convenient to +handle afterwards. These get "normalized" during parsing. + +Normalization does one thing alone. It munches through a `use` +statement. This may eventually stumble upon a group. That gets flattened +into a set of individual `use` statements. A single `use` statement then +yields a list. + +One note about terminology must be made here. `use` statements may have +arbitrary paths as their associated objects. They may also have a single +name. These are all referred to from here on as paths. + +#### The resolution algorithm + +The algorithm for resolution will now be explained. This follows a +fairly straightforward process. There is a single entrypoint routine. It +takes a module and yields another module. + +This is meant to be called from the afore mentioned wrapper. It is +called with another virtual module for the root. Recall the crate root +is initially only an item container. + +The new virtual module uses `ctest`'s own `Module` type. This is used to +keep track of the path to the module. This will be needed during +`use`-statement resolution. + +Resolution starts by bottomming out on the list of child modules. This +is to ensure it takes place in a depth-first manner. Then an adaptive +pass system is triggered over a single module. + +The current approach requires resolution to be depth-first. This is +because of the current limitations the algorithm has. The current +resolution strategy can only resolve "forward" `use` statements. + +A "forward" `use` statement is defined as a one that brings into scope +items from a descendant module. Its dual brings into scope items from +some ascendant module. + +These observations are made with respect to the module in which the +`use` statement appears. + +Note this does not exclude absolute `use` statements. A `use` statement +is determined as one of the above based off of the target item. If the +target item is a descendant then that is a "forward" `use` statement. + +Current work is focused on support for bidirectional imports. The rest +of the explanation will focus on the initial revision of the algorithm. +This supports only `use` statements that refer to descendant items. + +This justifies the need for a depth-first approach. A leaf module in the +crate's module tree will have no resolvable `use` statements. This is +also a good time to define "resolvable" and "unresolvable" `use` +statements. + +A "resolvable" `use` statement is said to be resolvable in some finite +number of resolution passes. Its dual is impossible to resolve in any +number of resolution passes. + +One example of the latter are `use` statements from third-party crates. +These will never resolve. The algorithm could attempt to inject the +crates into the module tree. But a simpler solution was found. + +That shall be explained later. + +The pass system explanation will now be resumed. A single resolution +pass is given by two steps. The first one is the production of a list of +resolution items. The second one merges these into the module at hand. + +The former resolves all `use` statements within a given module. This +yields a list of resolution items. Each resolution item corresponds with +one `use` statement. It can be "resolved" or "unresolved." + +A `use` statement is said to be "resolved" if the path it denotes has +yield a concrete item. Its dual refers to an item path that was not +found with respect to the module where the `use` statement appears at. + +Consider the following example. + +```rust +use foo::*; + +use bar::*; + +use std::any::*; + +mod bar { + mod foo { + pub struct Foo; + } +} +``` + +Resolution of the first `use` statement depends on the second. The first +`use` statement will yiled an unresolved item in the first pass. Only +upon the second `use` statement resolving will the former also resolve. + +Though note both statements in the above example are said to be +resolvable. A definition for this is given further above. The third +`use` statement is said to be unresolvable. + +A single pass maps the list of `use` statements. It uses the +`use`-statement resolution routine as map transform. This routine +munches through a given `use` statement's path. + +Each path of the segment consists of an identifier. This routine carries +with it a module. This serves as state. This state is initially seeded +by the module where the `use` statement appears at. + +Each identifier in the path gets looked up in this stateful module. A +match means the routine may continue munching. A mismatch means the +routine must yield an unresolved item. + +A munch that reaches the basecase denotes a resolved item. This is +produced much like an unresolved item. This bubbles up to the single +module resolution routine. That one mapped the list of `use` statements. + +This eventually yields a potentially non-empty list of resolution items. +These now need to be merged into the top-level module. This is the +module where the `use` statements that sourced the resolutions lived at. + +This now enters the second stage of a given resolution pass. Merging +iterates through the list of resolution items. Resolved items pack the +item the `use` statement referred to. This deserves further explanation. + +A `use` statement can refer to one of a non-glob or a glob. Non-globs +may be further specified as non-modules or modules. A glob packs in the +resolved item an item container with all items from the parent module. + +A non-glob packs in the resolved item an item container with itself only +as the contents. Notice how a non-module is small. A module carries with +it the subtree rooted at it in the crate's module tree. + +A resolved glob has a very similar topology to a module. It also packs +the subtree rooted at it in the crate's module tree. But it contains no +information on the "subroot" where that subtree was sourced from. + +Now back to the merging routine. Each resolved item also carries with it +the source `use` statement. This is looked up in the module into which +to merge. This is then is removed from the module into which to merge. + +The last step is to extend the module into which to merge. This is the +actual "merging." The packed item container is deconstructed into its +component items. These then extend the lists of items in the target. + +That finishes a single resolution pass. The next thing it does is go +back to the top-level resolution routine. That now determines whether +another pass is in order. + +This is determined by diffing the number of `use` statements. The +operands are the pre-pass module and the post-pass module. Passes over a +single module terminate if this comparison yields no differences. + +This is also what allows unresolvable `use`-statements not to break the +algorithm. These will not change once resolvable `use`-statements are +fully resolved. That is the sign that the pass system catches on. + +This is then repeated depth-first across all modules. The root module is +processed last. This one has at that point the entire subtree resolved. +It may need more than one pass. But it will resolve resovable imports. + +#### Extra notes + +A few details were glossed over the above explanation. These are not +essential to understanding the algorithm. Chief among them is the path +manipulation routine and the rename case of `use`-statement resolution. + +The latter will be explained first. A `use` statement in Rust can have a +trailing renamed identifier. This identifier needs to also be reflected +in `ctest`'s view of the crate's module tree. + +An item's path is often straightforward to rename. Modification of the +last segment of a path is all it takes to rename a non-module. But a +module is also a subtree for other items and modules. + +A module thus needs to recursively rename all items within the tree. +That is the purpose of the path manipulation routine. It modifies a +given module subtree with an externally-sourced closure. + +Use of this routine in the rename case is straightforward. The case of +modules first modifies the root's path. Then each item builds its path +anew. The base path becomes the root's new path. Other segments remain. + +The path manipulation routine is also used in the merge routine. Here +the logic is slightly more intricate. Path manipulation takes the module +in which a tree is rooted at. But a resolution item packs a container. + +Recall item containers differ from modules. Items containers exist +within modules. So path manipulation during merging requires a virtual +module. This itself requires special dummy values. + +A dummy path segment for the virtual module must be non-empty. This is +because the passed closure assumes so. This holds for all descendant +items. But there is another subtlety to this path manipulation call. + +The passed subtree contains not the modules. These need further +consideration. A module to be merged should not have the entire path of +descendant items modified. + +Only segments prior to the root's identifier should be removed. This +collides with the above path manipulation call. So modules are handled +separately. They do follow the same logic as the rename case above. + +One more thing happens in a pass. A merged module's items are checked +for duplicates. This can happen from overlapping `use`-statements. The +type for an item container implements that operation. + +Consider this example. + +```rust +use foo::*; + +use bar::*; + +mod foo { + pub struct Foo; + + mod bar { + pub struct Foo; + } +} +``` + +The crate root imports would bring two identical `Foo`s into scope. +Cases like these are in need of deduplication. + +Performance has not been taken into consideration. Certain data +structures could be improved. One example would be the use of +associative arrays for items. This would avoid deduplication. + +## Checklist + +- [ ] Relevant tests in `ctest/tests` and `ctest/src/tests.rs` have been + updated +- [ ] Tested locally (`LIBC_BLESS=1 cargo test -p ctest`); + +@rustbot label +stable-nominated diff --git a/notes/pr-write-up.md b/notes/pr-write-up.md index a2ca39a834412..23b0e170e71a7 100644 --- a/notes/pr-write-up.md +++ b/notes/pr-write-up.md @@ -1,53 +1,54 @@ -## Quick update +### Update 3 This is an update on the last few weeks' worth of work. Time was spent on a module resolution algorithm. This is necessary for correct item -paths. An example of this need is libc itself. +paths. An example of this need is `libc` itself. The current module tree shape has reexports at the crate root. This means items should be "referrable" by that path (i.e. their identifiers alone.) But the current strategy (in this patch) gets their source path. -This does not apply to upstream ctest. ctest currently assumes items are -reexported at the crate root. It only picks up on the item identifiers +This does not apply to upstream `ctest`. `ctest` currently assumes items +are reexported at the crate root. It only picks up on item identifiers during parsing. But module support needs import resolution. -Following a summary of related changes in this patchset is included. -This does not currently include all changes in this PR. +A summary of related changes in this patchset follows. This does not +currently include all changes in this PR. -### Changes to FfiItems parsing +#### Changes to `FfiItems` parsing -The FfiItems type needed to change its use of syn's visitor hooks. These -are not completely useful with the addition of modules. This also -uncovered what is believed to be a bug in upstream ctest. +The `FfiItems` type needed to change its use of `syn`'s visitor hooks. +These are not completely useful with the addition of modules. This also +uncovered what is believed to be a bug in upstream `ctest`. -The main concern here are non-module scope items. The syn visitor hooks -will visit every single matching item. This means items that only exist -within a function are also parsed. +The main concern here are non-module scope items. The `syn` visitor +hooks will visit every single matching item. This means items that only +exist within a function are also parsed. Such an item can not be used outside of the function. Other non-module -scopes also have this limitation (e.g. module-level const blocks.) -Upstream ctest will thus attempt to generate tsets for these as well. +scopes also have this limitation (e.g. module-level `const` blocks.) +Upstream `ctest` will thus attempt to generate tsets for these as well. One could argue downstream users can set up skips for these. But that is -only a workaround. This also applies to use-statement scanning. A use -statement can exist within a function. But those should not be resolved. +only a workaround. This also applies to `use`-statement scanning. A +`use` statement can exist within a function. But those should not be +resolved. -Bottom line: The syn visitor hooks had to be removed. Though it would be -more accurate to say that they were repurposed. Those trait methods now -live as free functions. +Bottom line: The `syn` visitor hooks had to be removed. Though it would +be more accurate to say that they were repurposed. Those trait methods +now live as free functions. -The solution went through the module hooks. These are the only ones we -can safely use. A module may not be declared within a function scope. -And ctest requires only the items that appear within modules. +The solution went through use of the module hooks. These are the only +ones we can safely use. A module may not be declared within a function +scope. And `ctest` requires only the items that appear within modules. The current approach wraps the crate root in a module. This is because the crate root is not parsed as a module. So the hook for modules is not run in those cases. Then the actual parsing is done in the hook. -Each of the items ctest needs are "manually" parsed. This calls into the -previous hooks. These were repurposed into free functions. This is where -they are now used. This deserves further explanation. +Each of the items `ctest` needs are "manually" parsed. This calls into +the previous hooks. These are now free functions. This is where they are +now used. This deserves further explanation. The crate root is wrapped in a virtual module. This is to trigger the module visitor hook. The virtual module needs to have a special @@ -57,27 +58,28 @@ The crate root gets all parsed items added to it. Any other module gets a new child module with all parsed items added to it. This builds up the module tree hierarchy as one would expect. -But all of this needs to exist within a wrapper. Filling in an FfiItems -is no more about calling into syn's file hook. It needs to first wrap -the items in the file in a module of their own. +But all of this needs to exist within a wrapper. Filling in an +`FfiItems` is no more about calling into `syn`'s file hook. It needs to +first wrap the items in the file in a module of their own. This is what eventually gets parsed. The item container is then -unwrapped from the virtual module. Note the module is a syn module type. -This needs another transformation for the resolution algorithm. +unwrapped from the virtual module. Note the module is a `syn` module +type. This needs another transformation for the resolution algorithm. -There is one last thing to note about parsing. Use-statements may come +There is one last thing to note about parsing. `use` statements may come as groups (paths wrapped in curly brackets.) This is not convenient to -handle afterward. These get "normalized" during parsing. +handle afterwards. These get "normalized" during parsing. -Normalization does one thing alone. It munches through a use-statement. -This may eventually stumble upon a group. That gets flattened into a set -of individual use-statements. A single use-statement then yields a list. +Normalization does one thing alone. It munches through a `use` +statement. This may eventually stumble upon a group. That gets flattened +into a set of individual `use` statements. A single `use` statement then +yields a list. -One note about terminology must be made here. Use-statements may have +One note about terminology must be made here. `use` statements may have arbitrary paths as their associated objects. They may also have a single name. These are all referred to from here on as paths. -### The resolution algorithm +#### The resolution algorithm The algorithm for resolution will now be explained. This follows a fairly straightforward process. There is a single entrypoint routine. It @@ -87,61 +89,63 @@ This is meant to be called from the afore mentioned wrapper. It is called with another virtual module for the root. Recall the crate root is initially only an item container. -The new virtual module uses ctest's own Module type. This is used to +The new virtual module uses `ctest`'s own `Module` type. This is used to keep track of the path to the module. This will be needed during -use-statement resolution. +`use`-statement resolution. Resolution starts by bottomming out on the list of child modules. This -is to ensure it takes place in a depth-first manner. Then it starts an -adaptive pass system over a single module. +is to ensure it takes place in a depth-first manner. Then an adaptive +pass system is triggered over a single module. The current approach requires resolution to be depth-first. This is because of the current limitations the algorithm has. The current -resolution strategy can only resolve "forward" use-statements. +resolution strategy can only resolve "forward" `use` statements. -A "forward" use-statement is defined as a use-statement that brings into -scope items from a descendant module. Its dual brings into scope items -from some ascendant module. +A "forward" `use` statement is defined as a one that brings into scope +items from a descendant module. Its dual brings into scope items from +some ascendant module. These observations are made with respect to the module in which the -use-statement appears. +`use` statement appears. -Note this does not exclude absolute use statements. A use-statement is -determined as one of the above based off of the target item. If the -target item is a descendant then that is a "forward" use-statement. +Note this does not exclude absolute `use` statements. A `use` statement +is determined as one of the above based off of the target item. If the +target item is a descendant then that is a "forward" `use` statement. Current work is focused on support for bidirectional imports. The rest of the explanation will focus on the initial revision of the algorithm. -This supports only use-statements that refer to descendant items. +This supports only `use` statements that refer to descendant items. This justifies the need for a depth-first approach. A leaf module in the -crate's module tree will have no resolvable use-statements. This is also -a good time to define "resolvable" and "unresolvable" use-statements. +crate's module tree will have no resolvable `use` statements. This is +also a good time to define "resolvable" and "unresolvable" `use` +statements. -A "resolvable" use-statement is said to be resolvable in some finite +A "resolvable" `use` statement is said to be resolvable in some finite number of resolution passes. Its dual is impossible to resolve in any number of resolution passes. -One example of the latter are use-statements from third-party crates. +One example of the latter are `use` statements from third-party crates. These will never resolve. The algorithm could attempt to inject the crates into the module tree. But a simpler solution was found. -That shall be explained soon. +That shall be explained later. The pass system explanation will now be resumed. A single resolution pass is given by two steps. The first one is the production of a list of resolution items. The second one merges these into the module at hand. -The former resolves all use-statements within a given module. This +The former resolves all `use` statements within a given module. This yields a list of resolution items. Each resolution item corresponds with -one use-statement. It can be "resolved" or "unresolved." +one `use` statement. It can be "resolved" or "unresolved." -A use-statement is said to be "resolved" if the path it denotes has +A `use` statement is said to be "resolved" if the path it denotes has yield a concrete item. Its dual refers to an item path that was not -found with respect to the module where the use-statement appears at. +found with respect to the module where the `use` statement appears at. Consider the following example. +```rust use foo::*; use bar::*; @@ -150,25 +154,26 @@ use std::any::*; mod bar { mod foo { - pub Foo; + pub struct Foo; } } +``` -Resolution of the first use-statement depends on the second. The first -use-statement will yiled an unresolved item in the first pass. Only upon -the second use-statement resolving will the former also resolve. +Resolution of the first `use` statement depends on the second. The first +`use` statement will yiled an unresolved item in the first pass. Only +upon the second `use` statement resolving will the former also resolve. Though note both statements in the above example are said to be -resolvable. A definition for this is given further above. The third use -statement is said to be unresolvable. +resolvable. A definition for this is given further above. The third +`use` statement is said to be unresolvable. -A single pass maps the list of use-statements. It uses the use-statement -resolution routine as map transform. This routine munches through a -given use-statement's path. +A single pass maps the list of `use` statements. It uses the +`use`-statement resolution routine as map transform. This routine +munches through a given `use` statement's path. Each path of the segment consists of an identifier. This routine carries with it a module. This serves as state. This state is initially seeded -by the module where the use-statement appears at. +by the module where the `use` statement appears at. Each identifier in the path gets looked up in this stateful module. A match means the routine may continue munching. A mismatch means the @@ -176,18 +181,18 @@ routine must yield an unresolved item. A munch that reaches the basecase denotes a resolved item. This is produced much like an unresolved item. This bubbles up to the single -module resolution routine. That one mapped the list of use-statements. +module resolution routine. That one mapped the list of `use` statements. This eventually yields a potentially non-empty list of resolution items. These now need to be merged into the top-level module. This is the -module where the use-statements that sourced them lived at. +module where the `use` statements that sourced the resolutions lived at. This now enters the second stage of a given resolution pass. Merging iterates through the list of resolution items. Resolved items pack the -item the use-statement referred to. This deserves further explanation. +item the `use` statement referred to. This deserves further explanation. -A use-statement can refer to one of a non-glob or a glob. Non-globs may -be further specified as non-modules or modules. A glob packs in the +A `use` statement can refer to one of a non-glob or a glob. Non-globs +may be further specified as non-modules or modules. A glob packs in the resolved item an item container with all items from the parent module. A non-glob packs in the resolved item an item container with itself only @@ -199,8 +204,8 @@ the subtree rooted at it in the crate's module tree. But it contains no information on the "subroot" where that subtree was sourced from. Now back to the merging routine. Each resolved item also carries with it -the source use-statement. This is looked up in the module into which to -merge. Then this is removed from the module into which to merge. +the source `use` statement. This is looked up in the module into which +to merge. This is then is removed from the module into which to merge. The last step is to extend the module into which to merge. This is the actual "merging." The packed item container is deconstructed into its @@ -210,32 +215,83 @@ That finishes a single resolution pass. The next thing it does is go back to the top-level resolution routine. That now determines whether another pass is in order. -This is determined by diffing the number of use-statements. The operands -are the pre-pass module and the post-pass module. Passes over a single -module terminate if this comparison yields no differences. +This is determined by diffing the number of `use` statements. The +operands are the pre-pass module and the post-pass module. Passes over a +single module terminate if this comparison yields no differences. -This is also what allows unresolvable use-statements not to break the -algorithm. These will not change once resolvable use-statements are +This is also what allows unresolvable `use`-statements not to break the +algorithm. These will not change once resolvable `use`-statements are fully resolved. That is the sign that the pass system catches on. This is then repeated depth-first across all modules. The root module is processed last. This one has at that point the entire subtree resolved. -It may need more than one pass. But it will surely find the items. +It may need more than one pass. But it will resolve resovable imports. -### Extra notes +#### Extra notes A few details were glossed over the above explanation. These are not essential to understanding the algorithm. Chief among them is the path -manipulation routine and the rename case of use-statement resolution. +manipulation routine and the rename case of `use`-statement resolution. -The latter will be explained first. A use-statement in Rust can have a +The latter will be explained first. A `use` statement in Rust can have a trailing renamed identifier. This identifier needs to also be reflected -in ctest's view of the crate's module tree. +in `ctest`'s view of the crate's module tree. An item's path is often straightforward to rename. Modification of the last segment of a path is all it takes to rename a non-module. But a module is also a subtree for other items and modules. - - A module thus needs to recursively rename all items within the tree. +That is the purpose of the path manipulation routine. It modifies a +given module subtree with an externally-sourced closure. + +Use of this routine in the rename case is straightforward. The case of +modules first modifies the root's path. Then each item builds its path +anew. The base path becomes the root's new path. Other segments remain. + +The path manipulation routine is also used in the merge routine. Here +the logic is slightly more intricate. Path manipulation takes the module +in which a tree is rooted at. But a resolution item packs a container. + +Recall item containers differ from modules. Items containers exist +within modules. So path manipulation during merging requires a virtual +module. This itself requires special dummy values. + +A dummy path segment for the virtual module must be non-empty. This is +because the passed closure assumes so. This holds for all descendant +items. But there is another subtlety to this path manipulation call. + +The passed subtree contains not the modules. These need further +consideration. A module to be merged should not have the entire path of +descendant items modified. + +Only segments prior to the root's identifier should be removed. This +collides with the above path manipulation call. So modules are handled +separately. They do follow the same logic as the rename case above. + +One more thing happens in a pass. A merged module's items are checked +for duplicates. This can happen from overlapping `use`-statements. The +type for an item container implements that operation. + +Consider this example. + +```rust +use foo::*; + +use bar::*; + +mod foo { + pub struct Foo; + + mod bar { + pub struct Foo; + } +} +``` + +The crate root imports would bring two identical `Foo`s into scope. +Cases like these are in need of deduplication. + +Performance has not been taken into consideration. Certain data +structures could be improved. One example would be the use of +associative arrays for items. This would avoid deduplication. From cf06ec29e53dab767f17454d658ab143e8eaceab Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:23:10 +0200 Subject: [PATCH 051/100] chore: wip --- notes/new-algo.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 notes/new-algo.md diff --git a/notes/new-algo.md b/notes/new-algo.md new file mode 100644 index 0000000000000..ddd63b098c5d7 --- /dev/null +++ b/notes/new-algo.md @@ -0,0 +1,80 @@ +The new algorithm needs to support bidirectional use statements. Current +support is based around one invariant. This holds only for forward use +statements. The depth-first approach exploits the invariant. The problem +comes from the possibility for leaf modules to contain use statements. +These were previously also considered. But previously these were +guaranteed to be unresolvable import statements. These refer to use +statements that target items from third-party crates. But bidirectional +imports means there are also backward use statements. These may refer to +some item in an ancestor module. And these are resolvable use +statements. The one thing relying on this the most is the single-module +resolution proposition. It bottoms out on the list of child modules. It +resoves leaf modules first. Then it bubbles back up. Ancestor module +then get resolved with the guarantee of a fully resolved subtree. This +needs to change. But prior notes revealed order matters not. To start +resolution from the crate root does not improve things. The ideal +resolution process is fairly clear. One import gets resolved. If the +target gets some changes within it then the source of the import +observes those changes. This may seem like it calls for nifty pointer +tricks. But a pure solution is preferred. One must specify what it means +to "observe" the changes in the target. Currently target merging implies +use statement removal. That means the whole thing is settled once a +target is found. But it does not have to be this way. A target may be +merged without need for use statement removal. The pass system observes +the number of use statements before and after a pass. This should in +theory prevent potential infinite loops. But that only holds under one +condition. Single-module resolution currently never goes through the +same module again. To "observe" the changes in the use statement target +requires another pass over the use statement source module. And yet +preservation of the use-statement implies no changes to the list of use +statements. So the pass system concludes the remaining use statements +are unresolvable. It then moves on to another module. This implies the +overarching flow of resolution through the module tree has to be thought +anew. It is insufficient for a given number of passes over a module to +all happen consecutively. But keeping track of the tree nodes that +potentially require another pass is a hassle. It is best if the +top-level call to resolve the module tree from the crate root is +changed. It should loop over all modules in the tree instead of over a +single module. It should be made into a potentially non-terminating +loop. A check would be performed after one full module pass were +performed. This would be very similar to the one currently used in the +pass system. But it would instead recursively check the lists of use +statements of all modules in the tree. If they all compared the same +then it would be safe to say the algorithm may now terminate. The point +here would be to exploit deduplication. An abstract example may be in +order. Consider the algorithm is at the crate root. It finds a +dependency-free reexport. This means the reexport can be immediately +resolved. And it chooses to resolve the reexport. This means it will be +produced as a resolution item. This item will then be merged into the +module in which the use statement appeared at. But here's the difference +with the prior revision of the algorithm. The use statement is not +removed. It is instead kept as-is. The termination condition in the +single module resolution proposition is then removed. A single pass is +the only pass ever performed on a given module. It is within one global +module tree pass. The termination check is then moved to the top-level +resolution proposition. This is the one that originally called into the +former with the crate root. But now it has been made into an "infinite" +loop. The termination condition stops being the number of use statements +pre- and post-pass. It becomes a full comparison of the module tree +before and after the global resolution pass. This should reveal whether +some module has had its list of items modified. The trick here would be +to perform deduplication everytime a single-module pass is done. This +would ensure one can merge one use statement over and over again without +tricking this check into believing there have been changes. The details +of this are still fuzzy. Further specification is required. Consider the +prior case of the crate root again. The use statement is resolved. +Assume the crate root has only one child module. Assume as well this +child module is a leaf module. Suppose this child module contains a use +statement pointing to the crate root. The module resolves this use +statement by adding all items from the root module. That will merge anew +its own items. But deduplication will remove them during merging. The +module-wide pass system will then check for changes in the tree. It will +see there were changes. So it will trigger another pass. This will +repeat the same steps. But the use statement in the crate root will +already cause deduplication to remove both the child module and its own +items from the child module. The same thing will happen in the child +module. This seems trivial. But it may very well be the solution. More +thought has to be put into test cases. There are likely edge cases that +this does not consider. The current data structures are a potentially +subpar fit for the task. But benchmarking should reveal that once the +thing is implemented. From eeb55b13d72ceedc577daf436d6bd2d4da6b6a31 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:33:20 +0200 Subject: [PATCH 052/100] chore: wip --- ctest/src/ffi_items.rs | 31 ++++++++++++--- notes/new-algo.md | 90 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 8 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 9cc0382b1f23e..bbce3cd00e5ce 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -442,11 +442,10 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re Some(GenericItem::Module(m)) => m, None => return Resolution::Unresolved, Some(_) => unreachable!( - "paths we parse in ctest include only modules, and not \ - enum variants; update this in the future if we start \ - supporting enum variants and thus start having imports \ - that can span multiple segments of a path without \ - strictly being nested modules" + "Paths parsed in ctest do not include enum variants. \ + Update this if support for enum variants is added. \ + Multi-segment import paths may then refer to items other \ + than modules." ), }; let new_use = RefinedUse { @@ -458,6 +457,10 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } } +fn is_self(ident: syn::Ident) -> bool { + ident.to_string() == "self" +} + fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module) -> Module { let new_root_path = f(root.path.clone()); let new_root_cache = path_to_string(&new_root_path); @@ -984,8 +987,24 @@ mod test1 { } } "#; + let tmp = "use self as root;"; let mut items = FfiItems::default(); - let file = syn::parse_file(source).unwrap(); + let file = syn::parse_file(tmp).unwrap(); items.visit_file(&file); + let is_self = items + .uses + .first() + .cloned() + .into_iter() + .filter_map(|u| { + if let RefinedUseTree::Rename(syn::UseRename { ident, .. }) = u.tree.clone() { + Some(ident.to_string() == "self") + } else { + None + } + }) + .next() + .unwrap(); println!("{:#?}", items); + println!("self is \"self\": {:#?}", is_self); } diff --git a/notes/new-algo.md b/notes/new-algo.md index ddd63b098c5d7..56ee25768dc47 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -76,5 +76,91 @@ items from the child module. The same thing will happen in the child module. This seems trivial. But it may very well be the solution. More thought has to be put into test cases. There are likely edge cases that this does not consider. The current data structures are a potentially -subpar fit for the task. But benchmarking should reveal that once the -thing is implemented. +subpar fit for the task. That should be the task of benchmarks. A recap +of the plan is in order. The idea is to perform module-wide passes. +These happen at the top-level resolution proposition. The termination +check should also be moved there. The check should not base itself off +of a diff of use statements. The check is over the entire tree. This +implies need for a copy of the module tree. That would have happened +anyway. That is because of the pure nature of the computations. +Discussion about other areas is in order. The single-module resolution +proposition should change. It should now be simpler. The pass system +does not exist anymore there. A single resolution should be made. Though +this should continue to be followed up by use of the merge proposition. +The other areas of the current revision can be left as-is. Path +manipulation will come in very handy. The current merging strategy is +key to the new algorithm. That should likely be left unchanged. A more +complex test case must be thought up. The test cases should change. +They've all had the same shape thus far. Consider the case of an +absolute use statement. Suppose it is found in some arbitrarily deep +module. Then suppose every other module has a reexport to it. The first +pass would import the module over to all other modules. This pass would +also resolve the absolute use statement. Some modules would already +observe this. These would be the descendants to the former. A second +pass would then be performed. There have been changes in the first pass. +That is why a second pass would be in order. The second pass would go +through the same use statements anew. This is where deduplication comes +in. The same use statements would be resolved anew. Deduplication would +remove duplicate items. The import to the denoted module would be kept. +These are key. An observation can be made about this algorithm. The +amount of work per module-wide pass is fixed in some ways. The number of +use statements considered is always the same. Dependent use statements +will not be resolved. They will once their dependency exists no more. +That is the one way in which work varies across passes. This can be +modeled in terms of a set of sentences. These sentences await +satisfiability. Satisfiability of some sentence depends on +satisfiability of some other sentence. Dependent sentences are defined +as such. At least one sentence must be dependency-free. This triggers +resolution of other sentences. There is an exception here. Some +sentences are unresolvable. And yet they appear as dependent. These +correspond with use statements from third-party crates. The algorithm +relies not on these. Unresolvable sentences are eventually skipped. The +algorithm has no notion of unresolvable sentences. It only ever +determines dependency of sentences. It does not determine which are the +dependencies. A dependent use statement can not be immediately resolved. +Some name in its path does not currently exist. At one point it may +exist. It is unknown whether that point is reachable. The diff +comparison determines if it may be reachable. An observable change in +the tree indicates further resolution is in order. No observable change +indicates no further resolution is possible. The diff thus determines if +some dependent sentence is satisfiable. It does not determine which +sentence is satisfiable. The algorithm always goes through all +sentences. It attempts to satisfy each sentence in turn. Some may yield +observable change. Some may not. Termination follows when there is no +observable change at all. Discussion is needed of the new machinery this +algorithm needs. Resolution of bidirectional use statements needs access +to the entire module tree. The stateful module in the +reexport-resolution proposition is not enough. There is need for the +entire module tree. This should reflect the most up-to-date snapshot. +That can be found after each single-module resolution. This was already +explored in the latest revision of the Idris proof. The depth-first +approach in the single-module resolution proposition must be kept. There +is now need for this to carry state. The state should consist of the +module tree snapshot. The seed should be the same as the module to +resolve. This means the initial module is the crate root. So should the +stateful module tree be. Then the single-module resolution proposition +can use this alongside the single-reexport resolution proposition. The +latter will need to add another piece of state. The state is now +composed of the parent module and the entire module tree. Note the +module tree state need not change across each reexport resolution. It +changes once the merging proposition is triggered. That will need +another auxiliary proposition. It should observe the changes in a given +subtree. Then it should effect those in a new crate module tree. The +details of the single-reexport resolution proof need discussion. This +may now support path keywords. These include: crate, super and self. A +review of the Rust reference may be in order. These are the only +keywords that must be considered. The syn use tree does not include +special support for any of these. That likely means they are to be used +as identifiers. The syn documentation on identifiers mentions that. +Keywords will be parsed as identifiers. A manually-constructed +identifier may not be a keyword. The logic is then simplified. The +basecase for names needs modification. It should check whether the +identifier refers to a keyword. This also affects the rename case. The +check will need to consult the syn documentation. Maybe there is already +a utility proposition for keywords. There is none. The string conversion +utility will be used instead. This should yield reasonable results. The +case is of a single identifier. There should be no need for +sanitization. This is a good opportunity for implementation. A change to +the above cases will not affect the prior revision of the algorithm. The +self keyword requires no module tree state. It may be readily +implemented. From 7fa5dbde5e4c520c50bf37d68a17daed6edc4aef Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:24:52 +0200 Subject: [PATCH 053/100] chore: wip --- ctest/src/ffi_items.rs | 128 +++++++++++++++++++++-------------------- notes/new-algo.md | 43 +++++++++++++- 2 files changed, 107 insertions(+), 64 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index bbce3cd00e5ce..e2901fdc65dbb 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -331,6 +331,19 @@ fn resolve_one(src: Module) -> Vec { } fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Resolution { + macro_rules! self_case { + ($it:ident, $target:expr) => { + if is_self($it.clone()) { + return Resolution::Resolved { + original_use, + items: FfiItems { + modules: vec![$target], + ..Default::default() + }, + }; + } + }; + } match &r#use.tree { RefinedUseTree::Name(syn::UseName { ident }) => { macro_rules! single_item { @@ -345,6 +358,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } }}; } + self_case!(ident, state); match state.items.search(ident.clone()) { Some(GenericItem::Type(t)) => single_item!(aliases: t), Some(GenericItem::Struct(s)) => single_item!(structs: s), @@ -387,46 +401,46 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } }}; } - match state.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => { - let new_path = { - let mut base_path_segments = m.path.segments.clone(); - base_path_segments.pop(); - base_path_segments.push(syn::PathSegment { - ident: rename.clone(), - arguments: syn::PathArguments::None, - }); + let renamer = |m: Module| { + let new_path = { + let mut base_path_segments = m.path.segments.clone(); + base_path_segments.pop(); + base_path_segments.push(syn::PathSegment { + ident: rename.clone(), + arguments: syn::PathArguments::None, + }); + syn::Path { + segments: base_path_segments, + ..m.path + } + }; + let new_path_len = new_path.segments.len(); + manipulate_path( + |p| { + let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( + new_path.clone(), + |mut p, s| { + p.segments.push(s); + p + }, + ); syn::Path { - segments: base_path_segments, - ..m.path + leading_colon: p.leading_colon, + ..new_path } - }; - let new_path_len = new_path.segments.len(); - let new_module = manipulate_path( - |p| { - let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( - new_path.clone(), - |mut p, s| { - p.segments.push(s); - p - }, - ); - syn::Path { - leading_colon: p.leading_colon, - ..new_path - } - }, - m, - ); - let new_items = FfiItems { - modules: vec![new_module], + }, + m, + ) + }; + self_case!(ident, renamer(state)); + match state.items.search(ident.clone()) { + Some(GenericItem::Module(m)) => Resolution::Resolved { + original_use, + items: FfiItems { + modules: vec![renamer(m)], ..Default::default() - }; - Resolution::Resolved { - original_use, - items: new_items, - } - } + }, + }, Some(GenericItem::Type(t)) => single_item!(aliases, t: Type), Some(GenericItem::Struct(s)) => single_item!(structs, s: Struct), Some(GenericItem::Union(u)) => single_item!(unions, u: Union), @@ -438,15 +452,19 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { - let new_state = match state.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => m, - None => return Resolution::Unresolved, - Some(_) => unreachable!( - "Paths parsed in ctest do not include enum variants. \ - Update this if support for enum variants is added. \ - Multi-segment import paths may then refer to items other \ - than modules." - ), + let new_state = if is_self(ident.clone()) { + state + } else { + match state.items.search(ident.clone()) { + Some(GenericItem::Module(m)) => m, + None => return Resolution::Unresolved, + Some(_) => unreachable!( + "Paths parsed in ctest do not include enum variants. \ + Update this if support for enum variants is added. \ + Multi-segment import paths may then refer to items \ + other than modules." + ), + } }; let new_use = RefinedUse { is_public: r#use.is_public, @@ -987,24 +1005,8 @@ mod test1 { } } "#; - let tmp = "use self as root;"; let mut items = FfiItems::default(); - let file = syn::parse_file(tmp).unwrap(); + let file = syn::parse_file(source).unwrap(); items.visit_file(&file); - let is_self = items - .uses - .first() - .cloned() - .into_iter() - .filter_map(|u| { - if let RefinedUseTree::Rename(syn::UseRename { ident, .. }) = u.tree.clone() { - Some(ident.to_string() == "self") - } else { - None - } - }) - .next() - .unwrap(); println!("{:#?}", items); - println!("self is \"self\": {:#?}", is_self); } diff --git a/notes/new-algo.md b/notes/new-algo.md index 56ee25768dc47..6c2d4cd6ca1b4 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -163,4 +163,45 @@ case is of a single identifier. There should be no need for sanitization. This is a good opportunity for implementation. A change to the above cases will not affect the prior revision of the algorithm. The self keyword requires no module tree state. It may be readily -implemented. +implemented. The current proof takes into account solely a lookup into +the container. The revised version should check for keywords. The only +feasible keyword to implement at present is the self keyword. This +should come before the lookup in the item container. Recall the current +stateful module corresponds exactly to the self module. A resolution +follows immediately if the self module is brought over. The question +becomes the format of the item container packed in the resolution item. +Should it be a virutal container? Yes. self imports no immediate set of +items. It only brings over a module. The simplest next keyword to parse +is the crate keyword. That keyword is meant to be the source of an +absolute import path. Is that supposed to mean anything to the +algorithm? Yes. The place where it is checked can not be any place. self +also can not be checked only in the name case. It also has to be checked +in the rename case. The case for paths also needs to consider it. That +makes it notable in all cases. Sanitification is not necessary. +Well-formed input is expected. The case for renames need some more +attention. The current logic for renames could be reused. It is the same +for both the lookup and the self case. The lookup first finds a module. +Then it decides to modify the path of the module. Then it manipulates +the paths of all items in the subtree rooted at that module. The idea is +the same. It may be feasible to have a closure do this. It would get +passed an arbitrary module to rename. The case of a path requires +further discussion. The self keyword may appear at the start of the +path. It may also appear at the end of a path. The latter matches +against the name basecase during reexport resolution. Well-formed input +does not contain a self module in the middle of a path. Or does it? It +does not. A diagnostic reports just this. The self keyword may only +appear as a starting or ending segment in a path. Well-formed input is +guaranteed here. That should precede the lookups. The self keyword +indicates to carry through with the same stateful module. This will not +use the utility macro. That questions the approach used in that macro. +Though one thing is clear. The lookup should go after the self check. +The new state is now conditionally determined. It can be the same old +state if the path head is the self keyword. It can be the result of the +lookup if the path head is not the self keyword. Discussion is merited +to support other keywords. That check in paths will eventually consider +also the super and crate keywords. The check will have to be expanded. +Though the same strategy should be feasible. The new state is +conditionally defined. It is the current module in the self case. It is +the parent module in the super case. It is the module tree in the crate +case. This makes it obvious there is need for the module tree as state. +The crate case would make both pieces of state become the module tree. From 328fde6493d9edbf04a4104419d827b0d990ea1c Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:36:23 +0200 Subject: [PATCH 054/100] chore: wip --- ctest/src/ast/constant.rs | 2 +- ctest/src/ast/field.rs | 2 +- ctest/src/ast/function.rs | 2 +- ctest/src/ast/module.rs | 13 +++ ctest/src/ast/parameter.rs | 2 +- ctest/src/ast/static_variable.rs | 2 +- ctest/src/ast/structure.rs | 2 +- ctest/src/ast/type_alias.rs | 2 +- ctest/src/ast/union.rs | 2 +- ctest/src/ffi_items.rs | 140 ++++++++++++++++++++++--------- notes/new-algo.md | 58 +++++++++++++ 11 files changed, 179 insertions(+), 48 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index 84d8d0ac44f05..7eb1c56f2e009 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -1,7 +1,7 @@ use crate::BoxStr; /// Represents a constant variable defined in Rust. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Const { pub(crate) public: bool, pub(crate) cached_path: BoxStr, diff --git a/ctest/src/ast/field.rs b/ctest/src/ast/field.rs index c3403961f8823..a0f9a3b97774f 100644 --- a/ctest/src/ast/field.rs +++ b/ctest/src/ast/field.rs @@ -1,7 +1,7 @@ use crate::BoxStr; /// Represents a field in a struct or union defined in Rust. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Field { pub(crate) public: bool, pub(crate) ident: BoxStr, diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index ce1d70cd9a6a3..176c06ea6d1ad 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -7,7 +7,7 @@ use crate::{ /// Represents a function signature defined in Rust. /// /// This structure is only used for parsing functions in extern blocks. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Fn { pub(crate) public: bool, #[expect(unused)] diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index c624086a784a9..d04720e109503 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -11,6 +11,19 @@ pub struct Module { pub(crate) items: FfiItems, } +// [NOTE]: a custom implementation is used because FfiItems does not have a +// PartialEq implementation. It is best if it does not have one. The +// current_module field in FfiItems is not meant for use post-parse-time. It +// would likely be unintuitive to have its PartialEq not take into account. +impl PartialEq for Module { + fn eq(&self, other: &Self) -> bool { + self.public == other.public + && self.cached_path == other.cached_path + && self.path == other.path + && self.items.custom_eq(&other.items) + } +} + impl Module { /// Returns the full path to the module item. /// diff --git a/ctest/src/ast/parameter.rs b/ctest/src/ast/parameter.rs index 58ed170931a70..9d8b8146e14da 100644 --- a/ctest/src/ast/parameter.rs +++ b/ctest/src/ast/parameter.rs @@ -1,7 +1,7 @@ use crate::BoxStr; /// Represents a parameter in a function signature defined in Rust. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Parameter { pub(crate) ident: BoxStr, #[expect(unused)] diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index d6115c437250b..d82c1d997e302 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -7,7 +7,7 @@ use crate::{ /// /// This structure is only used for parsing statics in extern blocks, /// as a result it does not have a field for storing the expression. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Static { pub(crate) public: bool, #[expect(unused)] diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index 11d8edaba3929..b94a842d19150 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -4,7 +4,7 @@ use crate::{ }; /// Represents a struct defined in Rust. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Struct { pub(crate) public: bool, pub(crate) cached_path: BoxStr, diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index 72d4b75401265..31445cad15ee9 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -1,7 +1,7 @@ use crate::BoxStr; /// Represents a type alias defined in Rust. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Type { pub(crate) public: bool, pub(crate) cached_path: BoxStr, diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index 6f1fa2975f856..4e83d06c34f04 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -4,7 +4,7 @@ use crate::{ }; /// Represents a union defined in Rust. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Union { pub(crate) public: bool, pub(crate) cached_path: BoxStr, diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index e2901fdc65dbb..6f2e949361373 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -144,7 +144,24 @@ impl FfiItems { }, items: self.clone(), }; - *self = resolve_use_trees(root_module).items; + *self = resolve_all(root_module).items; + } + + /// Perfomrs a comparison for equality between FfiItems without taking field + /// current_module into consideration. + /// + /// This is not an implementation of PartialEq for FfiItems because it would + /// be unintuitive to have it ignore one field. This is only ever needed + /// during use statement resolution. + pub(super) fn custom_eq(&self, other: &Self) -> bool { + self.aliases == other.aliases + && self.structs == other.structs + && self.unions == other.unions + && self.constants == other.constants + && self.foreign_functions == other.foreign_functions + && self.foreign_statics == other.foreign_statics + && self.uses == other.uses + && self.modules == other.modules } /// Searches for an item in the container, and returns an owned instance of @@ -282,6 +299,38 @@ fn normalize_path(path: syn::UseTree) -> Vec { } } +#[derive(Debug, Clone)] +struct PassPair { + subtree: Module, + whole_crate: Module, +} + +/// This is the resolution driver. It is the entrypoint for whole crate +/// resolution. +/// +/// It performs passes on a given module. It diffs the results of a whole-crate +/// pass with the pre-pass state. Equality in that comparison determines +/// termination. +fn resolve_all(root: Module) -> Module { + let (ControlFlow::Break(m) | ControlFlow::Continue(m)) = + iter::repeat(()).try_fold(root, |root, _| { + // [NOTE]: it holds that a pass over the crate root will yield the + // crate root as its subtree. It is thus safe to ignore whole-crate + // module tree (they're the same at this point.) + let PassPair { + subtree: new_root, .. + } = resolve_pass(&PassPair { + subtree: root.clone(), + whole_crate: root.clone(), + }); + match new_root == root { + true => ControlFlow::Break(new_root), + false => ControlFlow::Continue(new_root), + } + }); + m +} + enum Resolution { Resolved { original_use: RefinedUse, @@ -290,47 +339,58 @@ enum Resolution { Unresolved, } -fn resolve_use_trees(root: Module) -> Module { - let resolved_children = root - .items - .modules - .into_iter() - .map(resolve_use_trees) - .collect(); - let root = { - let items = FfiItems { - modules: resolved_children, - ..root.items - }; - Module { items, ..root } +fn resolve_pass(src: &PassPair) -> PassPair { + let (PassPair { whole_crate, .. }, resolved_children) = + root.items.modules.clone().into_iter().fold( + (src, Vec::new()), + |(PassPair { whole_crate, .. }, mut children), m| { + let pair = resolve_pass(PassPair { + subtree: m, + whole_crate, + }); + children.push(pair.subtree); + (pair, children) + }, + ); + let resolved_uses = resolve_one(PassPair { + subtree: Module { + items: FfiItems { + modules: resolved_children, + ..src.subtree.items + }, + ..src.subtree + }, + whole_crate, + }); + let new_root = merge_module(root.clone(), resolved_uses.clone()); + let new_root = Module { + items: new_root.items.dedup(), + ..new_root }; - let (ControlFlow::Continue(root) | ControlFlow::Break(root)) = - iter::repeat(()).try_fold(root, |root, _| { - let resolved_uses = resolve_one(root.clone()); - let new_root = merge_module(root.clone(), resolved_uses); - let new_root = Module { - items: new_root.items.dedup(), - ..new_root - }; - match root.items.uses.len() == new_root.items.uses.len() { - true => ControlFlow::Break(new_root), - false => ControlFlow::Continue(new_root), - } - }); - root + todo!("produce a new whole-crate module tree through the new subtree"); + new_root } -fn resolve_one(src: Module) -> Vec { - src.items +fn resolve_one(src: PassPair) -> Vec { + src.subtree + .items .uses .clone() .into_iter() - .map(|u| (u.clone(), u, src.clone())) - .map(|(ou, u, m)| resolve_use(ou, u, m)) + .map(|u| { + resolve_use( + u.clone(), + u, + PassPair { + subtree: src.clone(), + whole_crate: state.clone(), + }, + ) + }) .collect() } -fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Resolution { +fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! self_case { ($it:ident, $target:expr) => { if is_self($it.clone()) { @@ -358,8 +418,8 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } }}; } - self_case!(ident, state); - match state.items.search(ident.clone()) { + self_case!(ident, state.subtree); + match state.subtree.items.search(ident.clone()) { Some(GenericItem::Type(t)) => single_item!(aliases: t), Some(GenericItem::Struct(s)) => single_item!(structs: s), Some(GenericItem::Union(u)) => single_item!(unions: u), @@ -372,7 +432,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re } RefinedUseTree::Glob => Resolution::Resolved { original_use, - items: state.items, + items: state.subtree.items, }, RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => { macro_rules! single_item { @@ -432,8 +492,8 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re m, ) }; - self_case!(ident, renamer(state)); - match state.items.search(ident.clone()) { + self_case!(ident, renamer(state.subtree)); + match state.subtree.items.search(ident.clone()) { Some(GenericItem::Module(m)) => Resolution::Resolved { original_use, items: FfiItems { @@ -453,9 +513,9 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: Module) -> Re RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { let new_state = if is_self(ident.clone()) { - state + state.subtree } else { - match state.items.search(ident.clone()) { + match state.subtree.items.search(ident.clone()) { Some(GenericItem::Module(m)) => m, None => return Resolution::Unresolved, Some(_) => unreachable!( diff --git a/notes/new-algo.md b/notes/new-algo.md index 6c2d4cd6ca1b4..e36ce26798eda 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -205,3 +205,61 @@ conditionally defined. It is the current module in the self case. It is the parent module in the super case. It is the module tree in the crate case. This makes it obvious there is need for the module tree as state. The crate case would make both pieces of state become the module tree. +The next step seems to be to add support for the new state. This now +comprises the crate-wide module tree. The first thing that needs +modification is the top-level loop. This should be removed from the +single-module resolution proof. Though there is no obvious proposition +for global resolution. It seems like the place to put this is the +parsing entrypoint for the item container. That would be subpar. A +better approach may be to abstract that away into another proposition. +That proposition would perform the global passes. That should leave the +parsing entry point unmodified. The global resolution proposition should +never be called recursively. That should ensure one can reason about the +crate module as the only consumer of the proposition. The proof should +call into the proposition that performs one pass over the entire module +tree. The implication of that is another module. This is guaranteed to +be the post-pass state of the crate root. Then the diffing step should +come. This is a diff between the pre-pass and post-pass states of the +whole module tree. This comparison proposition needs discussion. It +should compare the shape of the tree. Maybe an automatically derived +PartialEq implementation will do. It will not. That would also take into +consideration the current_module field used at parse-time. Something +similar would be convenient. The proposition for equality would then +"write itself." A comparison of aliases would do. Then would follow a +comparison of records. And then a comparison of all other fields. Except +for the parse-time utility field. That should do the trick. This assumes +there are PartialEq implementations for the types used for items. That +does not necessarily hold. Though those should be feasible. Those +implementations seem done now. A comparison of a tree should trigger a +recursive comparison of the entire tree. Though note this is exposed as +a separate proposition. The driver seems to be now ready. The next thing +should be to change the state passed to the single-pass proposition. +This needs to now also pass the module tree in whole. That should be +seeded with the same value as is currently used for the crate root. This +awards a dedicated proposition. It should gather within it the state as +a subtree and the whole-crate module tree. This should also make up the +implication of a single resolution pass. Then it follows that there +exists a new state after each child module is processed. This calls for +a fold instead of a map. The fold implies a product of identity +functors. The pair is comprised of an updated state and a resolved list +of child modules. The updated state itself reflects the state carried +across passes. The resolved list represents the previously mapped list +of child modules. There is something unnecessary here. There is no need +to have a duplicate list of updated child modules. It is enough that the +stateful pair carries the subtree and the state. Except it is not. The +subtree in the stateful pair refers not to the module from which the +child modules spawned. There is need to reconstruct a new module. This +should hold the resolved child modules. Then comes resolution of the +module itself. This should hold this newly constructed module. The other +half of the pair should hold the stateful module tree from the fold over +the list of child modules. The proposition for single-module resolution +should now take a stateful pair. The list of resolutions need not carry +with it an updated whole-crate state. That changes only after a pass +over a given module is done. Discussion of the single-reexport +resolution proposition will be delayed for later. The next step follows +back in the single-pass proposition. The proof now has a set of +resolution items. It needs to merge those with the module currently at +hand. Then it needs to deduplicate the items. The last thing is to +update the stateful module tree. Merging already has a dedicated +proposition. Deduplication also has a dedicated proposition. Updating +the module tree does not. From e2b41f96c345452651ba9457344f86052dca6d9c Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:11:32 +0200 Subject: [PATCH 055/100] chore: wip --- ctest/src/ffi_items.rs | 76 +++++++++++++++++++++++++++--------------- notes/new-algo.md | 33 ++++++++++++++++-- 2 files changed, 81 insertions(+), 28 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 6f2e949361373..8d9649ab16ae2 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -147,12 +147,12 @@ impl FfiItems { *self = resolve_all(root_module).items; } - /// Perfomrs a comparison for equality between FfiItems without taking field + /// Performs a comparison for equality between FfiItems without taking field /// current_module into consideration. /// /// This is not an implementation of PartialEq for FfiItems because it would /// be unintuitive to have it ignore one field. This is only ever needed - /// during use statement resolution. + /// during use declaration resolution. pub(super) fn custom_eq(&self, other: &Self) -> bool { self.aliases == other.aliases && self.structs == other.structs @@ -274,8 +274,8 @@ pub(crate) struct RefinedUse { tree: RefinedUseTree, } -/// Gets rid of group reexports in a `use` statement by flattenning them into a -/// set of individual reexports. +/// Gets rid of group reexports in a `use` declaration by flattenning them into +/// a set of individual reexports. fn normalize_path(path: syn::UseTree) -> Vec { match path { UseTree::Name(n) => vec![RefinedUseTree::Name(n)], @@ -315,11 +315,11 @@ fn resolve_all(root: Module) -> Module { let (ControlFlow::Break(m) | ControlFlow::Continue(m)) = iter::repeat(()).try_fold(root, |root, _| { // [NOTE]: it holds that a pass over the crate root will yield the - // crate root as its subtree. It is thus safe to ignore whole-crate - // module tree (they're the same at this point.) + // crate root as its subtree. It is thus safe to ignore the + // whole-crate module tree. They are the same at this point. let PassPair { subtree: new_root, .. - } = resolve_pass(&PassPair { + } = resolve_pass(PassPair { subtree: root.clone(), whole_crate: root.clone(), }); @@ -331,6 +331,7 @@ fn resolve_all(root: Module) -> Module { m } +#[derive(Debug, Clone)] enum Resolution { Resolved { original_use: RefinedUse, @@ -339,41 +340,52 @@ enum Resolution { Unresolved, } -fn resolve_pass(src: &PassPair) -> PassPair { +fn resolve_pass(src: PassPair) -> PassPair { let (PassPair { whole_crate, .. }, resolved_children) = - root.items.modules.clone().into_iter().fold( - (src, Vec::new()), + src.subtree.items.modules.clone().into_iter().fold( + (src.clone(), Vec::new()), |(PassPair { whole_crate, .. }, mut children), m| { let pair = resolve_pass(PassPair { subtree: m, - whole_crate, + whole_crate: whole_crate.clone(), }); - children.push(pair.subtree); + children.push(pair.subtree.clone()); (pair, children) }, ); - let resolved_uses = resolve_one(PassPair { - subtree: Module { - items: FfiItems { - modules: resolved_children, - ..src.subtree.items - }, - ..src.subtree + let root_with_resolved_children = Module { + items: FfiItems { + modules: resolved_children, + ..src.subtree.items }, + ..src.subtree + }; + let resolved_uses = resolve_one(PassPair { + subtree: root_with_resolved_children.clone(), whole_crate, }); - let new_root = merge_module(root.clone(), resolved_uses.clone()); + let new_root = merge_module(root_with_resolved_children, resolved_uses); let new_root = Module { items: new_root.items.dedup(), ..new_root }; - todo!("produce a new whole-crate module tree through the new subtree"); - new_root + let new_whole_crate = update_tree(PassPair { + subtree: new_root, + whole_crate: new_whole_crate, + }); + PassPair { + subtree: new_root, + whole_crate: new_whole_crate, + } } -fn resolve_one(src: PassPair) -> Vec { - src.subtree - .items +fn resolve_one( + PassPair { + subtree: root, + whole_crate: state, + }: PassPair, +) -> Vec { + root.items .uses .clone() .into_iter() @@ -382,7 +394,7 @@ fn resolve_one(src: PassPair) -> Vec { u.clone(), u, PassPair { - subtree: src.clone(), + subtree: root.clone(), whole_crate: state.clone(), }, ) @@ -539,6 +551,18 @@ fn is_self(ident: syn::Ident) -> bool { ident.to_string() == "self" } +fn is_crate(ident: syn::Ident) -> bool { + ident.to_string() == "crate" +} + +fn is_super(ident: syn::Ident) -> bool { + ident.to_string() == "super" +} + +fn update_tree(src: Module, dst: Module) -> Module { + todo!(); +} + fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module) -> Module { let new_root_path = f(root.path.clone()); let new_root_cache = path_to_string(&new_root_path); diff --git a/notes/new-algo.md b/notes/new-algo.md index e36ce26798eda..a429fdb2c38a2 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -261,5 +261,34 @@ back in the single-pass proposition. The proof now has a set of resolution items. It needs to merge those with the module currently at hand. Then it needs to deduplicate the items. The last thing is to update the stateful module tree. Merging already has a dedicated -proposition. Deduplication also has a dedicated proposition. Updating -the module tree does not. +proposition. Deduplication also has a dedicated proposition. Whole-crate +module tree updates do not. That will require more work than the +equality proposition. The goal is to find a subtree in the crate tree. +Recursive updates are not necessary. The subtree need only update the +data of the subtree's root. That will be justified now. The simplest +case is a leaf node. These are resolved first. That is thanks to the +depth-first approach taken in the single-pass proposition. The +whole-crate module tree is updated here as well. The update would find +the leaf module. Then it would update its items. The leaf has no child +modules. So the only updated items are non-modules. The whole-crate +module now contains an updated leaf. Say it now goes to this leaf's +parent module. Suppose this parent module contains one other child +module. Further suppose that this other child is also a leaf. The +whole-crate module tree would be updated anew. Then it would reflect +both resolved child modules. Then comes the time to resolve the parent +module. One could attempt to resolve it and its children. There is no +need for the latter. There may be one exception to this. The module may +now have new child modules. That does not change one basic fact. The +module ought be found in the whole-crate tree. Then it needs to have its +items replaced with the updated module's items. That should get the job +done every time. A recap is in order. The proposition should assume one +module. Then it should assume another module. One is the crate root. The +other is the subtree. The subtree could also be the crate root. It is +best if a pass pair is passed instead. Though it should not imply a pass +pair. It should only imply a module. That should be the updated +whole-crate module tree. There is a problem in this approach. The +proposition will be used inductively in its proof. That means a pass +pair would be misleading. One of the projections refers to the +whole-crate tree. The other does not. The proposition will be used with +some other module in the whole-crate tree. The proof is not entirely +clear. From 75de1933672aeb2c082ae87aa84fafae19d7fab7 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:51:19 +0200 Subject: [PATCH 056/100] chore: wip --- ctest/src/ffi_items.rs | 62 ++++++++++++++++++++++++++++++++++++++---- notes/new-algo.md | 45 +++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 8d9649ab16ae2..cbe77ce14b98f 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -153,6 +153,9 @@ impl FfiItems { /// This is not an implementation of PartialEq for FfiItems because it would /// be unintuitive to have it ignore one field. This is only ever needed /// during use declaration resolution. + /// + /// The visibility restriction allows the Module type in ast/module.rs to + /// have access to it. pub(super) fn custom_eq(&self, other: &Self) -> bool { self.aliases == other.aliases && self.structs == other.structs @@ -362,17 +365,14 @@ fn resolve_pass(src: PassPair) -> PassPair { }; let resolved_uses = resolve_one(PassPair { subtree: root_with_resolved_children.clone(), - whole_crate, + whole_crate: whole_crate.clone(), }); let new_root = merge_module(root_with_resolved_children, resolved_uses); let new_root = Module { items: new_root.items.dedup(), ..new_root }; - let new_whole_crate = update_tree(PassPair { - subtree: new_root, - whole_crate: new_whole_crate, - }); + let new_whole_crate = update_tree(new_root.clone(), whole_crate); PassPair { subtree: new_root, whole_crate: new_whole_crate, @@ -430,6 +430,13 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> } }}; } + // [NOTE]: self may not appear as the name basecase of a well-formed + // path. And yet it is still considered here. That is because of + // path normalization. self may appear as part of a tail group in a + // use declaration's path. Path normalization flattens groups into + // individual use statements. Path normalizatio is done through + // normalize_path above. That happens at parse-time. That is why + // this function must consider the case of self as a name basecase. self_case!(ident, state.subtree); match state.subtree.items.search(ident.clone()) { Some(GenericItem::Type(t)) => single_item!(aliases: t), @@ -547,6 +554,33 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> } } +fn get_module(target: syn::Path, root: Module) { + if root.path == target { + return root; + } + todo!( + "Fold over the list of child modules. Do so with a termination-prone \ + transform." + ); +} + +fn get_parent_path(root: Module) -> syn::Path { + let new_segments = root + .path + .segments + .clone() + .into_iter() + .take(root.path.segments.len().saturating_sub(1)) + .fold(Punctuated::new(), |mut p, s| { + p.push(s); + p + }); + syn::Path { + leading_colon: None, + segments: new_segments, + } +} + fn is_self(ident: syn::Ident) -> bool { ident.to_string() == "self" } @@ -560,7 +594,23 @@ fn is_super(ident: syn::Ident) -> bool { } fn update_tree(src: Module, dst: Module) -> Module { - todo!(); + if src.path == dst.path { + return src.into(); + } + let new_children = dst + .items + .modules + .clone() + .into_iter() + .map(|m| update_tree(src.clone(), m)) + .collect(); + Module { + items: FfiItems { + modules: new_children, + ..dst.items + }, + ..dst + } } fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module) -> Module { diff --git a/notes/new-algo.md b/notes/new-algo.md index a429fdb2c38a2..34d868937fbbb 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -291,4 +291,47 @@ proposition will be used inductively in its proof. That means a pass pair would be misleading. One of the projections refers to the whole-crate tree. The other does not. The proposition will be used with some other module in the whole-crate tree. The proof is not entirely -clear. +clear. The destination module's path has to be checked with the source +module's path. A match means the latter's proof must be implied. A +mismatch means the former's children must be matched. This should be +done through a fold transform. Recall only one module matches. The fold +should be prone to termination. What should the fold's seed be? It is +known the source module will surely be found. That is not a guarantee +the type system understands. A dummy value should do. The handiest one +is the source module. This is incorrect. Recall the proposition performs +walks on the module tree. One such walk could hit a dead-end. Such a +dead-end would manifest itself in a leaf module. That should imply a +coproduct. Its injections should correspond with the identity functor +and an injective const functor. That should be the seed of the fold over +the list of child modules. This means the whole proposition should imply +the above coproduct. The proof is wrong. One should not fold over the +list of child modules. That would mean some modules are downright lost. +The simplest example is a one-level deep module tree. Suppose there are +an arbitrary number n of nodes. Assume n is greater than 1. Further +suppose that the update subtree is on of these child modules. The +current proof would discard both the root module and the sibling module. +The proof should map over the list of child modules. Then it should +imply the mismatched module with the updated children. One of those +children modules will have matched the source module. That should do it +for the single-pass proposition. The next thing should be the +single-reexport proposition. Each case will be discussed in turn. The +name basecase can only ever refer to self and super. The self case is +covered. A similar macro could be elaborated for the super case. Is +super really possible in the name basecase? It can not. Can the crate +keyword appear alone? Not the case. The only one that can appear as a +basecase is the self keyword. The crate keyword can only appear at the +start of a path. The super keyword can only appear in the rename case. +It is otherwise a segment head in a larger path. The rename case for a +super keyword should access the module right above the current one. That +means there is need for an auxiliary proposition. That proposition +should assume the whole-crate module tree. It should also assume a path. +It should then look for that path in the module tree. Then there should +be need for another auxiliary proposition. This should get the parent +path to some module. It should assume a module subtree. It should imply +a path. That will then be used in the assumption for the prior +proposition. The proof for the parent path is fairly simple. It should +take a certain number of segments from the assumed module's path. The +number should correspond with one fewer segment than the path contains. +The proposition to fetch a given module should first match on the path +of the subtree with the target path. A mismatch indicates it should fold +over the module's child paths. The fold should be prone to termination. From 64021b848fb175343f9cb6ce1e3a748be10e7b4b Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:13:28 +0200 Subject: [PATCH 057/100] chore: wip --- ctest/src/ffi_items.rs | 336 +++++++++++++++++++++++++++++------------ notes/new-algo.md | 6 + 2 files changed, 246 insertions(+), 96 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index cbe77ce14b98f..fcb899ff38a7a 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -8,13 +8,19 @@ use std::ops::{ Deref, }; +use proc_macro2::Span; use quote::ToTokens; use syn::punctuated::Punctuated; +use syn::token::Brace; use syn::visit::{ self, Visit, }; use syn::{ + Ident, + Path, + PathArguments, + PathSegment, UseTree, Visibility, }; @@ -130,8 +136,8 @@ impl FfiItems { vis: syn::parse_quote! { pub }, unsafety: None, mod_token: syn::token::Mod::default(), - ident: syn::Ident::new("__ctest_root_mod", proc_macro2::Span::call_site()), - content: Some((syn::token::Brace::default(), mod_items.clone())), + ident: Ident::new("__ctest_root_mod", Span::call_site()), + content: Some((Brace::default(), mod_items.clone())), semi: None, }; self.visit_item_mod(&file_mod); @@ -215,6 +221,12 @@ impl FfiItems { items.swap_remove(idx).into() } + /// Deduplicates the items in an item container by their path. Items of + /// differing kind, like functions and constants, that may happen to have + /// the same path will not get deduplicated. + /// + /// A consequence of calling this method is that the items are also sorted + /// by their paths. fn dedup(mut self) -> FfiItems { macro_rules! key_fn { () => { @@ -343,6 +355,17 @@ enum Resolution { Unresolved, } +/// Performs one pass through the provided pair of a subtree and the whole-crate +/// subtree. +/// +/// The input pair's fields correspond with the subtree in which to perform the +/// resolution pass on, and the tree at the crate root. The latter is required +/// to resolve absolute imports that refer to some item in an ancestor module +/// that is more than one level up the tree than the subtree being resolved. +/// +/// The returned pair contains updated snapshots of the input pair. The subtree +/// may have had new items added to it. The whole-crate module tree is +/// guaranteed to have integrated these changes. fn resolve_pass(src: PassPair) -> PassPair { let (PassPair { whole_crate, .. }, resolved_children) = src.subtree.items.modules.clone().into_iter().fold( @@ -379,6 +402,13 @@ fn resolve_pass(src: PassPair) -> PassPair { } } +/// Attempts to resolve the `use` declarations in a given module. +/// +/// The input pair consists of the module from which to source the `use` +/// declarations, and of the whole-crate module tree. The latter is used to +/// resolve certain types of paths in `use` declarations. +/// +/// See the documentation on [`resolve_pass`] for details. fn resolve_one( PassPair { subtree: root, @@ -402,17 +432,39 @@ fn resolve_one( .collect() } +/// Attempts to resolve a single `use` declaration. +/// +/// This can go one of two ways. +/// +/// 1. Either the `use` declaration matches a specific module in the stateful +/// pair, or +/// 2. the `use` declaration is assumed to be unresolved in this resolution +/// pass. +/// +/// The stateful pair initially consists of the module in which the `use` +/// declaration appears at. If the `use` declaration refers to an item by a path +/// with more than one segment, this function recursively munches through it. +/// +/// These recursive calls will only be made if the extracted path segment +/// matches against some item in the stateful subtree. Currently, the only types +/// of items that can match on a path that still has another unmunched segment +/// are modules. +/// +/// Upon finding the module, the new stateful subtree of the pair becomes this +/// module. This is then used in the recursive call with the path tail. +/// +/// The other element of the pair is the whole-crate module tree. That is used +/// to resolve ancestor paths (when `super` is found) and absolute paths (when +/// `crate` is found.) fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> Resolution { - macro_rules! self_case { - ($it:ident, $target:expr) => { - if is_self($it.clone()) { - return Resolution::Resolved { - original_use, - items: FfiItems { - modules: vec![$target], - ..Default::default() - }, - }; + macro_rules! single_module { + ($it:expr) => { + return Resolution::Resolved { + original_use: original_use.clone(), + items: FfiItems { + modules: vec![$it], + ..Default::default() + }, } }; } @@ -420,13 +472,12 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> RefinedUseTree::Name(syn::UseName { ident }) => { macro_rules! single_item { ($field:ident: $it:ident) => {{ - let items = FfiItems { - $field: vec![$it], - ..Default::default() - }; Resolution::Resolved { - original_use, - items, + original_use: original_use.clone(), + items: FfiItems { + $field: vec![$it], + ..Default::default() + }, } }}; } @@ -434,10 +485,32 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> // path. And yet it is still considered here. That is because of // path normalization. self may appear as part of a tail group in a // use declaration's path. Path normalization flattens groups into - // individual use statements. Path normalizatio is done through + // individual use statements. Path normalization is done through // normalize_path above. That happens at parse-time. That is why // this function must consider the case of self as a name basecase. - self_case!(ident, state.subtree); + // [NOTE]: crate does not appear as the tail end of a path in a use + // declaration under no circumstances, so that case is not + // considered. + if is_self(ident.clone()) { + single_module!(state.subtree.clone()); + } else if is_super(ident.clone()) { + let parent_path = get_parent_path(state.subtree.clone()); + let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) + else { + unreachable!( + "The module path is guaranteed to be found in the \ + whole crate module tree. It is the path of the \ + stateful subtree, which is either the module in which \ + the use declaration appears at, some descendant of \ + that module, or some descendant module to the crate \ + root after having stumbled on some ancestor-referring \ + use declaration. The parent some such module must be \ + below the crate root, as otherwise it would be \ + considered malformed input." + ); + }; + single_module!(parent.clone()); + } match state.subtree.items.search(ident.clone()) { Some(GenericItem::Type(t)) => single_item!(aliases: t), Some(GenericItem::Struct(s)) => single_item!(structs: s), @@ -456,13 +529,13 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => { macro_rules! single_item { ($field:ident , $it:tt : $ty:tt) => {{ - let mut new_segments = $it.path.segments; + let mut new_segments = $it.path.segments.clone(); new_segments.pop(); - new_segments.push(syn::PathSegment { + new_segments.push(PathSegment { ident: rename.clone(), - arguments: syn::PathArguments::None, + arguments: PathArguments::None, }); - let new_path = syn::Path { + let new_path = Path { segments: new_segments, ..$it.path }; @@ -484,42 +557,47 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> let new_path = { let mut base_path_segments = m.path.segments.clone(); base_path_segments.pop(); - base_path_segments.push(syn::PathSegment { + base_path_segments.push(PathSegment { ident: rename.clone(), - arguments: syn::PathArguments::None, + arguments: PathArguments::None, }); - syn::Path { + Path { segments: base_path_segments, ..m.path } }; let new_path_len = new_path.segments.len(); - manipulate_path( - |p| { - let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( - new_path.clone(), - |mut p, s| { - p.segments.push(s); - p - }, - ); - syn::Path { - leading_colon: p.leading_colon, - ..new_path - } - }, - m, - ) + manipulate_path(m, |p| { + let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( + new_path.clone(), + |mut p, s| { + p.segments.push(s); + p + }, + ); + Path { + leading_colon: p.leading_colon, + ..new_path + } + }) }; - self_case!(ident, renamer(state.subtree)); + if is_self(ident.clone()) { + single_module!(renamer(state.subtree.clone())); + } else if is_super(ident.clone()) { + let parent_path = get_parent_path(state.subtree.clone()); + let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) + else { + unreachable!( + "See the note on super in the name basecase. The same \ + applies here." + ); + }; + single_module!(renamer(parent.clone())); + } else if is_crate(ident.clone()) { + single_module!(renamer(state.whole_crate.clone())); + } match state.subtree.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => Resolution::Resolved { - original_use, - items: FfiItems { - modules: vec![renamer(m)], - ..Default::default() - }, - }, + Some(GenericItem::Module(m)) => single_module!(renamer(m)), Some(GenericItem::Type(t)) => single_item!(aliases, t: Type), Some(GenericItem::Struct(s)) => single_item!(structs, s: Struct), Some(GenericItem::Union(u)) => single_item!(unions, u: Union), @@ -531,11 +609,23 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> } RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { - let new_state = if is_self(ident.clone()) { - state.subtree + let new_subtree = if is_self(ident.clone()) { + state.subtree.clone() + } else if is_super(ident.clone()) { + let parent_path = get_parent_path(state.subtree.clone()); + let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) + else { + unreachable!( + "See the note on super in the name basecase. The same \ + applies here." + ); + }; + parent + } else if is_crate(ident.clone()) { + state.whole_crate.clone() } else { match state.subtree.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => m, + Some(GenericItem::Module(m)) => m.clone(), None => return Resolution::Unresolved, Some(_) => unreachable!( "Paths parsed in ctest do not include enum variants. \ @@ -546,25 +636,62 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> } }; let new_use = RefinedUse { - is_public: r#use.is_public, + is_public: r#use.is_public.clone(), tree: *tree.clone(), }; - resolve_use(original_use, new_use, new_state) + resolve_use( + original_use, + new_use, + PassPair { + subtree: new_subtree.clone(), + whole_crate: state.whole_crate.clone(), + }, + ) } } } -fn get_module(target: syn::Path, root: Module) { +/// Attempts to find a given module path (`target`) within a module subtree +/// (`root`). +fn get_module(target: syn::Path, root: Module) -> Option { if root.path == target { - return root; + return root.into(); } - todo!( - "Fold over the list of child modules. Do so with a termination-prone \ - transform." - ); + let (ControlFlow::Break(m) | ControlFlow::Continue(m)) = root + .items + .modules + .clone() + .into_iter() + .try_fold(None, |_, m| { + if let m @ Some(_) = get_module(target.clone(), m) { + ControlFlow::Break(m) + } else { + ControlFlow::Continue(None) + } + }); + m } +/// Fetches the parent path to a given module. +/// +/// If the module is a top-level item (i.e. it appears at the crate root,) the +/// returned path will be empty. This follows from the parsing strategy for +/// paths in `FfiItems`. The absolute paths are all `crate`-_unprepended_. fn get_parent_path(root: Module) -> syn::Path { + // [NOTE]: this performs a `saturating_sub` because there is one and only + // one item in the crate's module tree that has an empty path; The crate + // root. That is only wrapped in a `Module` of its own only after parsing. + // This function currently only gets used during parsing, so there is a + // chance this gets called with the virtual module that wraps the crate + // root's `FfiItems`. That is not logically possible, because well-formed + // input can not have a `use` declaration with `super` in the path at the + // crate root. This function is used when stumbling upon `super` in function + // `resolve_use`. + // + // The above assumption does not change the way this is implemented, except + // you could very well change the `saturating_sub` for overflowing + // subtraction. It is best if this function is protected against what would + // currently be misuse. let new_segments = root .path .segments @@ -581,18 +708,29 @@ fn get_parent_path(root: Module) -> syn::Path { } } +/// Checks if the given identifier was parsed as the Rust keyword `self`. fn is_self(ident: syn::Ident) -> bool { ident.to_string() == "self" } +/// Checks if the given identifier was parsed as the Rust keyword `crate`. fn is_crate(ident: syn::Ident) -> bool { ident.to_string() == "crate" } +/// Checks if the given identifier was parsed as the Rust keyword `super`. fn is_super(ident: syn::Ident) -> bool { ident.to_string() == "super" } +/// Integrates module subtree `src` into module subtree `dst`. +/// +/// It attempts to find `src` in `dst`. It returns an "updated" snapshot of +/// `dst` with the contents of `src` integrated. Note `src` may or may not have +/// been found within some subtree of `dst`. +/// +/// If not found, `dst` is returned unmodified. `src` may also match `dst` +/// itself. That would return `src`. fn update_tree(src: Module, dst: Module) -> Module { if src.path == dst.path { return src.into(); @@ -613,7 +751,11 @@ fn update_tree(src: Module, dst: Module) -> Module { } } -fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module) -> Module { +/// Applies a transform `f` to the path of each item in subtree `root`. +/// +/// The subtree root module itself also gets the transform applied to its path. +/// The `cached_path` field of each item is updated accordingly. +fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> syn::Path + Clone) -> Module { let new_root_path = f(root.path.clone()); let new_root_cache = path_to_string(&new_root_path); macro_rules! map_items { @@ -645,8 +787,7 @@ fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module .modules .clone() .into_iter() - .map(|m| (f.clone(), m)) - .map(|(f, m)| manipulate_path(f, m)) + .map(|m| manipulate_path(m, f.clone())) .collect(); let new_items = FfiItems { aliases: new_aliases, @@ -666,6 +807,15 @@ fn manipulate_path(f: impl ops::Fn(syn::Path) -> syn::Path + Clone, root: Module } } +/// Integrates the resolution items of `src` into `dst`. +/// +/// Each resolution item is either one of an `Unresolved` or a `Resolved` +/// variant. The former are essentially ignored. The latter pack an `FfiItems` +/// that is integrated into `dst`'s own items. That is the merging part. +/// +/// Note that, during merging, the items to be merged may end up duplicating +/// existing items on `dst`. It is call-site's responsibility to deduplicate +/// those through `FfiItems::dedup`. fn merge_module(dst: Module, src: Vec) -> Module { let new_path = dst.path.clone(); src.into_iter() @@ -702,10 +852,10 @@ fn merge_module(dst: Module, src: Vec) -> Module { // will be extracted, and the module trashed. But the module, // even though a dummy, will also have the closure further down // below applied to its path, so it needs to be non-empty. - let mut dummy_segment = syn::punctuated::Punctuated::new(); - dummy_segment.push(syn::PathSegment { - ident: syn::Ident::new("dummy", proc_macro2::Span::call_site()), - arguments: syn::PathArguments::None, + let mut dummy_segment = Punctuated::new(); + dummy_segment.push(PathSegment { + ident: Ident::new("dummy", Span::call_site()), + arguments: PathArguments::None, }); let items_without_modules = FfiItems { aliases, @@ -725,20 +875,17 @@ fn merge_module(dst: Module, src: Vec) -> Module { }, items: items_without_modules, }; - let new_module = manipulate_path( - |p| { - let Some(ident) = p.segments.last().cloned() else { - unreachable!( - "all parsed items have at least one segment in \ - their paths" - ) - }; - let mut new_path = new_path.clone(); - new_path.segments.push(ident); - new_path - }, - module, - ); + let new_module = manipulate_path(module, |p| { + let Some(ident) = p.segments.last().cloned() else { + unreachable!( + "all parsed items have at least one segment in \ + their paths" + ) + }; + let mut new_path = new_path.clone(); + new_path.segments.push(ident); + new_path + }); let new_nested_modules: Vec<_> = modules .into_iter() .map(|m| { @@ -748,19 +895,16 @@ fn merge_module(dst: Module, src: Vec) -> Module { segment in their paths" ); }; - manipulate_path( - |p| { - p.segments - .clone() - .into_iter() - .skip_while(|s| *s != base_path) - .fold(new_path.clone(), |mut p, s| { - p.segments.push(s); - p - }) - }, - m, - ) + manipulate_path(m, |p| { + p.segments + .clone() + .into_iter() + .skip_while(|s| *s != base_path) + .fold(new_path.clone(), |mut p, s| { + p.segments.push(s); + p + }) + }) }) .collect(); ( diff --git a/notes/new-algo.md b/notes/new-algo.md index 34d868937fbbb..261082876d95e 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -335,3 +335,9 @@ number should correspond with one fewer segment than the path contains. The proposition to fetch a given module should first match on the path of the subtree with the target path. A mismatch indicates it should fold over the module's child paths. The fold should be prone to termination. +There is need for a seed value. One knows the module can be found +somewhere in the crate graph. That is the main use of the proposition. +This could be generalized into implying a coproduct. One injection would +correspond with an injective const functor. The other would correspond +with the identity functor. The assumption that the module really does +exist can be left to call-site. From a9ec090ccb9c570607d5ef500376ff2ab9226c8c Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:58:25 +0200 Subject: [PATCH 058/100] chore: wip --- ctest/src/ffi_items.rs | 88 +++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index fcb899ff38a7a..113a958765742 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -144,7 +144,7 @@ impl FfiItems { let root_module = Module { public: bool::default(), cached_path: String::new().into_boxed_str(), - path: syn::Path { + path: Path { leading_colon: None, segments: Punctuated::new(), }, @@ -261,7 +261,7 @@ impl Default for FfiItems { foreign_statics: Default::default(), modules: Default::default(), uses: Default::default(), - current_module: syn::Path { + current_module: Path { leading_colon: None, segments: Default::default(), }, @@ -269,12 +269,25 @@ impl Default for FfiItems { } } +/// Refined type for [`syn::UsePath`] that does not consider group paths. +/// +/// This is a lot like [`RefinedUse`] in that it only exists as replacement for +/// [`syn::UsePath`] but uses [`RefinedUseTree`] instead of [`syn::UseTree`]. +/// See [`RefinedUseTree`] for details. #[derive(Clone, Debug, PartialEq)] pub(crate) struct RefinedUsePath { ident: syn::Ident, tree: Box, } +/// Refined type for [`syn::UseTree`] that does not consider group paths. +/// +/// This is the byproduct of normalizing a `use` declaration's path through +/// [`normalize_path`]. That function ensures some such path containing a group +/// as its tail end is converted into a set of paths without groups. +/// +/// This enumeration is thus equivalent to `syn::UseTree` but does not consider +/// the case of `syn::UseTree::Group`. #[derive(Clone, Debug, PartialEq)] pub(crate) enum RefinedUseTree { Path(RefinedUsePath), @@ -283,6 +296,11 @@ pub(crate) enum RefinedUseTree { Glob, } +/// Refined type for `use` declarations that do not consider group paths. +/// +/// This exists only for the purpose of refining [`syn::UseTree`], which is used +/// within [`syn::ItemUse`]. See type [`RefinedUseTree`] for the actual +/// refinement and details on the purpose of this type. #[derive(Clone, Debug, PartialEq)] pub(crate) struct RefinedUse { is_public: bool, @@ -314,6 +332,15 @@ fn normalize_path(path: syn::UseTree) -> Vec { } } +/// Stateful pair carried through resolution passes. +/// +/// Each `use` declaration requires access to either one of (1) the module where +/// it was sourced from, or (2) the crate root's subtree, or (3) any one +/// ascendant or descendant module of the module where it was sourced from. +/// +/// Points (2) and (3) require having access to the entire crate's module tree. +/// Some ancestor item is looked up by the parent path to the item being +/// searched in respect of. Point (1) is satisfied by the `subtree` field. #[derive(Debug, Clone)] struct PassPair { subtree: Module, @@ -346,6 +373,11 @@ fn resolve_all(root: Module) -> Module { m } +/// Marks a `use` declaration and a set of items as resolved or unresolved. +/// +/// This is the product of an attempt at resolving a given `use` declaration. It +/// is then used in [`merge_module`] to merge whichever `use` declarations +/// resolved to a set of items back into the module they were sourced from. #[derive(Debug, Clone)] enum Resolution { Resolved { @@ -438,6 +470,7 @@ fn resolve_one( /// /// 1. Either the `use` declaration matches a specific module in the stateful /// pair, or +/// /// 2. the `use` declaration is assumed to be unresolved in this resolution /// pass. /// @@ -457,12 +490,12 @@ fn resolve_one( /// to resolve ancestor paths (when `super` is found) and absolute paths (when /// `crate` is found.) fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> Resolution { - macro_rules! single_module { - ($it:expr) => { + macro_rules! single_item { + ($field:ident: $it:expr) => { return Resolution::Resolved { original_use: original_use.clone(), items: FfiItems { - modules: vec![$it], + $field: vec![$it], ..Default::default() }, } @@ -470,17 +503,6 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> } match &r#use.tree { RefinedUseTree::Name(syn::UseName { ident }) => { - macro_rules! single_item { - ($field:ident: $it:ident) => {{ - Resolution::Resolved { - original_use: original_use.clone(), - items: FfiItems { - $field: vec![$it], - ..Default::default() - }, - } - }}; - } // [NOTE]: self may not appear as the name basecase of a well-formed // path. And yet it is still considered here. That is because of // path normalization. self may appear as part of a tail group in a @@ -492,7 +514,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> // declaration under no circumstances, so that case is not // considered. if is_self(ident.clone()) { - single_module!(state.subtree.clone()); + single_item!(modules: state.subtree.clone()); } else if is_super(ident.clone()) { let parent_path = get_parent_path(state.subtree.clone()); let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) @@ -509,7 +531,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> considered malformed input." ); }; - single_module!(parent.clone()); + single_item!(modules: parent.clone()); } match state.subtree.items.search(ident.clone()) { Some(GenericItem::Type(t)) => single_item!(aliases: t), @@ -527,7 +549,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> items: state.subtree.items, }, RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => { - macro_rules! single_item { + macro_rules! renamed_single_item { ($field:ident , $it:tt : $ty:tt) => {{ let mut new_segments = $it.path.segments.clone(); new_segments.pop(); @@ -544,13 +566,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> path: new_path, ..$it }; - Resolution::Resolved { - original_use, - items: FfiItems { - $field: vec![new_item], - ..Default::default() - }, - } + single_item!($field: new_item); }}; } let renamer = |m: Module| { @@ -582,7 +598,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> }) }; if is_self(ident.clone()) { - single_module!(renamer(state.subtree.clone())); + single_item!(modules: renamer(state.subtree.clone())); } else if is_super(ident.clone()) { let parent_path = get_parent_path(state.subtree.clone()); let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) @@ -592,18 +608,18 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> applies here." ); }; - single_module!(renamer(parent.clone())); + single_item!(modules: renamer(parent.clone())); } else if is_crate(ident.clone()) { - single_module!(renamer(state.whole_crate.clone())); + single_item!(modules: renamer(state.whole_crate.clone())); } match state.subtree.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => single_module!(renamer(m)), - Some(GenericItem::Type(t)) => single_item!(aliases, t: Type), - Some(GenericItem::Struct(s)) => single_item!(structs, s: Struct), - Some(GenericItem::Union(u)) => single_item!(unions, u: Union), - Some(GenericItem::Const(c)) => single_item!(constants, c: Const), - Some(GenericItem::Fn(f)) => single_item!(foreign_functions, f: Fn), - Some(GenericItem::Static(s)) => single_item!(foreign_statics, s: Static), + Some(GenericItem::Module(m)) => single_item!(modules: renamer(m)), + Some(GenericItem::Type(t)) => renamed_single_item!(aliases, t: Type), + Some(GenericItem::Struct(s)) => renamed_single_item!(structs, s: Struct), + Some(GenericItem::Union(u)) => renamed_single_item!(unions, u: Union), + Some(GenericItem::Const(c)) => renamed_single_item!(constants, c: Const), + Some(GenericItem::Fn(f)) => renamed_single_item!(foreign_functions, f: Fn), + Some(GenericItem::Static(s)) => renamed_single_item!(foreign_statics, s: Static), None => Resolution::Unresolved, } } From 180243052bca1c43a453407a200d63f81d99bce6 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:31:08 +0200 Subject: [PATCH 059/100] chore: wip --- ctest/src/ast/module.rs | 45 ++++++++---- ctest/src/ffi_items.rs | 150 ++++++++++++++++------------------------ notes/new-algo.md | 24 ++++++- 3 files changed, 114 insertions(+), 105 deletions(-) diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index d04720e109503..c326b3a861e30 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,3 +1,6 @@ +use syn::Path; +use syn::punctuated::Punctuated; + use crate::BoxStr; use crate::ffi_items::FfiItems; @@ -7,23 +10,10 @@ use crate::ffi_items::FfiItems; pub struct Module { pub(crate) public: bool, pub(crate) cached_path: BoxStr, - pub(crate) path: syn::Path, + pub(crate) path: Path, pub(crate) items: FfiItems, } -// [NOTE]: a custom implementation is used because FfiItems does not have a -// PartialEq implementation. It is best if it does not have one. The -// current_module field in FfiItems is not meant for use post-parse-time. It -// would likely be unintuitive to have its PartialEq not take into account. -impl PartialEq for Module { - fn eq(&self, other: &Self) -> bool { - self.public == other.public - && self.cached_path == other.cached_path - && self.path == other.path - && self.items.custom_eq(&other.items) - } -} - impl Module { /// Returns the full path to the module item. /// @@ -44,3 +34,30 @@ impl Module { ident.to_string() } } + +impl Default for Module { + fn default() -> Self { + Self { + public: bool::default(), + cached_path: String::new().into_boxed_str(), + path: Path { + leading_colon: None, + segments: Punctuated::new(), + }, + items: FfiItems::default(), + } + } +} + +// [NOTE]: a custom implementation is used because FfiItems does not have a +// PartialEq implementation. It is best if it does not have one. The +// current_module field in FfiItems is not meant for use post-parse-time. It +// would likely be unintuitive to have its PartialEq not take into account. +impl PartialEq for Module { + fn eq(&self, other: &Self) -> bool { + self.public == other.public + && self.cached_path == other.cached_path + && self.path == other.path + && self.items.custom_eq(&other.items) + } +} diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 113a958765742..0450850a0154b 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -11,16 +11,23 @@ use std::ops::{ use proc_macro2::Span; use quote::ToTokens; use syn::punctuated::Punctuated; -use syn::token::Brace; +use syn::token::{ + Brace, + Mod, +}; use syn::visit::{ self, Visit, }; use syn::{ + File, Ident, + ItemMod, Path, PathArguments, PathSegment, + UseName, + UseRename, UseTree, Visibility, }; @@ -126,31 +133,26 @@ impl FfiItems { /// instance is always left with no reexports that are not sourced from /// third-party crates. pub(crate) fn visit_file(&mut self, file: &syn::File) { - let syn::File { + let File { attrs, items: mod_items, .. } = file; - let file_mod = syn::ItemMod { + let file_mod = ItemMod { attrs: attrs.clone(), vis: syn::parse_quote! { pub }, unsafety: None, - mod_token: syn::token::Mod::default(), + mod_token: Mod::default(), ident: Ident::new("__ctest_root_mod", Span::call_site()), content: Some((Brace::default(), mod_items.clone())), semi: None, }; self.visit_item_mod(&file_mod); let root_module = Module { - public: bool::default(), - cached_path: String::new().into_boxed_str(), - path: Path { - leading_colon: None, - segments: Punctuated::new(), - }, items: self.clone(), + ..Default::default() }; - *self = resolve_all(root_module).items; + *self = resolve_all(root_module.clone()).items.clone(); } /// Performs a comparison for equality between FfiItems without taking field @@ -355,7 +357,7 @@ struct PassPair { /// termination. fn resolve_all(root: Module) -> Module { let (ControlFlow::Break(m) | ControlFlow::Continue(m)) = - iter::repeat(()).try_fold(root, |root, _| { + iter::repeat(()).try_fold(root.clone(), |root, _| { // [NOTE]: it holds that a pass over the crate root will yield the // crate root as its subtree. It is thus safe to ignore the // whole-crate module tree. They are the same at this point. @@ -373,17 +375,14 @@ fn resolve_all(root: Module) -> Module { m } -/// Marks a `use` declaration and a set of items as resolved or unresolved. +/// Represents a resolved or unresolved `use` declaration. /// -/// This is the product of an attempt at resolving a given `use` declaration. It -/// is then used in [`merge_module`] to merge whichever `use` declarations -/// resolved to a set of items back into the module they were sourced from. +/// The state of resolution may change across resolution passes. It is used in +/// [`merge_module`] to merge whichever `use` declarations resolved to a set of +/// items back into the module they were sourced from. #[derive(Debug, Clone)] enum Resolution { - Resolved { - original_use: RefinedUse, - items: FfiItems, - }, + Resolved(FfiItems), Unresolved, } @@ -453,7 +452,6 @@ fn resolve_one( .into_iter() .map(|u| { resolve_use( - u.clone(), u, PassPair { subtree: root.clone(), @@ -489,20 +487,17 @@ fn resolve_one( /// The other element of the pair is the whole-crate module tree. That is used /// to resolve ancestor paths (when `super` is found) and absolute paths (when /// `crate` is found.) -fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> Resolution { +fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! single_item { ($field:ident: $it:expr) => { - return Resolution::Resolved { - original_use: original_use.clone(), - items: FfiItems { - $field: vec![$it], - ..Default::default() - }, - } + return Resolution::Resolved(FfiItems { + $field: vec![$it], + ..Default::default() + }) }; } match &r#use.tree { - RefinedUseTree::Name(syn::UseName { ident }) => { + RefinedUseTree::Name(UseName { ident }) => { // [NOTE]: self may not appear as the name basecase of a well-formed // path. And yet it is still considered here. That is because of // path normalization. self may appear as part of a tail group in a @@ -544,11 +539,8 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> None => Resolution::Unresolved, } } - RefinedUseTree::Glob => Resolution::Resolved { - original_use, - items: state.subtree.items, - }, - RefinedUseTree::Rename(syn::UseRename { ident, rename, .. }) => { + RefinedUseTree::Glob => Resolution::Resolved(state.subtree.items), + RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { macro_rules! renamed_single_item { ($field:ident , $it:tt : $ty:tt) => {{ let mut new_segments = $it.path.segments.clone(); @@ -651,13 +643,11 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> ), } }; - let new_use = RefinedUse { - is_public: r#use.is_public.clone(), - tree: *tree.clone(), - }; resolve_use( - original_use, - new_use, + RefinedUse { + is_public: r#use.is_public.clone(), + tree: *tree.clone(), + }, PassPair { subtree: new_subtree.clone(), whole_crate: state.whole_crate.clone(), @@ -669,7 +659,7 @@ fn resolve_use(original_use: RefinedUse, r#use: RefinedUse, state: PassPair) -> /// Attempts to find a given module path (`target`) within a module subtree /// (`root`). -fn get_module(target: syn::Path, root: Module) -> Option { +fn get_module(target: Path, root: Module) -> Option { if root.path == target { return root.into(); } @@ -718,7 +708,7 @@ fn get_parent_path(root: Module) -> syn::Path { p.push(s); p }); - syn::Path { + Path { leading_colon: None, segments: new_segments, } @@ -771,7 +761,7 @@ fn update_tree(src: Module, dst: Module) -> Module { /// /// The subtree root module itself also gets the transform applied to its path. /// The `cached_path` field of each item is updated accordingly. -fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> syn::Path + Clone) -> Module { +fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> Path + Clone) -> Module { let new_root_path = f(root.path.clone()); let new_root_cache = path_to_string(&new_root_path); macro_rules! map_items { @@ -836,30 +826,23 @@ fn merge_module(dst: Module, src: Vec) -> Module { let new_path = dst.path.clone(); src.into_iter() .filter_map(|r| { - if let Resolution::Resolved { - original_use, - items, - } = r - { - Some((original_use, items)) + if let Resolution::Resolved(items) = r { + Some(items) } else { None } }) .map( - |( - ou, - FfiItems { - aliases, - structs, - unions, - constants, - foreign_functions, - foreign_statics, - modules, - .. - }, - )| { + |FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + .. + }| { // [NOTE]: this sequence must have at least one segment, because // the invariant assumed in the closure for path manipulation of // the items to merge assumes so. This is true for all items to @@ -923,38 +906,25 @@ fn merge_module(dst: Module, src: Vec) -> Module { }) }) .collect(); - ( - ou, - FfiItems { - modules: new_nested_modules, - ..new_module.items - }, - ) + FfiItems { + modules: new_nested_modules, + ..new_module.items + } }, ) .fold( dst, |mut dst, - ( - ou, - FfiItems { - aliases, - structs, - unions, - constants, - foreign_functions, - foreign_statics, - modules, - .. - }, - )| { - let Some(idx) = dst.items.uses.iter().position(|u| *u == ou) else { - unreachable!( - "the use statement was sourced from the module which \ - this fold is using as seed value" - ) - }; - dst.items.uses.swap_remove(idx); + FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + .. + }| { dst.items.aliases.extend_from_slice(&aliases); dst.items.structs.extend_from_slice(&structs); dst.items.unions.extend_from_slice(&unions); diff --git a/notes/new-algo.md b/notes/new-algo.md index 261082876d95e..87286d5a17271 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -340,4 +340,26 @@ somewhere in the crate graph. That is the main use of the proposition. This could be generalized into implying a coproduct. One injection would correspond with an injective const functor. The other would correspond with the identity functor. The assumption that the module really does -exist can be left to call-site. +exist can be left to call-site. The proof for all propositions in the +revised algorithm seems complete. To humanly verify everything in it, +let use review the workings on the revised algorithm. The algorithm is +based off of the idea that repeated resolution of the module tree will +eventually converge to a snapshot of the tree that is unchanged after +performing another resolution pass. To that extent, the algorithm driver +consists solely of a use of the resolution proposition, and a recursive +comparison of the resulting tree with the pre-pass tree. If such +comparison happens to imply boolean truth, then one may safely assume +that another pass will not reveal further information. Termination thus +follows. Now onto the resolution proposition. The proof for this one is +ever so slightly more intricate; It really only has more steps. The +assumptions consist solely of a pair of a module subtree and of the +whole-crate module tree. The former is the one from which use +declarations are meant to be sourced from. The latter serves in both +potentially resolving some of those use declarations, and in updating +itself with the result of those resolutions. Such update only takes +place after the target module is itself updated. This one's update is +the merging proposition. That takes in the module where the use +declarations were sourced from, and the potentially empty set of +resolution items yield from the single-module resolution proposition. +Let that on hold for a moment. The proof for the pass proposition has +not been explained in full. From 3234a5e778b9331f7b9e974b21e5870152ac5fb2 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:36:50 +0200 Subject: [PATCH 060/100] chore: wip --- ctest/src/ffi_items.rs | 54 ++++++++++--------- notes/new-algo.md | 119 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 26 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 0450850a0154b..b52f68cfee51c 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -398,18 +398,23 @@ enum Resolution { /// may have had new items added to it. The whole-crate module tree is /// guaranteed to have integrated these changes. fn resolve_pass(src: PassPair) -> PassPair { - let (PassPair { whole_crate, .. }, resolved_children) = - src.subtree.items.modules.clone().into_iter().fold( - (src.clone(), Vec::new()), - |(PassPair { whole_crate, .. }, mut children), m| { - let pair = resolve_pass(PassPair { - subtree: m, - whole_crate: whole_crate.clone(), - }); - children.push(pair.subtree.clone()); - (pair, children) - }, - ); + let ( + PassPair { + whole_crate: whole_crate_with_resolved_children, + .. + }, + resolved_children, + ) = src.subtree.items.modules.clone().into_iter().fold( + (src.clone(), Vec::new()), + |(PassPair { whole_crate, .. }, mut children), m| { + let pair = resolve_pass(PassPair { + subtree: m, + whole_crate: whole_crate.clone(), + }); + children.push(pair.subtree.clone()); + (pair, children) + }, + ); let root_with_resolved_children = Module { items: FfiItems { modules: resolved_children, @@ -419,14 +424,14 @@ fn resolve_pass(src: PassPair) -> PassPair { }; let resolved_uses = resolve_one(PassPair { subtree: root_with_resolved_children.clone(), - whole_crate: whole_crate.clone(), + whole_crate: whole_crate_with_resolved_children.clone(), }); let new_root = merge_module(root_with_resolved_children, resolved_uses); let new_root = Module { items: new_root.items.dedup(), ..new_root }; - let new_whole_crate = update_tree(new_root.clone(), whole_crate); + let new_whole_crate = update_tree(new_root.clone(), whole_crate_with_resolved_children.clone()); PassPair { subtree: new_root, whole_crate: new_whole_crate, @@ -452,7 +457,7 @@ fn resolve_one( .into_iter() .map(|u| { resolve_use( - u, + u.clone(), PassPair { subtree: root.clone(), whole_crate: state.clone(), @@ -738,22 +743,22 @@ fn is_super(ident: syn::Ident) -> bool { /// If not found, `dst` is returned unmodified. `src` may also match `dst` /// itself. That would return `src`. fn update_tree(src: Module, dst: Module) -> Module { - if src.path == dst.path { - return src.into(); + if src.path.clone() == dst.path.clone() { + return src.clone().into(); } - let new_children = dst + let new_children: Vec<_> = dst .items .modules .clone() .into_iter() - .map(|m| update_tree(src.clone(), m)) + .map(|m| update_tree(src.clone(), m.clone())) .collect(); Module { items: FfiItems { - modules: new_children, - ..dst.items + modules: new_children.clone(), + ..dst.items.clone() }, - ..dst + ..dst.clone() } } @@ -821,7 +826,7 @@ fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> Path + Clone) -> /// /// Note that, during merging, the items to be merged may end up duplicating /// existing items on `dst`. It is call-site's responsibility to deduplicate -/// those through `FfiItems::dedup`. +/// those through [`FfiItems::dedup`]. fn merge_module(dst: Module, src: Vec) -> Module { let new_path = dst.path.clone(); src.into_iter() @@ -866,13 +871,12 @@ fn merge_module(dst: Module, src: Vec) -> Module { ..Default::default() }; let module = Module { - public: false, - cached_path: String::new().into_boxed_str(), path: syn::Path { leading_colon: None, segments: dummy_segment, }, items: items_without_modules, + ..Default::default() }; let new_module = manipulate_path(module, |p| { let Some(ident) = p.segments.last().cloned() else { diff --git a/notes/new-algo.md b/notes/new-algo.md index 87286d5a17271..dc9b6d45d7d83 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -362,4 +362,121 @@ the merging proposition. That takes in the module where the use declarations were sourced from, and the potentially empty set of resolution items yield from the single-module resolution proposition. Let that on hold for a moment. The proof for the pass proposition has -not been explained in full. +not been explained in full. This proposition will first recurse through +all child modules of the module being currently processed. That should +not be necessary to do first, but it is the more intricate part of the +proposition's proof, so we're best off getting it out of the way sooner +rather than later. The nifty part about this is that it is not enough to +simply have each of the child modules recursively resolved; Recall each +call provides proof of the stateful pair of a subtree and the +whole-crate module tree. In other words, there is need for a fold over +the list of child modules, such that the whole-crate part of the +stateful pair is aware of the result of resolving each of the child +modules. This is because, as mentioned above, each resolution pass ends +with an "updating" of the whole-crate element of the stateful pair +(though still a pure computation) carried across recursive calls to the +resolution proposition. This fold is seeded by an unlabeled product +comprising the stateful pair and the list of resolved child modules, +which starts empty. For each child module, it first calls into the same +resolution proposition as we are in right now. The input pair here is +the whole-crate module tree as received from the fold and the child +module in question, also as received from the fold. The list of child +modules is then updated with the resolved subtree implied from that use +of the single-pass proposition. The updated pair in the fold is then +comprised of this updated subtree (the child module), and of the +whole-crate module tree that was implied in the above use of the +single-pass resolution proposition. And that just about makes the more +complex part of the single-pass resolution proposition's proof. The next +steps just build up a new module with the updated child modules, and +feed that into the proposition that actually attempts to resolve the +list of use declarations in a given module, alongside the snapshot of +the whole-crate module tree obtained from the above fold. The +proposition that attempts to resolve the use declarations in a given +module is fairly simple in that it simply maps over all use declarations +in the passed subtree. The map transform is the single-use declaration +resolution proposition, which we shall explain later on. Now back to the +single-pass resolution proposition's proof. Once the list of use +declarations has yield a potentially non-empty list of resolution items, +these are to be merged back into the module where the use declarations +were sourced from. That is handed off to the merging proposition. This +proposition goes through a two-stage pipeline; It first gets rid of the +unresolved use declarations in the pass, and then goes through each of +the resolved ones to extract the set of items that the use declaration +referred to. These then get their paths modified such that they are +correctly integrated into the module into which to merge, and then the +resulting lists get appended to the existing lists of items in the +module into which to merge. That merits some further discussion, but +that will be left for later. Now back again to the single-pass +resolution proposition. We now have an updated subtree, which makes up +the first half of the stateful pair used across passes, and the only +thing left is to update the whole-crate module tree. To achieve that, we +delegate to another proposition that attempts to find some module a into +another module b, and, if found, integrates module a into module b's +subtree. Module a could very well be module b itself, in which case the +proposition would imply module a anew. Otherwise, if module a is within +some arbitrarily deep layer of the subtree rooted at module b, that one +subtree will be found and replaced with module a (which itself is a +subtree, of course.) The tree-update proposition's proof is fairly +simple in nature. It checks if the path of module b matches that of +module a and implies the latter if such is the case; Otherwise, it +attempts to map over the list of child modules in module b, where the +map transform is the same tree-update proposition. That either yields +the module untouched (if its path did not match that of module a,) or +otherwise yields module a. This is actually quite nice in that it will +halt the search+replace as soon as the module in question is found. +Granted, the whole list of child items will be processed, but that does +not seem like a huge deal. There's likely more pressing performance +bottlenecks. The updated whole-tree module tree, back up at the +single-pass resolution proposition, is now ready, and with that, the +proof is done. The implied stateful pair is comprised of the latest +snapshot of the subtree being resolved, and of a snapshot of the +whole-crate module tree that is aware of this updated subtree (for it +has just been integrated into it.) The only core proposition that +remains to be reviewed is the single-use declaration proposition. The +proof for this one is, at least at a high-level, simple to grasp. It +recursively munches through the use declaration's path. The ever so +slightly more complex parts of it are concerned with handling cases such +as a use declaration that introduces renamed items. Then there's also +the three keywords Rust accepts in paths, which either (1) introduce an +item by its crate-relative (absolute) path, or (2) introduce some +ancestor module through a super-relative path, or (3) introduce some +descendant module, if not themselves, through a self-relative path. +Those will all be explained shortly. We shall start by deconstructing +the proof for each of the cases that a use declaration may represent. A +use declaration could be a single identifier. That identifier, as parsed +by syn, could be one of the super, self or crate keywords, or could +refer to some genuine identifier that was not a Rust keyword. +Thankfully, syn parsing takes place after macro expansion, which itself +takes place after a cargo-check run (or it should, in principle,) and +use-declaration resolution takes place after syn parsing. This means we +can trust all input is well-formed input; That also means we can discard +the possibility for a single identifier use declaration to refer to the +crate keyword. No matter whether the use declaration really was made out +of a single segment, or if we got to this basecase after munching +through some larger (multi-segment) use declaration, the crate keyword +simply cannot appear at the tail end of a use declaration. The crate +keyword, as per the Rust Reference, may only appear as part of the start +of a use declaration's path. The path may not necessarily be a +multi-path segment referring to some other item, so long as the use +declaration introduces a renamed identifier for the crate keyword. That +means the crate keyword can only logically appear as either the head of +a longer path, or as a part of a renamed use declaration. That only +leaves the self and super keywords for consideration. These need some +further explanation, as the muncher proof must be ready for technically +malformed input. According to the Rust Reference, the self keyword in a +use declaration may only appear at the start or end of a use declaration +not containing any groups. The latter case, much like with the crate +keyword, requires the use declaration to introduce a renamed identifier +for the self (current) module. The nifty part about this is that these +rules apply to use declarations that do not contain a group at its tail +end. If a use declaration does contain a group, the self keyword may +appear bare (without renames) at some top-level, nested use declaration +introduced in the group. The ctest-specific parsing logic performs +"normalization" of paths in use declarations, such that if they contain +some group, this group is deconstructed into a set of use declarations. +This, in turn, means that we could very well stumble upon some use +declaration with a trailing self keyword that is not part of a nested +use declaration (of course, within a group.) Long story short; We need +to handle both the self case and the super case in the name basecase of +the single-use resolution proposition even though these would get +flagged as malformed input by the compiler. From f200494e1ab96039198de99ddf61ce41ea0640f9 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:39:27 +0200 Subject: [PATCH 061/100] chore: wip --- ctest/src/ffi_items.rs | 58 ++++++++++----------- notes/new-algo.md | 114 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 33 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index b52f68cfee51c..948709e9fd5b7 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -195,8 +195,7 @@ impl FfiItems { .clone() .into_iter() .map(GenericItem::Static); - let mut items: Vec<_> = self - .modules + self.modules .clone() .into_iter() .map(GenericItem::Module) @@ -206,21 +205,24 @@ impl FfiItems { .chain(unions) .chain(structs) .chain(aliases) - .collect(); - let key_fn = |it: &GenericItem| match it { - GenericItem::Type(t) => t.ident(), - GenericItem::Struct(s) => s.ident(), - GenericItem::Union(u) => u.ident(), - GenericItem::Const(c) => c.ident(), - GenericItem::Fn(f) => f.ident(), - GenericItem::Static(s) => s.ident(), - GenericItem::Module(m) => m.ident(), - }; - items.sort_unstable_by_key(key_fn); - let Ok(idx) = items.binary_search_by_key(&ident.to_string(), key_fn) else { - return None; - }; - items.swap_remove(idx).into() + .find(|it| { + macro_rules! cmp { + ($($var:tt),+ $(,)?) => { + match it {$( + GenericItem::$var(it) => it.ident() == ident.to_string(), + )+} + }; + } + cmp! { + Type, + Struct, + Union, + Const, + Fn, + Static, + Module, + } + }) } /// Deduplicates the items in an item container by their path. Items of @@ -544,7 +546,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { None => Resolution::Unresolved, } } - RefinedUseTree::Glob => Resolution::Resolved(state.subtree.items), + RefinedUseTree::Glob => Resolution::Resolved(state.subtree.items.clone()), RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { macro_rules! renamed_single_item { ($field:ident , $it:tt : $ty:tt) => {{ @@ -663,24 +665,16 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { } /// Attempts to find a given module path (`target`) within a module subtree -/// (`root`). +/// (`root`.) fn get_module(target: Path, root: Module) -> Option { - if root.path == target { - return root.into(); + if root.path.clone() == target.clone() { + return root.clone().into(); } - let (ControlFlow::Break(m) | ControlFlow::Continue(m)) = root - .items + root.items .modules .clone() .into_iter() - .try_fold(None, |_, m| { - if let m @ Some(_) = get_module(target.clone(), m) { - ControlFlow::Break(m) - } else { - ControlFlow::Continue(None) - } - }); - m + .find_map(|m| get_module(target.clone(), m.clone())) } /// Fetches the parent path to a given module. @@ -715,7 +709,7 @@ fn get_parent_path(root: Module) -> syn::Path { }); Path { leading_colon: None, - segments: new_segments, + segments: new_segments.clone(), } } diff --git a/notes/new-algo.md b/notes/new-algo.md index dc9b6d45d7d83..ac19f11d72f6c 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -479,4 +479,116 @@ declaration with a trailing self keyword that is not part of a nested use declaration (of course, within a group.) Long story short; We need to handle both the self case and the super case in the name basecase of the single-use resolution proposition even though these would get -flagged as malformed input by the compiler. +considered malformed input by the compiler. Let's get going then. Let us +first discuss which of the keywords or the case of a regular identifier +should come first. The former is fairly simple to check for as the +Display implementation of syn's Ident lets you pry into the "raw" token +that was parsed. That only leaves the latter case, which happens to also +be fairly straightforward. A use declaration refers to some name in the +module tree, and for all we know in this instance, that name must be +found in the current subtree of the stateful pair we keep across passes. +If the original use declaration was actually made out of a larger +fraction of path segments, it is not our responsibility (nor do we even +have the information) to track whether the stateful subtree is, indeed, +the module where we'll find the identifier in question. We decide to +handle first the case of keywords, though this would not matter much; +Rust raw identifiers do not allow any one of the above three keywords to +be used for items. The case of the self keyword means the current +stateful subtree ought be implied as-is. The case of super implies a +lookup of the parent module is in order. That is where two other +anciliary propositions come in; One to determine the parent path to some +module, and another one to search for a module path provided a module +subtree. The former's proof is fairly straightforward; A module path (or +any item's path, for that matter) is made out of segments, the last of +which is the module's identifier. It thus holds that the parent path is +the path resulting of destructuring those segments and discarding the +last one. Functional programming languages have an operation exactly for +this, but we have to make do with an iterator that takes the first n - 1 +segments of the path, where n is the number of segments in the path. Of +note is the fact that the proposition will yield a path with no segments +if the assumed module happens to live in the crate root itself. That is +because absolute paths, which are the ones stored in each parsed item, +are crate-unprepended. This means all items that appear in the crate +root have single-segment absolute paths, instead of two-segment paths +with crate prepended to them. This has thus far not caused any issues, +so it has been left untouched from initial work on the whole ctest +extension (back when the parsing facilities were extended.) The implied +path of this proposition then becomes the first assumption of the +module-fetching proposition. Its second assumption is the subtree in +which to search for that path. The proof is, again, fairly simple; The +path is matched against the passed subtree. If it matches, then we have +an answer. Otherwise, we lazily seek through the list of child modules +in search of the target path by inductively using the same proposition. +Much like the tree-update proposition's proof, whole-crate traversal +will only take place if either (1) the target path happens to match the +very last module in a left-to-right DFS, or (2) the target path matches +that of no module in the assumed subtree. This latter case merits some +further discussion. We have a notion that this proposition only ever +gets used by providing as subtree in the very first call the whole-crate +module tree. The search performed in the proof could end up yielding an +const functor injective to the unit, which would mean the search failed +to find a module with that path. We could unwrap the coproduct holding +the injection with the above const functor, but that's just assuming +risks in the proposition's proof that are best left to call-site. So +this proposition actually implies a coproduct with two injections; An +identity functor, and an injective const functor. Now back to the +single-use resolution proposition. This had been left in the super case, +where we had been getting ready a lookup of the parent module to the +current stateful subtree. That used the two propositions just reviewed +to first fetch the parent path of the current stateful module, such that +it could attempt to find that path in the whole-crate module tree. That +just about makes up the keywords subcase in the name basecase of the +overarching proposition. The next subcase is that of regular +identifiers; These need to be looked up in the stateful subtree. But +that's about it. The lookup uses the search proposition associated to an +item container, which itself is fairly straightforward. It first gathers +into a single container all items that are not use declarations, to then +sort that container, perform a binary search on it, and then index the +container with all gathered items. This smells like a performance +bottleneck. It may be a good idea to just perform a lazy linear-cost +traversal of the container with all gathered items, as we end up +incurring unnecessary costs for sorting and searching. That seems to get +the job done just fine. It is also more obvious what the proof is doing +at first glance. Now back at the single-use resolution proposition. We +had left this one at the point where the matched identifier was being +looked up as an item in the stateful subtree. If no item's identifier +matched the passed identifier, then we have no further way of attempting +to resolve the use declaration; It's unresolved. Otherwise, the matched +item (resulting from the look up) is packed in an item container. It may +now be a good idea to make a quick review of the basecases considered in +the single-use resolution proposition's proof. There are a total of +three; One is the single-name basecase we just explained above, another +one is the glob basecase, and the other one is the rename basecase. The +only recursive path through the proof is the path case, where the tail +end of the path makes up another use declaration to munch through. The +glob basecase is the simplest of them all; The item container within the +stateful subtree ought be returned as-is. Note that the item container +packed with resolved resolution items will be deconstructed in the +merging proposition's proof, and each of its fields will extend the same +field in the item container of the module into which to merge. A glob +import brings into scope all items from the module making up the head +segment of a path, so there is need to return the items of the stateful +subtree and not an item container wrapping it. This contrasts with a +path that refers to a specific module or set of modules, which must +itself be wrapped in an item container, such that items of the module(s) +themselves do not get merged, but rather the module in its entirety. The +next and last basecase in the proof is the rename case. This is +essentially the same as the name case, except we have to rewrite a bunch +of item paths. Due to their similarity, we shall skip over the entire +details of the proof, and only discuss the details of renaming. These +are fairly simple to explain for non-module items, but will require +introducing an anciliary proposition for module items. The former only +require replacing the last segment of their paths with the renamed +identifier. Modules, on the other hand, need to take into consideration +the subtree rooted in them. A module's path dictates the path of both +its items and the child modules contained within it. The child modules +and its item are also affected by the path of the parent module. This +goes on and on until we hit the leaf modules. Renaming a module thus +requires both changing its path and changing the path of all items in +the subtree rooted at that module. This is also used when rewriting item +paths in the merging proposition's proof, so it was thought best to +abstract the whole thing into a path manipulation proposition. This is +the same as that devised for the first revision of the algorithm, so we +won't go into the details of the proposition's proof. It's best if we +focus on the closure that is passed to the proposition in the rename +case of the single-use resolution proposition. From 7045a17b2365d2a160ebca29f39b490abb884084 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:40:59 +0200 Subject: [PATCH 062/100] chore: wip --- ctest/src/ffi_items.rs | 294 ++++++++++++++++++++--------------------- notes/new-algo.md | 73 +++++++++- notes/pr-answer.md | 38 ++---- 3 files changed, 224 insertions(+), 181 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 948709e9fd5b7..d1dcbad9604d4 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1,6 +1,5 @@ //! Conversion of Rust code to a simplified abstract syntax tree. -use std::borrow::Borrow; use std::iter; use std::ops::{ self, @@ -370,8 +369,8 @@ fn resolve_all(root: Module) -> Module { whole_crate: root.clone(), }); match new_root == root { - true => ControlFlow::Break(new_root), - false => ControlFlow::Continue(new_root), + true => ControlFlow::Break(new_root.clone()), + false => ControlFlow::Continue(new_root.clone()), } }); m @@ -408,35 +407,37 @@ fn resolve_pass(src: PassPair) -> PassPair { resolved_children, ) = src.subtree.items.modules.clone().into_iter().fold( (src.clone(), Vec::new()), - |(PassPair { whole_crate, .. }, mut children), m| { + |(PassPair { whole_crate, .. }, children), m| { let pair = resolve_pass(PassPair { - subtree: m, + subtree: m.clone(), whole_crate: whole_crate.clone(), }); - children.push(pair.subtree.clone()); - (pair, children) + let mut new_children = children.clone(); + new_children.push(pair.subtree.clone()); + let new_children = new_children.clone(); + (pair, new_children) }, ); let root_with_resolved_children = Module { items: FfiItems { - modules: resolved_children, - ..src.subtree.items + modules: resolved_children.clone(), + ..src.subtree.items.clone() }, - ..src.subtree + ..src.subtree.clone() }; let resolved_uses = resolve_one(PassPair { subtree: root_with_resolved_children.clone(), whole_crate: whole_crate_with_resolved_children.clone(), }); - let new_root = merge_module(root_with_resolved_children, resolved_uses); + let new_root = merge_module(root_with_resolved_children.clone(), resolved_uses.clone()); let new_root = Module { - items: new_root.items.dedup(), - ..new_root + items: new_root.items.clone().dedup(), + ..new_root.clone() }; let new_whole_crate = update_tree(new_root.clone(), whole_crate_with_resolved_children.clone()); PassPair { - subtree: new_root, - whole_crate: new_whole_crate, + subtree: new_root.clone(), + whole_crate: new_whole_crate.clone(), } } @@ -446,26 +447,15 @@ fn resolve_pass(src: PassPair) -> PassPair { /// declarations, and of the whole-crate module tree. The latter is used to /// resolve certain types of paths in `use` declarations. /// -/// See the documentation on [`resolve_pass`] for details. -fn resolve_one( - PassPair { - subtree: root, - whole_crate: state, - }: PassPair, -) -> Vec { - root.items +/// See [`resolve_pass`] for details. +fn resolve_one(state: PassPair) -> Vec { + state + .subtree + .items .uses .clone() .into_iter() - .map(|u| { - resolve_use( - u.clone(), - PassPair { - subtree: root.clone(), - whole_crate: state.clone(), - }, - ) - }) + .map(|u| resolve_use(u.clone(), state.clone())) .collect() } @@ -498,12 +488,12 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! single_item { ($field:ident: $it:expr) => { return Resolution::Resolved(FfiItems { - $field: vec![$it], + $field: vec![$it.clone()], ..Default::default() }) }; } - match &r#use.tree { + match r#use.tree.clone() { RefinedUseTree::Name(UseName { ident }) => { // [NOTE]: self may not appear as the name basecase of a well-formed // path. And yet it is still considered here. That is because of @@ -536,63 +526,60 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { single_item!(modules: parent.clone()); } match state.subtree.items.search(ident.clone()) { - Some(GenericItem::Type(t)) => single_item!(aliases: t), - Some(GenericItem::Struct(s)) => single_item!(structs: s), - Some(GenericItem::Union(u)) => single_item!(unions: u), - Some(GenericItem::Const(c)) => single_item!(constants: c), - Some(GenericItem::Fn(f)) => single_item!(foreign_functions: f), - Some(GenericItem::Static(s)) => single_item!(foreign_statics: s), - Some(GenericItem::Module(m)) => single_item!(modules: m), + Some(GenericItem::Type(t)) => single_item!(aliases: t.clone()), + Some(GenericItem::Struct(s)) => single_item!(structs: s.clone()), + Some(GenericItem::Union(u)) => single_item!(unions: u.clone()), + Some(GenericItem::Const(c)) => single_item!(constants: c.clone()), + Some(GenericItem::Fn(f)) => single_item!(foreign_functions: f.clone()), + Some(GenericItem::Static(s)) => single_item!(foreign_statics: s.clone()), + Some(GenericItem::Module(m)) => single_item!(modules: m.clone()), None => Resolution::Unresolved, } } RefinedUseTree::Glob => Resolution::Resolved(state.subtree.items.clone()), RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { - macro_rules! renamed_single_item { - ($field:ident , $it:tt : $ty:tt) => {{ + macro_rules! new_path { + ($it:expr) => {{ let mut new_segments = $it.path.segments.clone(); new_segments.pop(); new_segments.push(PathSegment { ident: rename.clone(), arguments: PathArguments::None, }); - let new_path = Path { - segments: new_segments, - ..$it.path - }; + let new_segments = new_segments.clone(); + Path { + segments: new_segments.clone(), + ..$it.path.clone() + } + }}; + } + macro_rules! renamed_single_item { + ($field:ident , $ty:tt : $it:expr) => {{ + let new_path = new_path!($it.clone()); let new_item = $ty { - cached_path: path_to_string(&new_path), - path: new_path, - ..$it + cached_path: path_to_string(new_path.clone()), + path: new_path.clone(), + ..$it.clone() }; - single_item!($field: new_item); + single_item!($field: new_item.clone()); }}; } let renamer = |m: Module| { - let new_path = { - let mut base_path_segments = m.path.segments.clone(); - base_path_segments.pop(); - base_path_segments.push(PathSegment { - ident: rename.clone(), - arguments: PathArguments::None, - }); - Path { - segments: base_path_segments, - ..m.path - } - }; + let new_path = new_path!(m.clone()); let new_path_len = new_path.segments.len(); - manipulate_path(m, |p| { + manipulate_path(m.clone(), |p| { let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( new_path.clone(), - |mut p, s| { + |p, s| { + let mut p = p.clone(); p.segments.push(s); + let p = p.clone(); p }, ); Path { leading_colon: p.leading_colon, - ..new_path + ..new_path.clone() } }) }; @@ -612,13 +599,15 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { single_item!(modules: renamer(state.whole_crate.clone())); } match state.subtree.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => single_item!(modules: renamer(m)), - Some(GenericItem::Type(t)) => renamed_single_item!(aliases, t: Type), - Some(GenericItem::Struct(s)) => renamed_single_item!(structs, s: Struct), - Some(GenericItem::Union(u)) => renamed_single_item!(unions, u: Union), - Some(GenericItem::Const(c)) => renamed_single_item!(constants, c: Const), - Some(GenericItem::Fn(f)) => renamed_single_item!(foreign_functions, f: Fn), - Some(GenericItem::Static(s)) => renamed_single_item!(foreign_statics, s: Static), + Some(GenericItem::Module(m)) => single_item!(modules: renamer(m.clone())), + Some(GenericItem::Type(t)) => renamed_single_item!(aliases, Type: t.clone()), + Some(GenericItem::Struct(s)) => renamed_single_item!(structs, Struct: s.clone()), + Some(GenericItem::Union(u)) => renamed_single_item!(unions, Union: u.clone()), + Some(GenericItem::Const(c)) => renamed_single_item!(constants, Const: c.clone()), + Some(GenericItem::Fn(f)) => renamed_single_item!(foreign_functions, Fn: f.clone()), + Some(GenericItem::Static(s)) => { + renamed_single_item!(foreign_statics, Static: s.clone()) + } None => Resolution::Unresolved, } } @@ -635,29 +624,23 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { applies here." ); }; - parent + parent.clone() } else if is_crate(ident.clone()) { state.whole_crate.clone() } else { match state.subtree.items.search(ident.clone()) { Some(GenericItem::Module(m)) => m.clone(), - None => return Resolution::Unresolved, - Some(_) => unreachable!( - "Paths parsed in ctest do not include enum variants. \ - Update this if support for enum variants is added. \ - Multi-segment import paths may then refer to items \ - other than modules." - ), + Some(_) | None => return Resolution::Unresolved, } }; resolve_use( RefinedUse { - is_public: r#use.is_public.clone(), + is_public: r#use.is_public, tree: *tree.clone(), }, PassPair { subtree: new_subtree.clone(), - whole_crate: state.whole_crate.clone(), + ..state.clone() }, ) } @@ -667,7 +650,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { /// Attempts to find a given module path (`target`) within a module subtree /// (`root`.) fn get_module(target: Path, root: Module) -> Option { - if root.path.clone() == target.clone() { + if root.path == target { return root.clone().into(); } root.items @@ -703,8 +686,10 @@ fn get_parent_path(root: Module) -> syn::Path { .clone() .into_iter() .take(root.path.segments.len().saturating_sub(1)) - .fold(Punctuated::new(), |mut p, s| { + .fold(Punctuated::new(), |p, s| { + let mut p = p.clone(); p.push(s); + let p = p.clone(); p }); Path { @@ -737,7 +722,7 @@ fn is_super(ident: syn::Ident) -> bool { /// If not found, `dst` is returned unmodified. `src` may also match `dst` /// itself. That would return `src`. fn update_tree(src: Module, dst: Module) -> Module { - if src.path.clone() == dst.path.clone() { + if src.path == dst.path { return src.clone().into(); } let new_children: Vec<_> = dst @@ -762,7 +747,7 @@ fn update_tree(src: Module, dst: Module) -> Module { /// The `cached_path` field of each item is updated accordingly. fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> Path + Clone) -> Module { let new_root_path = f(root.path.clone()); - let new_root_cache = path_to_string(&new_root_path); + let new_root_cache = path_to_string(new_root_path.clone()); macro_rules! map_items { ($field:ident: $ty:tt) => {{ root.items @@ -770,12 +755,12 @@ fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> Path + Clone) -> .clone() .into_iter() .map(|it| { - let new_path = f(it.path); - let new_cache = path_to_string(&new_path); + let new_path = f(it.path.clone()); + let new_cache = path_to_string(new_path.clone()); $ty { - cached_path: new_cache, - path: new_path, - ..it + cached_path: new_cache.clone(), + path: new_path.clone(), + ..it.clone() } }) .collect::>() @@ -792,23 +777,23 @@ fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> Path + Clone) -> .modules .clone() .into_iter() - .map(|m| manipulate_path(m, f.clone())) + .map(|m| manipulate_path(m.clone(), f.clone())) .collect(); let new_items = FfiItems { - aliases: new_aliases, - structs: new_structs, - unions: new_unions, - constants: new_constants, - foreign_functions: new_foreign_functions, - foreign_statics: new_foreign_statics, - modules: new_modules, + aliases: new_aliases.clone(), + structs: new_structs.clone(), + unions: new_unions.clone(), + constants: new_constants.clone(), + foreign_functions: new_foreign_functions.clone(), + foreign_statics: new_foreign_statics.clone(), + modules: new_modules.clone(), ..Default::default() }; Module { - cached_path: new_root_cache, - path: new_root_path, - items: new_items, - ..root + cached_path: new_root_cache.clone(), + path: new_root_path.clone(), + items: new_items.clone(), + ..root.clone() } } @@ -823,10 +808,11 @@ fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> Path + Clone) -> /// those through [`FfiItems::dedup`]. fn merge_module(dst: Module, src: Vec) -> Module { let new_path = dst.path.clone(); - src.into_iter() + src.clone() + .into_iter() .filter_map(|r| { - if let Resolution::Resolved(items) = r { - Some(items) + if let Resolution::Resolved(items) = r.clone() { + Some(items.clone()) } else { None } @@ -855,64 +841,69 @@ fn merge_module(dst: Module, src: Vec) -> Module { ident: Ident::new("dummy", Span::call_site()), arguments: PathArguments::None, }); + let dummy_segment = dummy_segment.clone(); let items_without_modules = FfiItems { - aliases, - structs, - unions, - constants, - foreign_functions, - foreign_statics, + aliases: aliases.clone(), + structs: structs.clone(), + unions: unions.clone(), + constants: constants.clone(), + foreign_functions: foreign_functions.clone(), + foreign_statics: foreign_statics.clone(), ..Default::default() }; let module = Module { - path: syn::Path { + path: Path { leading_colon: None, - segments: dummy_segment, + segments: dummy_segment.clone(), }, - items: items_without_modules, + items: items_without_modules.clone(), ..Default::default() }; - let new_module = manipulate_path(module, |p| { + let new_module = manipulate_path(module.clone(), |p| { let Some(ident) = p.segments.last().cloned() else { unreachable!( - "all parsed items have at least one segment in \ - their paths" + "All parsed items have at least one segment in \ + their paths." ) }; let mut new_path = new_path.clone(); new_path.segments.push(ident); - new_path + let new_path = new_path.clone(); + new_path.clone() }); let new_nested_modules: Vec<_> = modules + .clone() .into_iter() .map(|m| { let Some(base_path) = m.path.segments.last().cloned() else { unreachable!( - "all items post-parsing have at least one \ - segment in their paths" + "All items post-parsing have at least one \ + segment in their paths." ); }; - manipulate_path(m, |p| { + manipulate_path(m.clone(), |p| { p.segments .clone() .into_iter() .skip_while(|s| *s != base_path) - .fold(new_path.clone(), |mut p, s| { + .fold(new_path.clone(), |p, s| { + let mut p = p.clone(); p.segments.push(s); - p + let p = p.clone(); + p.clone() }) }) }) .collect(); FfiItems { - modules: new_nested_modules, - ..new_module.items + modules: new_nested_modules.clone(), + ..new_module.items.clone() } }, ) .fold( - dst, - |mut dst, + dst.clone(), + |dst, FfiItems { aliases, structs, @@ -923,6 +914,7 @@ fn merge_module(dst: Module, src: Vec) -> Module { modules, .. }| { + let mut dst = dst.clone(); dst.items.aliases.extend_from_slice(&aliases); dst.items.structs.extend_from_slice(&structs); dst.items.unions.extend_from_slice(&unions); @@ -934,7 +926,8 @@ fn merge_module(dst: Module, src: Vec) -> Module { .foreign_statics .extend_from_slice(&foreign_statics); dst.items.modules.extend_from_slice(&modules); - dst + let dst = dst.clone(); + dst.clone() }, ) } @@ -943,9 +936,7 @@ fn merge_module(dst: Module, src: Vec) -> Module { /// 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(); +fn append_path(base: Path, new: Ident) -> Path { if base.segments.is_empty() { syn::parse_quote! { #new } } else { @@ -955,9 +946,8 @@ fn append_path(base: impl Borrow, new: impl Borrow) -> sy /// 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() +fn path_to_string(path: Path) -> BoxStr { + path.into_token_stream() .to_string() .replace(|c: char| c.is_ascii_whitespace(), "") .into_boxed_str() @@ -1018,8 +1008,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 path = append_path(&table.current_module, &i.sig.ident); - let cached_path = path_to_string(&path); + let path = append_path(table.current_module.clone(), i.sig.ident.clone()); + let cached_path = path_to_string(path.clone()); let parameters = i .sig .inputs @@ -1065,8 +1055,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 path = append_path(&table.current_module, &i.ident); - let cached_path = path_to_string(&path); + let path = append_path(table.current_module.clone(), i.ident.clone()); + let cached_path = path_to_string(path.clone()); let ty = i.ty.deref().clone(); let link_name = extract_single_link_name(&i.attrs); @@ -1082,8 +1072,8 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { let public = is_visible(&i.vis); - let path = append_path(&table.current_module, &i.ident); - let cached_path = path_to_string(&path); + let path = append_path(table.current_module.clone(), i.ident.clone()); + let cached_path = path_to_string(path.clone()); let ty = i.ty.deref().clone(); table.aliases.push(Type { @@ -1096,8 +1086,8 @@ fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { let public = is_visible(&i.vis); - let path = append_path(&table.current_module, &i.ident); - let cached_path = path_to_string(&path); + let path = append_path(table.current_module.clone(), i.ident.clone()); + let cached_path = path_to_string(path.clone()); let fields = match &i.fields { syn::Fields::Named(fields) => collect_fields(&fields.named), syn::Fields::Unnamed(fields) => collect_fields(&fields.unnamed), @@ -1114,8 +1104,8 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { let public = is_visible(&i.vis); - let path = append_path(&table.current_module, &i.ident); - let cached_path = path_to_string(&path); + let path = append_path(table.current_module.clone(), i.ident.clone()); + let cached_path = path_to_string(path.clone()); let fields = collect_fields(&i.fields.named); table.unions.push(Union { @@ -1128,8 +1118,8 @@ fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { let public = is_visible(&i.vis); - let path = append_path(&table.current_module, &i.ident); - let cached_path = path_to_string(&path); + let path = append_path(table.current_module.clone(), i.ident.clone()); + let cached_path = path_to_string(path.clone()); let ty = i.ty.deref().clone(); table.constants.push(Const { @@ -1172,8 +1162,8 @@ impl<'ast> Visit<'ast> for FfiItems { } = i else { unreachable!( - "this runs post cargo-expand, which inlines all \ - modules" + "This runs post cargo-expand, which inlines all \ + modules." ); }; let is_crate_root = ident == "__ctest_root_mod"; @@ -1181,7 +1171,7 @@ impl<'ast> Visit<'ast> for FfiItems { self.clone() } else { let out = FfiItems { - current_module: append_path(&self.current_module, ident), + current_module: append_path(self.current_module.clone(), ident.clone()), ..Default::default() }; out @@ -1230,7 +1220,7 @@ impl<'ast> Visit<'ast> for FfiItems { } else { self.modules.push(Module { public: matches!(vis, Visibility::Public(_)), - cached_path: path_to_string(&running_items.current_module), + cached_path: path_to_string(running_items.current_module.clone()), path: running_items.current_module.clone(), items: running_items, }); diff --git a/notes/new-algo.md b/notes/new-algo.md index ac19f11d72f6c..73a3e83742656 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -591,4 +591,75 @@ abstract the whole thing into a path manipulation proposition. This is the same as that devised for the first revision of the algorithm, so we won't go into the details of the proposition's proof. It's best if we focus on the closure that is passed to the proposition in the rename -case of the single-use resolution proposition. +case of the single-use resolution proposition. The first cases to +consider are the keywords. In this instance, it is completely legal to +find any one of the three keywords that are expected in use declartion +paths. The treatment itself is the same as that in the regular name +basecase of the proposition. The only difference is in the fact each of +the implied modules (the current stateful subtree, or the parent to the +statful subtree, or the crate-root module) require having their paths +and the paths of all items in the tree rooted at them changed. The +renaming consists of first renaming the module in question by changing +the last segment of its path, and then using the path manipulation +proposition mentioned above. The closure passed to the path manipulation +proposition discards as many segments off of of the item's path as the +new length of the renamed tree root contains, and yields the rest. These +are then prepended the new root module's path. An example is the path +std::any getting renamed to std::dynamic. Items such as std::any::TypeId +discard as many segments from their left as the length of the tree root +(here std::any) contains. That leaves std::any::TypeId's path with a +single segment, TypeId. This segment then gets prepended the renamed +path of the tree root, such that it ends up as std::dynamic::TypeId. Of +note is that the path manipulation proposition will apply the transform +to both the items in the given module tree, as well as the module tree +root itself. The above strategy still works when applied to the tree +root, because all segments of its path will be discarded, and the new +path would be prepended to an empty path, yielding the new path anew. +This works thanks to the fact path segments make up a semigroup and a +monoid functor. The neutral element is a list of empty segments, and the +append operation is intuitive enough. The details of the implementation +have been changed to reflect a less verbose approach that does not +repeat the same path manipulation logic for the top-level module. This +applies in much the same way to other non-module items, which implies +one need not repeat the same logic for both non-modules and modules. +This now lives as a macro within the scope of the renamed case of the +single-use resolution proposition. With that, a review of this case +seems done, and the only case left is the inductive path case. This one +behaves in much the same way as one would expect from a functional +approach to a right fold proof. The head of the path is extracted, and +first matched against the set of keywords we conceive possible in use +declarations. Each of these will imply a new stateful subtree that will +be paired up anew with the stateful whole-crate module tree when +inductively using the use-declaration proposition with the tail end of +the path. The self case is, again, fairly straightforward; The same +stateful subtree as carried thus far is implied as the stateful subtree +for the rest of the path. The super case requires performing the same +parent module look up as explained in prior cases of the single-use +resolution proposition. The crate case simply yields the stateful +whole-crate module tree, which makes the next use of this proposition +have as stateful pair the same elements (for the subtree is now +structurally equal to the whole-crate module tree.) The subcase for +items is, again, much the same as in other cases of the single-use +resolution proposition. A look up is performed on the stateful subtree +and only if a module is found does use-declaration munching continue. +The case of any other item is made to crash the whole thing because that +is simply impossible; Except this could potentially be incorrect. ctest +parses all use declarations in a crate. This brings with it both +resolvable and unresolvable use declarations. Thus far, it had been +assumed that the latter could only consist of use declarations that +refer to items from third-party crates. But this may not be entirely +correct. Users have the entire, unrestricted power of the Rust module +system in their hands. They could very well have use declarations that +at first seem like they are resolvable, for they refer to an item we can +find in the crate root, but that refer to a glob import of the variants +of an enumeration. The unconceivable subsubcase explained above would +make the whole thing crash instead of implying an unresolved path. On +second thought, there is no way the look up yields an item and the item +turns out to be an enumeration because enumerations don't even get +parsed in the first place. This implies there is just no way an item +gets returned from the path lookup without it being a module. Still, +it's best if we imply an unresolved resolution instead of crashing, +considering the effects of such implication simply mean no other item +will get merged into the module where the use declarations are currently +being sourced from. That should do it for the single-use resolution +proposition, which means the review is technically done. diff --git a/notes/pr-answer.md b/notes/pr-answer.md index 033bb67d007a0..25f51ec7a0e85 100644 --- a/notes/pr-answer.md +++ b/notes/pr-answer.md @@ -1,30 +1,12 @@ ->This PR broke by removing PowerPC ->support, which has been in `libc` since 2020 when it was added and ->documented as being used for the Wii in -> +>Is there any fallout from this? It is technically correct by the rule +>that pointers are not `Send`/`Sync`, so I'm inclined to leave it. +>Unfortunately these auto traits are a bit fragile for `libc` types in +>general since the occasional addition of a pointer field does happen. > ->Why was that done in this PR and how was it concluded that PowerPC ->wasn't in use? - -The reason is in the patch message. But "Rust target" may have been too -broad a term. Sorry for that. The change meant no in-tree Rust target in -upstream `rust-lang/rust`. - -The PR merged after the new usage guidelines merged. These indicate in -[^1] the current support policy. That mentions `rust-lang/libc` support -depends on Rust support in `rust-lang/rust`. - -libc takes the liberty to remove other kinds of targets. This change was -accepted in the PR. Whether libc also reserves the right to maintain -support is unknown. Though @tgross35 or @JohnTitor may think otherwise. - -There's also another argument to be made for out-of-tree targets. The -new usage guidelines point out in [^2] the API breakage for in-tree -tiered targets. Almost anything goes for tier 3. - -There's tier 3 targets where `core` fails to build. It would seem like -truly _anything_ goes for out-of-tree targets. Full breakage seems like -a feasible expectation. +>> 1. Maybe a commit history rewrite would be in order. +> +>Unless some exceptional circumstance happens, there is no rewriting +>history. -[^1]: -[^2]: +Then maybe adding some comments about this to the usage guidelines would +do? From 03755ad83a03bb8be0908d06c4ea0f6408d248af Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:02:16 +0200 Subject: [PATCH 063/100] chore: wip --- ctest/src/ast/module.rs | 20 +++++- ctest/src/ffi_items.rs | 156 +++++++++++++++++++++++++++++++--------- notes/new-algo.md | 89 ++++++++++++++++++++++- notes/pr-answer.md | 24 ++++--- 4 files changed, 241 insertions(+), 48 deletions(-) diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index c326b3a861e30..b807a7561b8ae 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,5 +1,10 @@ -use syn::Path; +use std::cmp::Ordering; + use syn::punctuated::Punctuated; +use syn::{ + Path, + PathSegment, +}; use crate::BoxStr; use crate::ffi_items::FfiItems; @@ -28,11 +33,22 @@ impl Module { /// 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 { + let Some(PathSegment { ident, .. }) = self.path.segments.last() else { unreachable!("all parsed items have at least one element in their path") }; ident.to_string() } + + /// Comparison function for use when sorting modules in [`FfiItems::dedup`]. + /// + /// This is not implemented as part of [`Ord`] because the implementation is + /// inconsistent with the [`PartialEq`] implementation. + pub(crate) fn custom_cmp(&self, other: &Self) -> Ordering { + match self.cached_path.cmp(&other.cached_path) { + Ordering::Equal => self.items.custom_cmp(&other.items), + other => other, + } + } } impl Default for Module { diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index d1dcbad9604d4..4c890107f806d 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1,5 +1,6 @@ //! Conversion of Rust code to a simplified abstract syntax tree. +use std::cmp::Ordering; use std::iter; use std::ops::{ self, @@ -21,7 +22,9 @@ use syn::visit::{ use syn::{ File, Ident, + Item, ItemMod, + ItemUse, Path, PathArguments, PathSegment, @@ -174,6 +177,31 @@ impl FfiItems { && self.modules == other.modules } + /// Comparison function for use when sorting and deduplicating modules in + /// [`FfiItems::dedup`]. + /// + /// This is used in [`Module::custom_cmp`]. It trusts that the modules to + /// compare have the same overall shape, and one will be a "natural" + /// extension of the other. + /// + /// We consider extension here to mean that one module subtree is based on + /// the other, and thus only contains both the items from the other and + /// possibly more. + pub(super) fn custom_cmp(&self, other: &Self) -> Ordering { + let (ours, theirs) = (self.modules.len(), other.modules.len()); + match ours.cmp(&theirs) { + Ordering::Equal => self + .modules + .clone() + .into_iter() + .zip(other.modules.clone()) + .fold(Ordering::Equal, |_, (our_m, their_m)| { + our_m.items.custom_cmp(&their_m.items) + }), + other => other.reverse(), + } + } + /// Searches for an item in the container, and returns an owned instance of /// that item. /// @@ -230,26 +258,42 @@ impl FfiItems { /// /// A consequence of calling this method is that the items are also sorted /// by their paths. - fn dedup(mut self) -> FfiItems { + fn dedup(self) -> FfiItems { macro_rules! key_fn { () => { |it| it.cached_path.clone() }; } macro_rules! dedup { - ($it:ident) => {{ - self.$it.sort_by_key(key_fn!()); - self.$it.dedup_by_key(key_fn!()); + ($($it:tt),+ $(,)?) => {{ + let items = self.clone(); + $( + let mut $it = self.$it.clone(); + $it.sort_unstable_by_key(key_fn!()); + $it.dedup_by_key(key_fn!()); + let items = FfiItems { + $it: $it.clone(), + ..items.clone() + }; + )+ + items }}; } - dedup!(aliases); - dedup!(structs); - dedup!(unions); - dedup!(constants); - dedup!(foreign_functions); - dedup!(foreign_statics); - dedup!(modules); - self + let new_items = dedup!( + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + ); + let mut new_modules = self.modules.clone(); + new_modules.sort_unstable_by(Module::custom_cmp); + new_modules.dedup_by(|lhs, rhs| lhs.path == rhs.path); + FfiItems { + modules: new_modules.clone(), + ..new_items.clone() + } } } @@ -871,7 +915,7 @@ fn merge_module(dst: Module, src: Vec) -> Module { let new_path = new_path.clone(); new_path.clone() }); - let new_nested_modules: Vec<_> = modules + let nested_modules_with_new_paths: Vec<_> = modules .clone() .into_iter() .map(|m| { @@ -895,6 +939,32 @@ fn merge_module(dst: Module, src: Vec) -> Module { }) }) .collect(); + fn remove_uses(m: Module) -> Module { + let new_modules: Vec<_> = m + .items + .modules + .clone() + .into_iter() + .map(remove_uses) + .collect(); + Module { + items: FfiItems { + uses: vec![], + modules: new_modules.clone(), + ..m.items.clone() + }, + ..m.clone() + } + } + // [NOTE]: this removes all use declarations in all descendant + // modules of the subtrees to merge because otherwise merged + // modules can increase the number of use declarations in the + // crate's module tree, which causes non-termination. + let new_nested_modules: Vec<_> = nested_modules_with_new_paths + .clone() + .into_iter() + .map(remove_uses) + .collect(); FfiItems { modules: new_nested_modules.clone(), ..new_module.items.clone() @@ -1153,13 +1223,13 @@ fn visit_item_foreign_mod(table: &mut FfiItems, i: &syn::ItemForeignMod) { } impl<'ast> Visit<'ast> for FfiItems { - fn visit_item_mod(&mut self, i: &'ast syn::ItemMod) { - let syn::ItemMod { + fn visit_item_mod(&mut self, i: &'ast ItemMod) { + let ItemMod { vis, ident, content: Some((_, mod_items)), .. - } = i + } = i.clone() else { unreachable!( "This runs post cargo-expand, which inlines all \ @@ -1167,43 +1237,43 @@ impl<'ast> Visit<'ast> for FfiItems { ); }; let is_crate_root = ident == "__ctest_root_mod"; - let mut running_items = if is_crate_root { + let running_items = if is_crate_root { self.clone() } else { - let out = FfiItems { + FfiItems { current_module: append_path(self.current_module.clone(), ident.clone()), ..Default::default() - }; - out + } }; + let mut running_items = running_items.clone(); let uses: Vec<_> = mod_items .iter() .cloned() .filter_map(|it| match it { - syn::Item::Type(t) => { + Item::Type(t) => { visit_item_type(&mut running_items, &t); None } - syn::Item::Struct(s) => { + Item::Struct(s) => { visit_item_struct(&mut running_items, &s); None } - syn::Item::Union(u) => { + Item::Union(u) => { visit_item_union(&mut running_items, &u); None } - syn::Item::Const(c) => { + Item::Const(c) => { visit_item_const(&mut running_items, &c); None } - syn::Item::ForeignMod(m) => { + Item::ForeignMod(m) => { visit_item_foreign_mod(&mut running_items, &m); None } - syn::Item::Use(syn::ItemUse { vis, tree, .. }) => normalize_path(tree) + Item::Use(ItemUse { vis, tree, .. }) => normalize_path(tree) .into_iter() .map(move |tree| RefinedUse { - is_public: matches!(vis, syn::Visibility::Public(_)), + is_public: matches!(vis, Visibility::Public(_)), tree, }) .into(), @@ -1211,18 +1281,17 @@ impl<'ast> Visit<'ast> for FfiItems { }) .flatten() .collect(); - visit::visit_item_mod(&mut running_items, i); - running_items.uses = uses; - + running_items.uses = uses.clone(); + let running_items = running_items.clone(); if is_crate_root { - *self = running_items; + *self = running_items.clone(); } else { self.modules.push(Module { public: matches!(vis, Visibility::Public(_)), cached_path: path_to_string(running_items.current_module.clone()), path: running_items.current_module.clone(), - items: running_items, + items: running_items.clone(), }); } } @@ -1248,7 +1317,7 @@ mod test1 { mod test3 { pub struct Foo; - fn failure() { + fn foo() { extern "C" { fn ctime(); } @@ -1257,8 +1326,27 @@ mod test1 { } } "#; - let mut items = FfiItems::default(); let file = syn::parse_file(source).unwrap(); + let items = FfiItems::default(); + let mut items = items.clone(); + items.visit_file(&file); + let items = items.clone(); + println!("{:#?}", items); +} + +#[test] +fn tmp1() { + let source = r#" +use test1::*; + +pub struct Foo; + +mod test1 { use super::*; } + "#; + let file = syn::parse_file(source).unwrap(); + let items = FfiItems::default(); + let mut items = items.clone(); items.visit_file(&file); + let items = items.clone(); println!("{:#?}", items); } diff --git a/notes/new-algo.md b/notes/new-algo.md index 73a3e83742656..792e21ac6b8b0 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -662,4 +662,91 @@ it's best if we imply an unresolved resolution instead of crashing, considering the effects of such implication simply mean no other item will get merged into the module where the use declarations are currently being sourced from. That should do it for the single-use resolution -proposition, which means the review is technically done. +proposition, which means the review is technically done. A larger +surface area of the code has been made pure, which should hopefully +avoid issues unrelated to the core logic. Before testing things out, it +would be best to attempt to reason through the high level idea of the +algorithm. We're trying to resolve use declarations, which here means +that as many use declarations as possible should be resolvable. To that +extent, the algorith performs crate-wide resolution passes until the +state of the post-pass crate module tree is structurally equal to that +of the pre-pass state. There is no difference between procedures across +passes, and unlike the first revision of the algorithm, the logic +depends not on the removal of use declarations after each pass. Each +pass first bottoms out on the module tree, and actually starts resolving +use declarations from the leaf modules up. After a complete pass over +the use declrations of a given module are finished, the resolution items +are merged into that source module. Then this module is integrated into +the crate root that is used to perform resolution of absolute paths and +super-relative paths during use declaration resolution. This repeats +itself with all modules in the crate's module tree, each one receving an +updated snapshot of the whole-crate module tree, which is used when +attempting to resolve its own set of use declarations. This idea works +because no matter how may times a single use declaration is resolved, +the set of items that are effectively merged into the module where the +use declaration was sourced from always has its list of items +deduplicated by their paths. Path rewriting happens during merging, +right before the items become part of the module into which to merge. It +seems like the logic is solid. Time to test things out. It would seem +the latest test worked out just fine. Now onto testing out the new +functionality. Resolution enters an infinite loop when testing with a +super-relative use declaration. The test ought be first minimized to +something that really only causes the loop. So the issue does not happen +in a very simplified case. That means there must be some edge case that +is causing non-termination of the iterations over the resolution pass. +The failing test had multiple cycles in the graph where use declarations +are edges, modules are nodes and the direction of an edge is given by +the module where the referred-to item(s) are. I think I can start +guessing why the test fails, and it may very well be due to cyclic use +declarations. Suppose we have a contrived example of a crate root with +one child module, where the child module contains a super-relative glob +import, and the crate root contains an glob import of the child module's +items. As per the current algorithm, the child module will be resolved +first. The use declaration will be resolvable, and all items in the +crate root will be imported over to the child module. This means the +child module will contain itself. Then comes the root module. The one +and only use declaration within it is also going to be resolved quite +easily. After one pass, diffing notices there has been progress and so +it decides to perform another pass. The issue comes from the fact we +just imported into the list of child modules of both the crate root and +the leaf module the dual module with its list of use declarations. In +the next resolution pass, there will be more use declarations to resolve +than there were before, including twice as many as there were before. +The invariant about the number of use declarations being fixed is thus +broken. This also hints at the fact deduplication is not good enough. +Two items with the same paths getting removed at random does not satisfy +the needs of the algorithm. For non-module items, this gets the job done +just fine. For modules, though, it becomes a necessity to keep, among +duplicate modules, the one module that currently has the largest subtree +rooted at it. This should hold because use declaration resolution always +expands or leaves unmodified a module tree. This discussion has thus +revealed that there is need to modify two things; The handling of merged +modules, and the deduplication proposition. During mering, the subtrees +rooted at the modules to merge should be stripped of all use +declarations. That is likely to require an anciliary proposition. The +removal of use declarations at merge-time is done. It happens right +after the modules to merge get their paths updated, and right before +they are about to become part of the module into which to merge. Now +comes deduplication, where modules need special attention. Sorting them +is likely going to be necessary, so that is going to be left in. Though +sorting based on the path will not be enough. The path should continue +being the primary way of sorting, but upon hitting some two modules +whose paths turn out to compare equal, we should further compare the +sizes of the subtrees, and invert the resulting ordering. This is so +that the largest subtree appears first, and we can use the same +comparison closure for the deduplication proposition (which keeps only +the first item of a run of consecutive items.) The logic seems done but +it may be potentially faulty. The comparison logic first considers the +paths of the modules, after which it recursively compares the subtrees +rooted at them. The issue may come from the fact that the subtree +recursion calls back again into the proposition for the top-level +modules. This means that if some two modules have the same path, and yet +post-resolution one of them got a new child module, but this one's path +happened to compare lexicographically smaller than some other +pre-existing module, then the module we'd stick with first would end up +being the older snapshot. Something else that doesn't quite strike me as +correct is the comparison used to determine equality among modules durin +deduplication. This can not be the same as the one used when sorting the +list of modules, because we need all modules whose paths compare equal +to be considered for removal; Only the first one among such set, which +is the first thanks to the other comparison logic, will remain. diff --git a/notes/pr-answer.md b/notes/pr-answer.md index 25f51ec7a0e85..317fac08536a9 100644 --- a/notes/pr-answer.md +++ b/notes/pr-answer.md @@ -1,12 +1,14 @@ ->Is there any fallout from this? It is technically correct by the rule ->that pointers are not `Send`/`Sync`, so I'm inclined to leave it. ->Unfortunately these auto traits are a bit fragile for `libc` types in ->general since the occasional addition of a pointer field does happen. -> ->> 1. Maybe a commit history rewrite would be in order. -> ->Unless some exceptional circumstance happens, there is no rewriting ->history. +>Like what? The rules for how `Send` and `Sync` are automatically +>implemented are pretty clear, and docs already indicate whether a type +>implements these or not. -Then maybe adding some comments about this to the usage guidelines would -do? +Sure it may be in the docs, but the section of rustdoc on blanket impls +seems to me to not be nearly as often looked at as other impls. + +It would thus seem that libc users would likely not realize until they +got a compile error. Granted, that already serves as a "fix," so it may +just be enough. + +The usage guidelines could mention that we may sweep in some such +changes across non-breaking SemVer releases. Then folks would be more +actively on the look out of types becoming `!Send`. From 8d2e81d964a530777cf8d21e85c5b1207498d9f0 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:16:02 +0200 Subject: [PATCH 064/100] chore: wip --- ctest/src/ffi_items.rs | 49 +++++++++++++++++++++++++------- notes/new-algo.md | 63 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 4c890107f806d..dbd96050b16cd 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -184,11 +184,17 @@ impl FfiItems { /// compare have the same overall shape, and one will be a "natural" /// extension of the other. /// - /// We consider extension here to mean that one module subtree is based on + /// We consider "extension" here to mean that one module subtree is based on /// the other, and thus only contains both the items from the other and - /// possibly more. + /// possibly some more items. pub(super) fn custom_cmp(&self, other: &Self) -> Ordering { let (ours, theirs) = (self.modules.len(), other.modules.len()); + // [NOTE]: the goal here is to have modules with "larger" subtrees + // appear first in the item container, so declaring them to be "smaller" + // is required for slice::sort_unstable_by to put them first in the + // sorted list of modules in FfiItems::dedup. The same applies the other + // way around with "smaller" subtrees that we would like to have appear + // later in the sorted list of modules. match ours.cmp(&theirs) { Ordering::Equal => self .modules @@ -196,6 +202,11 @@ impl FfiItems { .into_iter() .zip(other.modules.clone()) .fold(Ordering::Equal, |_, (our_m, their_m)| { + // [NOTE]: this is intended to not start anew a "regular" + // module comparison through Module::custom_cmp, but to + // rather have all subsequent comparisons be based off of + // the sizes of module subtrees rather than the + // lexicographical comparison of their paths. our_m.items.custom_cmp(&their_m.items) }), other => other.reverse(), @@ -252,12 +263,11 @@ impl FfiItems { }) } - /// Deduplicates the items in an item container by their path. Items of - /// differing kind, like functions and constants, that may happen to have - /// the same path will not get deduplicated. + /// Deduplicates the items in an item container by their path. /// - /// A consequence of calling this method is that the items are also sorted - /// by their paths. + /// Items of differing kind, like functions and constants, that may happen + /// to have the same path will not get deduplicated. A consequence of + /// calling this method is that the items are also sorted by their paths. fn dedup(self) -> FfiItems { macro_rules! key_fn { () => { @@ -996,13 +1006,12 @@ fn merge_module(dst: Module, src: Vec) -> Module { .foreign_statics .extend_from_slice(&foreign_statics); dst.items.modules.extend_from_slice(&modules); - let dst = dst.clone(); dst.clone() }, ) } -/// Appends a new module-local item to an absolute path that does *not* start +/// 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. @@ -1337,7 +1346,7 @@ mod test1 { #[test] fn tmp1() { let source = r#" -use test1::*; +// use test1::*; pub struct Foo; @@ -1350,3 +1359,23 @@ mod test1 { use super::*; } let items = items.clone(); println!("{:#?}", items); } + +#[test] +fn tmp2() { + let m1 = Module { + cached_path: String::from("a").into_boxed_str(), + items: FfiItems::default(), + ..Default::default() + }; + let m2 = Module { + cached_path: String::from("a").into_boxed_str(), + items: FfiItems { + modules: vec![Module::default()], + ..Default::default() + }, + ..Default::default() + }; + let mut ms = vec![m1, m2]; + ms.sort_unstable_by(Module::custom_cmp); + println!("{:#?}", ms.clone()); +} diff --git a/notes/new-algo.md b/notes/new-algo.md index 792e21ac6b8b0..6a7df15abf2bf 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -738,15 +738,62 @@ comparison closure for the deduplication proposition (which keeps only the first item of a run of consecutive items.) The logic seems done but it may be potentially faulty. The comparison logic first considers the paths of the modules, after which it recursively compares the subtrees -rooted at them. The issue may come from the fact that the subtree -recursion calls back again into the proposition for the top-level -modules. This means that if some two modules have the same path, and yet +rooted at them. The issue may come from the fact that subtree recursion +calls back again into the proposition for the top-level modules. This +means that if some two modules have the same path, and yet post-resolution one of them got a new child module, but this one's path happened to compare lexicographically smaller than some other pre-existing module, then the module we'd stick with first would end up being the older snapshot. Something else that doesn't quite strike me as -correct is the comparison used to determine equality among modules durin -deduplication. This can not be the same as the one used when sorting the -list of modules, because we need all modules whose paths compare equal -to be considered for removal; Only the first one among such set, which -is the first thanks to the other comparison logic, will remain. +correct is the comparison used to determine equality among modules +during deduplication. This can not be the same as the one used when +sorting the list of modules, because we need all modules whose paths +compare equal to be considered for removal; Only the first one among +such set, which is the first thanks to the other comparison logic, will +remain. One place where there could be issues concerning use +declarations is the single-use resolution proposition. The case for glob +imports brings over everything from the target module, but this should, +in theory, not cause any issues. Even if the resolution item packs use +declarations, these are ignored during merging so it should be safe to +leave the logic as-is. It may be worth to add a comment noting that to +the single-use resolution proposition. But before that, testing is in +order. The same reduced failing test should be run anew. It fails again. +Something is causing it to enter an infinite loop. Debugging this would +be great. Maybe there's some information out there on rust-lldb. That +seems like a timesink, so it would be best if we first tried to think +about the problem some more. Debugging will not work without spending a +significant amount of time looking into it. Best to try and reason +through the program logic. The use declaration in the leaf module will +be resolved first, and that will import over all items from the +whole-crate module tree. The merging would yield a child module that +contains a child module of its own referring to itself. The difference +here is that this nested self should not contain use statements. This +hints at the fact, which has just been confirmed, that the failing test +can be further reduced not just to the case of a set of cyclic use +declarations, but to a single super-relative use declaration. The +current failing test has a single module beyond the crate root, where +the former contains a use declaration that glob imports the crate root's +items. The correct course here would be for the leaf module to import +itself and remove its one use declaration before moving on. Then this +would be integrated into the whole-crate module tree and resolution +would move on to the crate root. At this point, resolution should find +there is nothing to do in the crate root. That should finish the first +pass. The diffing should have decided that another pass is in order, as +it should have noticed that there is a subtree size mismatch between the +pre-pass and post-pass states. The infinite loop did not take place in +the absence of cyclic use declarations before the deduplication routine +got tweaked, which likely hints at the fact the underlying issue may be +in the refactoring. A test to sort a bunch of modules should be devised. +This may reveal the infinite loop is taking place during sorting. The +test does not fail when basing one off of purely item paths. That hints +at the fact the issue may be in the secondary sorting condition. Sorting +by the subtree structure also seems to work. That does imply the issue +may not be in the sorting routine at all, though maybe these tests are +too contrived compared with the scenario faced in the test. It could +also be that the comparison function does not implement a total order, +and that causes issues when comparing sizes. One alternative to +performing module deduplication this way is to, during merging, attempt +to find all modules about to be merged in the module into which to +merge, then remove those off of the latter, and then merge the modules +packed as part of the resolution item. That would avoid the need for +deduplicating modules after merging From b2935cc3d1a16ed83858ef77a2e237ec5325e222 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:15:08 +0200 Subject: [PATCH 065/100] chore: wip --- ctest/src/ast/structure.rs | 17 +++++ ctest/src/ffi_items.rs | 147 ++++++++++++++++++++++++++++++++----- notes/new-algo.md | 49 ++++++++++++- 3 files changed, 192 insertions(+), 21 deletions(-) diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index b94a842d19150..d5f840eb933f7 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -1,3 +1,6 @@ +use syn::Path; +use syn::punctuated::Punctuated; + use crate::{ BoxStr, Field, @@ -32,3 +35,17 @@ impl Struct { ident.to_string() } } + +impl Default for Struct { + fn default() -> Self { + Self { + public: bool::default(), + cached_path: String::new().into_boxed_str(), + path: Path { + leading_colon: None, + segments: Punctuated::new(), + }, + fields: vec![], + } + } +} diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index dbd96050b16cd..4c8e2d965ea82 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -142,7 +142,7 @@ impl FfiItems { } = file; let file_mod = ItemMod { attrs: attrs.clone(), - vis: syn::parse_quote! { pub }, + vis: Visibility::Public(syn::Token![pub](Span::call_site())), unsafety: None, mod_token: Mod::default(), ident: Ident::new("__ctest_root_mod", Span::call_site()), @@ -333,10 +333,19 @@ impl Default for FfiItems { /// See [`RefinedUseTree`] for details. #[derive(Clone, Debug, PartialEq)] pub(crate) struct RefinedUsePath { - ident: syn::Ident, + ident: Ident, tree: Box, } +impl Default for RefinedUsePath { + fn default() -> Self { + Self { + ident: Ident::new("__dummy", Span::call_site()), + tree: Box::default(), + } + } +} + /// Refined type for [`syn::UseTree`] that does not consider group paths. /// /// This is the byproduct of normalizing a `use` declaration's path through @@ -345,11 +354,12 @@ pub(crate) struct RefinedUsePath { /// /// This enumeration is thus equivalent to `syn::UseTree` but does not consider /// the case of `syn::UseTree::Group`. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Default)] pub(crate) enum RefinedUseTree { Path(RefinedUsePath), - Name(syn::UseName), - Rename(syn::UseRename), + Name(UseName), + Rename(UseRename), + #[default] Glob, } @@ -358,7 +368,7 @@ pub(crate) enum RefinedUseTree { /// This exists only for the purpose of refining [`syn::UseTree`], which is used /// within [`syn::ItemUse`]. See type [`RefinedUseTree`] for the actual /// refinement and details on the purpose of this type. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Default)] pub(crate) struct RefinedUse { is_public: bool, tree: RefinedUseTree, @@ -483,12 +493,21 @@ fn resolve_pass(src: PassPair) -> PassPair { subtree: root_with_resolved_children.clone(), whole_crate: whole_crate_with_resolved_children.clone(), }); - let new_root = merge_module(root_with_resolved_children.clone(), resolved_uses.clone()); + let new_root_with_duplicates = + merge_module(root_with_resolved_children.clone(), resolved_uses.clone()); let new_root = Module { - items: new_root.items.clone().dedup(), - ..new_root.clone() + items: new_root_with_duplicates.items.clone().dedup(), + ..new_root_with_duplicates.clone() }; let new_whole_crate = update_tree(new_root.clone(), whole_crate_with_resolved_children.clone()); + println!( + "finished module pass on module {}", + if &*new_root.cached_path == "" { + "root".into() + } else { + new_root.cached_path.clone() + }, + ); PassPair { subtree: new_root.clone(), whole_crate: new_whole_crate.clone(), @@ -503,6 +522,15 @@ fn resolve_pass(src: PassPair) -> PassPair { /// /// See [`resolve_pass`] for details. fn resolve_one(state: PassPair) -> Vec { + println!( + "found {} use declarations in module {}", + state.subtree.items.uses.len(), + if &*state.subtree.cached_path == "" { + "root".into() + } else { + state.subtree.cached_path.clone() + }, + ); state .subtree .items @@ -542,13 +570,18 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! single_item { ($field:ident: $it:expr) => { return Resolution::Resolved(FfiItems { - $field: vec![$it.clone()], + $field: Vec::from([$it.clone()]), ..Default::default() }) }; } match r#use.tree.clone() { RefinedUseTree::Name(UseName { ident }) => { + println!( + "entered name resolution case with {} in module {}", + ident.clone(), + state.subtree.cached_path.clone() + ); // [NOTE]: self may not appear as the name basecase of a well-formed // path. And yet it is still considered here. That is because of // path normalization. self may appear as part of a tail group in a @@ -590,8 +623,27 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { None => Resolution::Unresolved, } } - RefinedUseTree::Glob => Resolution::Resolved(state.subtree.items.clone()), + RefinedUseTree::Glob => { + println!( + "entered glob resolution case in module {}", + if &*state.subtree.cached_path == "" { + "root".into() + } else { + state.subtree.cached_path.clone() + }, + ); + Resolution::Resolved(state.subtree.items.clone()) + } RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { + println!( + "entered rename resolution case with {:?} in module {}", + UseRename { + ident: ident.clone(), + as_token: syn::Token![as](Span::call_site()), + rename: rename.clone(), + }, + state.subtree.cached_path.clone() + ); macro_rules! new_path { ($it:expr) => {{ let mut new_segments = $it.path.segments.clone(); @@ -627,8 +679,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { |p, s| { let mut p = p.clone(); p.segments.push(s); - let p = p.clone(); - p + p.clone() }, ); Path { @@ -667,6 +718,11 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { } RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { + println!( + "entered path resolution case with {} in module {}", + ident.clone(), + state.subtree.cached_path.clone() + ); let new_subtree = if is_self(ident.clone()) { state.subtree.clone() } else if is_super(ident.clone()) { @@ -719,7 +775,7 @@ fn get_module(target: Path, root: Module) -> Option { /// If the module is a top-level item (i.e. it appears at the crate root,) the /// returned path will be empty. This follows from the parsing strategy for /// paths in `FfiItems`. The absolute paths are all `crate`-_unprepended_. -fn get_parent_path(root: Module) -> syn::Path { +fn get_parent_path(root: Module) -> Path { // [NOTE]: this performs a `saturating_sub` because there is one and only // one item in the crate's module tree that has an empty path; The crate // root. That is only wrapped in a `Module` of its own only after parsing. @@ -1346,18 +1402,14 @@ mod test1 { #[test] fn tmp1() { let source = r#" -// use test1::*; - pub struct Foo; mod test1 { use super::*; } "#; let file = syn::parse_file(source).unwrap(); - let items = FfiItems::default(); - let mut items = items.clone(); + let mut items = FfiItems::default(); items.visit_file(&file); - let items = items.clone(); - println!("{:#?}", items); + println!("{:#?}", items.clone()); } #[test] @@ -1379,3 +1431,58 @@ fn tmp2() { ms.sort_unstable_by(Module::custom_cmp); println!("{:#?}", ms.clone()); } + +#[test] +fn tmp3() { + let dst_child_path = Path { + leading_colon: None, + segments: { + let mut out = Punctuated::new(); + out.push(PathSegment { + ident: Ident::new("foo", Span::call_site()), + arguments: PathArguments::None, + }); + out.clone() + }, + }; + let dst = Module { + items: FfiItems { + modules: Vec::from([Module { + path: dst_child_path.clone(), + cached_path: path_to_string(dst_child_path.clone()), + items: FfiItems { + structs: Vec::from([{ + let path = Path { + leading_colon: None, + segments: Punctuated::new(), + }; + Struct { + path: path.clone(), + cached_path: path_to_string(path.clone()), + ..Default::default() + } + }]), + ..Default::default() + }, + ..Default::default() + }]), + ..Default::default() + }, + ..Default::default() + }; + let src = Vec::from([Resolution::Resolved(FfiItems { + modules: Vec::from([Module { + path: dst_child_path.clone(), + cached_path: path_to_string(dst_child_path.clone()), + items: FfiItems { + modules: Vec::from([Module::default()]), + uses: Vec::from([RefinedUse::default()]), + ..Default::default() + }, + ..Default::default() + }]), + ..Default::default() + })]); + let merged = merge_module(dst.clone(), src.clone()); + println!("{:#?}", merged.clone()); +} diff --git a/notes/new-algo.md b/notes/new-algo.md index 6a7df15abf2bf..5cb05894bd126 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -796,4 +796,51 @@ performing module deduplication this way is to, during merging, attempt to find all modules about to be merged in the module into which to merge, then remove those off of the latter, and then merge the modules packed as part of the resolution item. That would avoid the need for -deduplicating modules after merging +deduplicating modules after merging. Either way, it seems best not to +get rid of the current infrastructure until it is proven incorrect. The +first thing to attempt should be a test of the module merging +proposition, which may be the source of issues as it is the one place +where use declarations are removed. The test should exercise two things; +For one, the module into which to merge should end up with duplicate +modules. But the use declarations of the modules to merge should not be +brought over to the former. It would seem use declarations do get +properly cleaned up. Control flow isn't even entering the path case of +the use declaration, which may prompt at the fact the issue is way +before that. It would seem the issue is in the fact stdout is not being +emitted by the test harness. There's probably some option to fix this. +The whole thing works only in the child module, and at that point it +just keeps on going on an infinite loop. The issue has been identified. +The problem lies in the perspective the rest of the crate has of a given +subtree after a resolution pass. Suppose we have a one-level deep module +tree with two modules; A crate root and a leaf module. Assume the leaf +module contains a glob import of all items in the crate root. After the +first pass on the leaf module, the use declaration resolved to the leaf +module itself, which nests it in the module tree. At this point, the +module tree contains a two-level deep module hierarchy, with the crate +root, a node module and a leaf module. The leaf module contains no use +declarations; The crate root contains no use declarations and the node +module continues to have one use declaration. The current revision of +the algorithm will still resolve the use declaration, which this time +contains a snapshot of a one-level deep subtree rooted at the leaf +module. This happens to be "larger" as per our comparison proposition +than the 0-depth module tree containing solely the nested leaf module. +The natural consequence to this is that the node module keeps the larger +snapshot and the resolution driver determines it should go for another +pass. The next pass goes through the same motions; Namely, it resolves +the use declaration anew, and finds the "largest" subtree to keep in the +node module is the now three-level deep subtree rooted at it (which is +represented in the current snapshot of the whole-crate module tree.) +This keeps repeating endlessly because each time the driver diffs the +pre-pass and post-pass states, it notices there are changes, so it +decides to go for another round. The way the algorithm was initially +conceived failed to realize that each subsequent resolution pass to the +one where all use declarations converged will still differ from the last +resolution pass. In other words, the algorithm was assuming that each +new resolution would see the module tree's state as it was after the +pass where convergence took place. The issue with this assumption is +that one can not determine when is it that convergence took place. Let +us describe the ideal result in the above example; The glob import in +the leaf module should have been resolved as it currently does, but the +next resolution pass should have performed resolution with respect to +the original whole-crate module-tree, and not to the state of the +whole-crate module tree after the first resolution pass was done. From 4cf8c4ff71e11e561307671821bb1852f3ada541 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:00:31 +0200 Subject: [PATCH 066/100] chore: wip --- notes/new-algo.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/notes/new-algo.md b/notes/new-algo.md index 5cb05894bd126..a24c524dd9c16 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -843,4 +843,77 @@ us describe the ideal result in the above example; The glob import in the leaf module should have been resolved as it currently does, but the next resolution pass should have performed resolution with respect to the original whole-crate module-tree, and not to the state of the -whole-crate module tree after the first resolution pass was done. +whole-crate module tree after the first resolution pass was done. The +solution could be in performing subsequent resolution passes with the +initial state of the crate's module tree. The issue then would be that +no resolution pass would provide updated information to subsequent +resolution passes. Even the first pass would be futile; In the above +example, the leaf module would get the items from the crate root, then +another pass would run after diffing with the pre-pass module tree with +the post-pass state. This second pass would attempt to resolve the same +use declaration, and this would actually turn out well. The initial +snapshot of the leaf module without itself nested would be removed +during deduplication, leaving the larger one-level deep node module in +its stead. For this to work out, the whole-crate module tree would have +to be updated back at the resolution driver only to perform the diff. +And yet each subsequent resolution would be fed the initial whole-crate +module tree. Does even rustc allow this? It does. It also allows +arbitrarily nested modules within the module with the super-relative +glob import, and subsequent references to their items through the name +of the module (which is only made visible thanks to the import.) Maybe +resolution of super imports can be delayed? Nay, because a single +super-relative glob import is enough for non-termination. No matter the +state of the crate, super-relative imports that affect the list of child +modules in the source module seem to be the root cause of this issue. +This can likely also be extended to absolute imports that refer to some +ancestor module. Maybe the issue can be solved by identifying paths in +the module graph. Two examples come to mind that could be solved by this +strategy. Suppose the module graph consists of a two-level deep +hierarchy with a crate root, a node module and a leaf module. Assume the +crate root contains a glob import of the items in the leaf module. Then +assume the leaf module contains a glob import of the items in the node +module. If we think of the use declarations as directed edges, we can +draw a path from the crate root all the way up to the node module. +Resolution should thus take place by walking the reverse path; The +super-relative module should be resolved first, and only then should the +use declaration in the crate root be resolved. Let us explain the second +example; Suppose we are resolving the same module hierarchy as presented +above. This time, the crate root contains a glob import of the leaf +module's items, and the node module contains a glob import of the crate +root's items. Then again, we find that a path is drawn where, if walked +in reverse, the crate root import would be resolved first, and only then +would the super-relative import be resolved. Indeed, it seems use +declaration resolution reduces to the problem of finding connected +components and walking paths in reverse. But there's two things here +that aren't entirely clear; How would the ideal situation proceed when +posed with multiple possible paths in the walk, and how would cycles be +handled? Say in the second example above the crate root had another glob +import of the node module's items. This would mean there would be two +possible paths to be walked from the node module. Both would have their +first stop at the crate root, but then they could follow up on either +one of the leaf module or the node module. Let us assume cycles are +discarded upon detection. Then only one path could be walked. But should +cycles really be discarded? The idea currently seems to be as follows. + +- Build a directed graph where the nodes are the modules, and the edges + are the use declarations, such that the direction of the edges is + given by the module where the target item(s) of the use declaration + lives in. + +- Detect all connected components in the graph. This should be fairly + straightforward. + +- For each connected component, compute all paths and subsequently sort + the paths by whether they aren't cycles or not. The non-cyclic paths + should come first. + +- For each component, and for each computed path in the sorted list + obtained above, perform a reverse walk of the path, resolving the use + declarations in the order they are found in this reverse walk. Remove + any resolved use declaration. + +This leaves open the question about initially unresolvable use +declarations, which require information introduced by another use +declaration to even attempt resolution. It would seem the list of graph +paths would have to be updated each time a graph path were resolved in +full. From 434af88e14791314c7a39b23265919528be201b6 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:45:30 +0200 Subject: [PATCH 067/100] chore: wip --- notes/new-algo.md | 175 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/notes/new-algo.md b/notes/new-algo.md index a24c524dd9c16..421934b3a3464 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -916,4 +916,177 @@ This leaves open the question about initially unresolvable use declarations, which require information introduced by another use declaration to even attempt resolution. It would seem the list of graph paths would have to be updated each time a graph path were resolved in -full. +full. To recap, the algorithm will need to, after parse-time is done and +before resolution starts, build a graph off of the use declarations and +modules. Because the nodes are the modules, and the use declarations are +the edges, we can likely base ourselves off of the same tree. But this +discussion is not one we should be having now. The priority is to think +through the handling of cyclic paths. How would ideal handling of this +look like? Let us consider a cyclic example. Suppose the crate root +contains both a child module and a glob import of the child's items. +Suppose as well that the child module contains a glob import of the +crate root's items. How would the ideal solution look like? The crate +root should end up with the child's items but not with itself nested. +This whole approach may not be the right way of thinking about it. +Apparently, Rust allows the same arbitrary nesting behavior that was +thought inadequate in the second revision of the algorithm. But it does +not loop infinitely when resolving. In the same example as presented +above, rustc will gladly allow the user to refer to some arbitrarily +self-nested child module, which is exactly the behavior of repeated +resolution in the second revision of the algorithm. For future +reference, this is illustrated in the following code snippet. + +```rust +mod foo { + pub struct Foo; + + mod bar { + use super::*; + + const FOO: bar::bar::bar::Foo = bar::bar::Foo; + } +} +``` + +The fact rustc does hit termination even though it shouldn't terminate +resolution is likely because they treat either one of paths or modules +as codata. In other words, to rustc, the subtree rooted at a given +module is possibly infinite. Of course, allowing codata requires knowing +you're handling codata, as otherwise you can kiss goodbye to +termination. This means the second revision of the algorithm is still +feasible, so long as these potentially infinite trees can be detected. +This prompts at the fact efforts should now focus on integrating another +algorithm for codata detection. Maybe we can skim the literature to try +and find out if there's something for this. But it's best if we give it +a shot ourselves first. The one point at which we know for sure that +termination took place is back at the resolution driver. This is because +before that, we only really know we are resolving "some" module in the +single-pass resolution proposition. Granted, one could compare the +subtree module tree with the whole crate module tree, and do whatever +then, but we do just this once the call unwinds back to the driver, so +it seem like a waste to not profit the comparison that already takes +place in the driver. Back at the resolution driver, the heuristic for +termination will have to be improved. An additional pass must be made +after each resolution pass that checks if some module contains itself. +If it does, then we know for sure that whatever is under the subtree +rooted at that module can be referred to through multiple paths that do +not officially appear anywhere. This does mean items will need a new +marker field that indicates whether their paths are propositionally +equal to paths with each segment before the last repeated an arbitrary +amount of times. But maybe this can generalized to any item. See, by the +time we get to parsing and resolving use declarations, we know for sure +that input is well-formed because it would not have passed cargo-check +otherwise. That does mean every path by which an item is referred to is +technically correct. Thus, any item that can be referred to by one of +these infinitely nested paths can be safely assumed to also be skippable +in ctest through one of these infinite paths. In other words, we can +allow all items to be referred to by these infinite paths, and just +about call it. Granted, if somebody used some such path for an item that +does not allow it in the skips of the crate being tested, ctest would +give it carte blanche. Maybe it's a good idea to ask Mr. Gross about +this. But it would be best to have a working implementation that +showcases the behavior first. To recap, the new pass that ought be added +to the resolution driver should follow these steps: + +1. Traverse the whole-crate module tree anew, checking among the list of + child modules to a given module whether there exists some module for + which there is another child module with the same path. + +2. If there is, flag the module so as to know that the next resolution + pass, once it has resolved the module in question, should not attempt + to replace the nested self with some newly resolved self. + +3. Imply the whole-crate module tree before the recursive comparison + with the pre-pass state. The diffing should happen after the nested + self has been flagged, as otherwise we risk non-termination. + +It's unclear how would the diffing step ignore the nested self module. +Is it supposed to be wary of the "flag" that these nested self modules +are marked with? Likely not. The idea is lacking in some respect; The +point of resolution is to eventually converge to a stable state where +resolving any use declaration anew would not produce changes on the +whole-crate module tree. What would the ideal case in a case such as the +currently failing test look like? Let us include the code snippet of the +test here. + +```rust +pub struct Foo; + +mod foo { + use super::*; +} +``` + +The tests fail at the point where the driver diffs the pre-pass state +corresponding to the first pass, with the post-pass state after the +second pass. The former contains the crate root, the foo module and a +the foo module nested anew within it. The post-pass state contains the +crate root, the foo module, then the foo module nested within it, and +then the foo module nested again. But this is not the point where it +should really detect that the foo module should not be nested any +further. It actually is, though. The effects of the first pass are +required to understand that the items in the subtree rooted at a given +module can be referred to by an infinite number of paths. It is once the +same module is nested a third time that resolution should remove the +second level of nesting, essentially "normalizing" a potentially +infinite subtree. This can happen during deduplication before returning +to the resolution driver, where the "larger" subtree is scanned for a +third ocurrence of the top-level module within a direct line of +descendant modules. The idea is to perform a tree traversal that, for +each module, checks the next two descendant modules and ensures they are +not the same module. If they are, then the whole subtree rooted at the +the third ocurrence of the module in question should be removed. Then +the resolution driver can perform the diffing as it currently does, and +the recursive comparison should consider the pre-pass and post-pass +states to be structurally equal. Before getting into the weeds of it, +though, it may be a better idea to think about edge cases where this +will remove some nested self module that should not be removed. A +simpler edge case may be an absolute path to the crate root followed by +a rename. Say we have a crate root and a single child module, where the +latter contains a fine-grained renamed import of the crate root. After +the first pass, the child module contains the crate root as its own +child module. After the second pass, the resolved crate root is "larger" +than the prior crate root, so the child module gets the "new" crate +root. The solution to this problem would notice that the same nested +crate root exists within the larger subtree, and should favor removal of +the nested crate root. Then it should flag all items that appear before +the now removed self-module as being addressable through an "infinite" +set of nested paths that repeat the non-removed segments of the path. To +further illustrate this idea, consider the following example, which is +much like the one in the currently failing test, but reduces the +complexity of the type of use declaration to a renamed fine-grained +import. + +```rust +mod foo { + use crate as root; + + pub struct Foo; + + const FOO: root::foo::root::foo::root::foo::Foo = Foo; +} +``` + +The crate root is imported through a renamed identifier, which on each +subsequent pass allows further nestedness of the same crate root within +module foo. rustc understands that there is potential non-termination if +the use declaration resolution does not halt after the crate root is +found in the subtree rooted at the crate root itself. That seems to be +the pattern that identifies module subtrees as codata; Given some module +a, another snapshot of module a may be found within the subtree rooted +at module a. Though this seems to collide with having a set of child +modules each contain an import referring to the crate root. Or not. If +really "viewed" as a tree, a different copy of the crate root exists on +each child module that has imported the crate root. A "linear" scan over +all items in a given module, here the crate root, would attempt to +remove the nested crate root off of each of the child modules containing +it as a consequence of resolving the super-relative imports. That is +undesired, because what is really needed here is to keep the first +nested crate root, but not any subsequently nested crate roots. In other +words, if a given tree traversal stumbles upon the same module where the +traversal started on, and then further down the tree finds the same +module again, it should only remove this latter ocurrence and not the +first nested ocurrence. But it should "mark" all items in the module +where the first nested ocurrence appeared at as being addressable by an +"infinite" path consisting of repeated occurrences of the path up to the +point where the removed module appeared. From 612934c8d76ac7a4080f0d6605731142dfaa7ed2 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:46:39 +0200 Subject: [PATCH 068/100] chore: wip --- notes/new-algo.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/notes/new-algo.md b/notes/new-algo.md index 421934b3a3464..2c98918586162 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -1089,4 +1089,85 @@ module again, it should only remove this latter ocurrence and not the first nested ocurrence. But it should "mark" all items in the module where the first nested ocurrence appeared at as being addressable by an "infinite" path consisting of repeated occurrences of the path up to the -point where the removed module appeared. +point where the removed module appeared. On this same topic, how are +paths for the same item reexported through different modules connected +to each other? The whole point of having different paths available for a +given item is that a skip over the path in a reexport will also skip +over the original item, as otherwise it would not make much sense to +have the reexporting module's test not include the item, but the source +module include the item. That doesn't void the need for use declaration +resolution, but does mean that the current approach is effectively +useless at getting at this. Resolution should not import an item over to +the module where the use declaration takes it into account; It should +only add to that item's set of addressable paths the path of the target +module. Doing that without resorting to an anciliary data structure +seems doable until one looks into initially unresolvable use +declarations. Those have a dependency on one or more other use +declarations, which makes resolution as it is currently conceived +necessary. But maybe both ideas can be merged. All items could have +their path fields modified to consist of a collection of possible paths, +which would be expanded each time a use declaration were resolved. This +would require expanding the set of use declarations in both the source +item and the newly imported item. That does mean the source module of +the item (not the use declaration) would have to be kept as part of the +resolution state in the single-use resolution proposition. But that's +already getting into the details of the implementation; Those are best +left for later. The goal here would be to have the set of addressable +paths of the item in both the source and target modules consider all +paths by which that one item can be referred to. This would allow +fine-grained skipping, which does tno take into account more than one +item at a time; It does perform deterministic traversals, so one could +trust a given item could be the one to hold all addressable paths for +itself, but that seems like unnecessary complexity at a potentially +higher cost. Though this would also require changes to the skipping +logic. The current skips are all bunched up as wrappers over each type +of item, and exposed to the closures that the user feeds to the test +generator. The simplest way to accomplish this would be to add +documentation to the skipping functions mentioning that the user can +access the set of paths by which an item can be referred to through some +public method on the types used for each item. The current API to +provide access to a single path would be removed in favor of providing +access to a set of possible paths associated with the term. So to recap, +the following changes are necessar to potentially have use declaration +resolution be ready for use in other areas of ctest. + +- Modifying the path field of each item type to hold a collection of + paths instead of a singular path. During parsing, the parsed path + would make up the singleton list of paths considered for a given item. + +- Changing the merging proposition to stop renaming the paths of items + to be merged, in favor of adding the renamed path to the list of + addressable paths of the item. + +- Changing the merging proposition to traverse the whole-crate module + tree and add to the source item the renamed path obtained above. This + will need changing the first assumption of the proposition to instead + be a stateful pair of the subtree where the use declarations were + sourced from, and the whole-crate module tree to perform a search and + an update on. + +- Changing the deduplication proposition to add another step after + deduplicating the modules in the item container. This new step should + traverse the subtree and check that the root module of this subtree + does not appear more than once in a path down the tree. + +The first three points seem clear. The last point is going to need more +discussion. Suppose we have a crate root with one child module, and the +child contains another child module. Then assume the crate root contains +an import of all items in the leaf module, and another import of all +items in the node module. Then assume the node module has a glob import +of all items in the crate root. Initially, the crate root can not +resolve the use declaration that points to the leaf module because it +has no knowledge of it; It first needs to bring over all items from the +node module. The algorithm performs DFS so it reaches the node module +before it gets to the crate root. That will attempt to resolve the crate +root, which will add the crate root to the list of child modules to the +node module. Then the crate root will resolve its own node module import +to bring over a child module that contains a nested crate root. That +should be the end state of the first resolution pass. Before starting +the second pass, deduplication takes place, but will currently find +nothing to deduplicate. The issue comes from having two duplicates down +two different paths in the subtree rooted at the crate root. One +corresponds with the crate root brought over from the node module, and +the other corresponds with the crate root that is now a child of the +node module. From f8b0a01bd1f7233c023b48114ebfe19f5333957a Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:27:34 +0200 Subject: [PATCH 069/100] chore: wip --- notes/new-algo.md | 110 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 12 deletions(-) diff --git a/notes/new-algo.md b/notes/new-algo.md index 2c98918586162..238602e68932a 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -895,19 +895,19 @@ one of the leaf module or the node module. Let us assume cycles are discarded upon detection. Then only one path could be walked. But should cycles really be discarded? The idea currently seems to be as follows. -- Build a directed graph where the nodes are the modules, and the edges +* Build a directed graph where the nodes are the modules, and the edges are the use declarations, such that the direction of the edges is given by the module where the target item(s) of the use declaration lives in. -- Detect all connected components in the graph. This should be fairly +* Detect all connected components in the graph. This should be fairly straightforward. -- For each connected component, compute all paths and subsequently sort +* For each connected component, compute all paths and subsequently sort the paths by whether they aren't cycles or not. The non-cyclic paths should come first. -- For each component, and for each computed path in the sorted list +* For each component, and for each computed path in the sorted list obtained above, perform a reverse walk of the path, resolving the use declarations in the order they are found in this reverse walk. Remove any resolved use declaration. @@ -992,11 +992,11 @@ to the resolution driver should follow these steps: child modules to a given module whether there exists some module for which there is another child module with the same path. -2. If there is, flag the module so as to know that the next resolution +1. If there is, flag the module so as to know that the next resolution pass, once it has resolved the module in question, should not attempt to replace the nested self with some newly resolved self. -3. Imply the whole-crate module tree before the recursive comparison +1. Imply the whole-crate module tree before the recursive comparison with the pre-pass state. The diffing should happen after the nested self has been flagged, as otherwise we risk non-termination. @@ -1128,25 +1128,25 @@ access the set of paths by which an item can be referred to through some public method on the types used for each item. The current API to provide access to a single path would be removed in favor of providing access to a set of possible paths associated with the term. So to recap, -the following changes are necessar to potentially have use declaration +the following changes are necessary to potentially have use declaration resolution be ready for use in other areas of ctest. -- Modifying the path field of each item type to hold a collection of +* Modifying the path field of each item type to hold a collection of paths instead of a singular path. During parsing, the parsed path would make up the singleton list of paths considered for a given item. -- Changing the merging proposition to stop renaming the paths of items +* Changing the merging proposition to stop renaming the paths of items to be merged, in favor of adding the renamed path to the list of addressable paths of the item. -- Changing the merging proposition to traverse the whole-crate module +* Changing the merging proposition to traverse the whole-crate module tree and add to the source item the renamed path obtained above. This will need changing the first assumption of the proposition to instead be a stateful pair of the subtree where the use declarations were sourced from, and the whole-crate module tree to perform a search and an update on. -- Changing the deduplication proposition to add another step after +* Changing the deduplication proposition to add another step after deduplicating the modules in the item container. This new step should traverse the subtree and check that the root module of this subtree does not appear more than once in a path down the tree. @@ -1170,4 +1170,90 @@ nothing to deduplicate. The issue comes from having two duplicates down two different paths in the subtree rooted at the crate root. One corresponds with the crate root brought over from the node module, and the other corresponds with the crate root that is now a child of the -node module. +node module. To illustrate this, consider the following diagram. + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> node[Node module] + node ==> child[Child module] + root -.-> child + root --> node + node --> root +``` + +The thicker directed edge indicates a connection in the graph, while the +thinner lines indicate a reexport of the target node's modules. The +crate root additionally has a dotted reexport to the child module, which +indicates it is not resolvable during the first resolution pass. That +one reexport depends on the glob import of the node module's items to +the crate root. We'll describe now the states during the first pass. The +algorithm performs DFS, so it will first reach the child module and find +there's no reexports. At that point, it will move back to the node +module, where it finds there's a reexport of all items in the crate +root. This brings over the node module and the child module to the node +module's list of child modules. That can been seen in the following +graph. + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> node[Node module] + + node ==> child[Child module] + node ==> oldnode[Node module] + + subgraph old[" "] + oldnode ==> oldchild[Child module] + end + + root -.-> child + root --> node + node --> root +``` + +The imported-over items, during deduplication, also remove the use +declarations in the imported modules. This means the nested node module +(the only item in the crate root) gets the use declaration to the crate +root removed. Still during the first pass, the algorithm reaches the +crate root, and attempts to resolve the reexport of all items in the +node module. This should bring over the child module to the crate root, +and thus make the reexport of the child's items now resolvable. The +crate root also ends up with the node module that contains itself +nested. The end state of the first pass is as follows. + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> node[Node module] + root ==> newchild[Child module] + root ==> newnode[Node module] + + subgraph new[" "] + newchild + newnode + newnodechild + end + + newnode ==> newnodechild[Child module] + + node ==> child[Child module] + node ==> oldnode[Node module] + + subgraph old[" "] + oldnode ==> oldchild[Child module] + end + + root --> newchild + root --> node + node --> root +``` From fbb78a0ce8b7336446bf8d3bfe639cfa1e378fef Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:01:18 +0200 Subject: [PATCH 070/100] chore: wip --- notes/new-algo.md | 170 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 7 deletions(-) diff --git a/notes/new-algo.md b/notes/new-algo.md index 238602e68932a..57f23df826c33 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -1179,7 +1179,7 @@ config: --- graph root[Crate root] ==> node[Node module] - node ==> child[Child module] + node ==> child[Leaf module] root -.-> child root --> node node --> root @@ -1206,11 +1206,11 @@ config: graph root[Crate root] ==> node[Node module] - node ==> child[Child module] + node ==> child[Leaf module] node ==> oldnode[Node module] subgraph old[" "] - oldnode ==> oldchild[Child module] + oldnode ==> oldchild[Leaf module] end root -.-> child @@ -1235,7 +1235,7 @@ config: --- graph root[Crate root] ==> node[Node module] - root ==> newchild[Child module] + root ==> newchild[Leaf module] root ==> newnode[Node module] subgraph new[" "] @@ -1244,16 +1244,172 @@ graph newnodechild end - newnode ==> newnodechild[Child module] + newnode ==> newnodechild[Leaf module] - node ==> child[Child module] + node ==> child[Leaf module] node ==> oldnode[Node module] subgraph old[" "] - oldnode ==> oldchild[Child module] + oldnode ==> oldchild[Leaf module] end root --> newchild root --> node node --> root ``` + +The above graph makes it obvious that, during the second resolution +pass, the reexport in the node module of all items in the crate root +would bring over the child module and node module that are the children +of the crate root, but those would be deduplicated with the ones in the +node module. The one imported item that would not be deduplicated is the +node module, which would be again nested in the node module. That would +prompt non-termination, as the same pattern would repeat anew the next +resolution pass. It would seem the natural solution is thus to add some +further logic to the super case of the single-use resolution function. +Super-relative use statements would first fetch the parent module, then +they would strip from the parent module the child module where the use +declaration currently lives at. In theory, some arbitrary chain of +super-relative path segments can only appear at the start of a use +declaration's path, so this should be extensible to the stateful subtree +that we carry through resolution passes. Except the module that would +have to have the codata pattern added to its set of items' possible +paths would have to be the module where the use declaration is sourced +from. That means that part of the strategy will have to be delayed until +later on, potentially at merge time. This last thing isn't entirely +clear just yet. It may be a good idea to first reconsider the above +example and reproduce the graph without having super-relative imports +bring over the module where the use declaration appears at. Suppose then +that we are posed with the same crate module tree, where a crate root +has one child module, and this child module has another child module. +The crate root contains a glob import of the items in the node module +and a glob import of the items in the leaf module. The latter one is not +resolvable during the first pass because it requires having the leaf +module accessible as a direct descendat of the crate root. This only +happens when the crate root resolves its glob import of the node +module's items (among which we find the leaf module.) + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> node[Node module] + node ==> child[Leaf module] + root -.-> child + root --> node + node --> root +``` + +During the first resolution pass, the first module that will attempt +resolution is the node module. This follows the same depth-first +approach as has already been mentioned above. The node module this time +ends up with no new items, as the only item in the crate root is the +node module itself, which we are now skipping. The next step of the +resolution pass goes through resolving the glob-import in the crate root +of the node module. That should import over the contents of the node +module, which here means the leaf module. The end state after the first +resolution pass would look as follows. + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> node[Node module] + root ==> newchild[Leaf module] + node ==> child[Leaf module] + subgraph new[" "] + newchild + end + root --> newchild + root --> node + node --> root +``` + +After the first pass, the resolution driver would conclude that +resolution must continue, as there have been changes between the pre- +and post-pass states of the whole-crate module-tree. During the second +resolution pass, the same depth-first order as before would be applied. +Resolution of the node module's use declaration would be attempted, and +that would import all items from the crate root but the stateful +subtree, which here means the node root. That would bring over a +duplicate of the leaf module to the list of child modules to the node +module. During deduplication in the single-module resolution pass, the +node module would have that duplicate leaf module cleansed. This would +then mean that the final state of the second resolution pass would be +equivalent to that of the first resolution pass. This would prompt the +resolution driver to terminate. The graph does not take into account +importing items, but the second resolution pass would also be capable of +resolving the leaf module glob import found in the crate root. That +would import all items in the leaf module over to the crate root, which +would effectively make the resolution driver perform another pass. That +would then bring over the leaf module's items to the node module, +because of the glob import of the crate root's items found in the node +module. But beyond that, no other use declaration in the third pass +should change the module tree. The glob import of the node module's +items in the crate root would end up deduplicating the leaf module's +items and the leaf module itself. The glob import of the leaf module's +items would also end up deduplicating this module's items after having +already been added to the crate root in the second resolution pass. Now, +how does the above graph, which represents the end state of the +whole-crate module tree post-resolution, look like in comparison with +the ideal case? In the ideal case, one may refer to the items in the +node module with an arbitrarily nested chain of the node module's +identifier. As an example, take the following code snippet, which is +accepted by rustc and replicates the crate module tree considered in +this running example. + +```rust +mod foo { + pub use super::*; + + pub struct Foo; + + mod bar { + pub struct Bar; + } +} + +pub use self::{bar::*, foo::*} +``` + +The above code would allow referring to both the Foo and Bar records +through some arbitrarily nested foo::foo::... path. This does mean the +codata pattern has do be expanded to all items in the subtrees rooted at +the child modules of the module where the super-relative use declaration +lives at. And I would say that's about it. Except that's not it. Notice +how the codata pattern can also manifest itself through the imports of +the node module's items at the crate root. If there were some function +baz at the crate root that referred to record Foo by some path +foo::foo::...::Foo, rustc would accept it. The reason for that is that +the crate root is also meant to get a copy of the items in the node +module with the codata pattern added to their list of addressable paths. +That should be doable in the deduplication function, which now just +orders and deduplicates non-modules by their paths. It will have to be +refactored into deduplicating those items by the number of addresssable +paths they contain. The more addressable paths an item has, the more +up-to-date is their state is in the module tree. This should hold +because we can say there's an invariant about an item's list of +addressable paths. These will only ever be a singleton list or a larger +list, and the list itself will never shrink. So as a quick recap for +future me implementing this: + +* There is need to change all parsed items' types to have their path + field indicate a set of addressable paths instead of only a single + path. + +* There is need for the single-use resolution function to remove the use + declaration's module from the list of child modules of the parent + module fetched in the super subcase of both name and path cases. + +* There is need for the deduplication function to change the sorting + function to instead take into account both the cached path, and in the + case of equivalent cached paths, the length of the addressabele paths. + + The latter should have those items with the longest lists appear first + post-sorting. This means it will have to adopt the same + reverse-ordering strategy as the special case when deduplicating + modules. From d6d6632c05437a078b4b8af1156008e6dac30f2f Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:14:36 +0200 Subject: [PATCH 071/100] chore: wip --- notes/new-algo.md | 66 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/notes/new-algo.md b/notes/new-algo.md index 57f23df826c33..11bd69bc6ea40 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -1406,10 +1406,72 @@ future me implementing this: module fetched in the super subcase of both name and path cases. * There is need for the deduplication function to change the sorting - function to instead take into account both the cached path, and in the - case of equivalent cached paths, the length of the addressabele paths. + logic to instead take into account both the cached path, and in the + case of equivalent cached paths, the length of the addressable paths. The latter should have those items with the longest lists appear first post-sorting. This means it will have to adopt the same reverse-ordering strategy as the special case when deduplicating modules. + +There are some concerns about the first point, though. Consider the +following example, and accompnying graph. + +```rust +mod foo { + mod bar { + use super::super::*; + } +} + +pub struct Foo; +``` + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> node[Node module] + node ==> leaf[Leaf module] + leaf --> root +``` + +In this instance, one would find that the first resolution step will +attempt to resolve the use declaration in the leaf module. For that +matter, there are no other use declarations, so that really is the only +possible use declaration we can resolve. The issue here would be that +the super-relative module would not be capable of finding the leaf +module among the immediate descendants of the crate root. In other +words, the strategy should not just attempt to remove the module where +the use declaration is sourced at, but rather traverse the subtree of +the target module once resolved, and attempt to find the module where +the use declaration was sourced at. After the first pass, the end result +of this strategy would look as follows. + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> node[Node module] + node ==> leaf[Leaf module] + leaf ==> node2[Node module] + leaf --> root + subgraph graph2[" "] + node2 + end +``` + +The second resolution pass would end up doing the same thing, which +would prompt the resolution driver to terminate. Just to be sure, +though, the second pass would, again, attempt to resolve the use +declaration pointing towards the items of the crate root. This would +bring over the node module and whatever items lived in the crate root +(in the above example, record Foo.) But that should be about it, because +the leaf module would be removed off of the imported node module. +Without getting into the details of the implementation, it seems there's +at least one thing to note here. What would be the codata pattern of all +items' list of addressable paths under the leaf module? From 8ecebe63bf704ad839e253036a4e5baebdaec632 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:31:33 +0200 Subject: [PATCH 072/100] chore: wip --- notes/new-algo.md | 255 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 252 insertions(+), 3 deletions(-) diff --git a/notes/new-algo.md b/notes/new-algo.md index 11bd69bc6ea40..5db3002917dea 100644 --- a/notes/new-algo.md +++ b/notes/new-algo.md @@ -1471,7 +1471,256 @@ though, the second pass would, again, attempt to resolve the use declaration pointing towards the items of the crate root. This would bring over the node module and whatever items lived in the crate root (in the above example, record Foo.) But that should be about it, because -the leaf module would be removed off of the imported node module. +the leaf module would be removed off of the imported node module before +the resolution item were yield in the single-use resolution function. Without getting into the details of the implementation, it seems there's -at least one thing to note here. What would be the codata pattern of all -items' list of addressable paths under the leaf module? +at least one thing that doesn't seem quite right here. What would be the +codata pattern of all items' list of addressable paths under the leaf +module? Expanding the code snippet with example uses may be in order. + +```rust +mod foo { + mod bar { + use super::super::*; + + const FOO: foo::bar::foo::bar::Foo = Foo; + } +} + +pub struct Foo; +``` + +The codata pattern is in the two-segment fragment foo::bar. That means +all items now living in the leaf module, among which we find record Foo, +are meant to have the codata pattern associate the upward path of some +ancestor modules except for the module where the item is sourced from. +Here, this means that the items brought over to the lea module would +have to be capable of being addressed by their absolute paths, except +for the first segment of the path, which in this instance is "invisible" +because it is the crate root. In other words, the codata pattern +detection and addition to the list of addressable paths of the item must +happen after the item is renamed to reflect the path of its new parent +module. That doesn't currently happen nor can it possibly take place in +the single-use resolution function because we do not keep track of the +module where the use declaration was sourced from. That would mean that +specific piece of logic would have to be moved to the merging function. +This does change the plan outlined in the bullet points above. + +* Change the path field of all parsed items into accepting a set of + addressable paths instead of a single path. + +* Change the merge function to perform another step after renaming and + before extending the module into which to merge. This step should + consist of looking up each module to merge in search of the module + into which to merge within the subtree rooted at them. + + This would ensure that the module into which to merge is not found + nested within the subtree it is a root of. + +This strategy has some fairly large flaws as it stands. We considered +changing the super subcase of the name and path cases of the single-use +resolution function, but that is not a great idea. See, that function +takes into account some stateful pair of a subtree and the whole-crate +module tree, as well as some use declaration. For all we know, once we +unwind from the leaf module to the node module after munching through +the first super segment, we will also remove the node module off of the +crate root after stumbling upon the second super segment. That would +return back to the leaf module a crate root that does not contain the +node module. The resulting module tree is incorrect. One solution to +this would be to extensively refactor the case of super-relative +segments. Instead of individually munching through each segment, and +yielding the immediate parent before moving on to the next segment, all +super segments could be munched at once. Basically, it would go like +this. + +* Have some auxiliary function that was used as soon as a super-relative + import were detected. This function would eagerly munch through all + super segments of the path, but only the super segments. As it went on + munching, it would count the number of super segments, and yield this + number upon hitting the basecase. The basecases here would be hit once + there were either no more segments left in the path, or the next + segment is a non-super-relative segment. + +* The returned number would indicate the amount of times we would have + to fetch the parent module of the current stateful subtree. This would + yield the target module after all super-relative segments are + processed. + + This target module can then be used in the module fetching function to + obtain the "last" ancestor module the use declaration points at. This + module would then be rid of the current stateful subtree by attempting + to look up the latter in the former. This is the key part in avoiding + having some module nested within itself. + +So that's the non-termination problem solved. The next problem is about +dealing with the codata pattern. This we could potentially also solve by +integrating it into the above scheme. We know that the items brought +over from the use declaration have to be addressable by a nested path. +But that is only the first part of the story. Thus far, we've been +considering fairly couth though still pathological cases. What about a +super-relative import that, after going up to some ancestor module, goes +back down into some unrelated branch of the whole-crate module tree? +Consider the following code snippet example, as well as the accompanying +graph. + +```rust +mod foo { + mod bar { + use super::super::baz::*; + } +} + +mod baz { + pub struct Baz; +} +``` + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> foo & baz + foo ==> bar + bar --> baz +``` + +In this case, we would want the module tree post-resolution to look as +follows. + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> foo & baz + foo ==> bar + bar ==> baz2[baz] + bar --> baz2[baz] + subgraph n[" "] + baz2 + end +``` + +The module resolution strategy would reach module bar, find the use +declaration, and assuming the eager super munching strategy were used, +it would first fetch the crate root, then it would strip bar off of the +subtree in the crate root, and then it would continue munching through +the rest of the super-relative use declaration, which points at the baz +module (a child of the crate root.) The next case that would be hit in +the single-use resolution function would be the name basecase, which +would refer to module baz. This would then be looked up in the stateful +subtree carried across passes, and it would be successfully resolved. +All items from baz would be imported over to the bar module in the end. + +Further attempts at resolution would go through the same motions. +Namely, and just in case, the crate root would be fetched anew, the bar +module would be wiped off of its subtree, and the baz module would be +found in the next call to the single-use resolution function. Then it +would bring over all the items from module baz, which would be +deduplicated with those already brought over from the first resolution +pass. + +Now, how would this work with a super-relative glob import in the baz +module? Suppose the baz module always comes second in the resolutions +made of the child modules to the crate root. This would imply that the +foo module would be processed first, and thus the bar module's use +declaration would be processed first as well. The code snippet for this +example and the accompanying graph would look as follows. + +```rust +mod foo { + mod bar { + use super::super::baz::*; + + const FOO: baz::baz::baz::baz::Foo = Foo; + } +} + +pub struct Foo; + +mod baz { + pub use super::*; + + pub const BAZ: baz::baz::baz::baz::Foo = Foo; +} +``` + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> foo + root ==> baz + foo ==> bar + bar --> baz + baz --> root +``` + +In this case, we would want to have the end graph look as follows. + +```mermaid +--- +config: + layout: elk +--- +graph + root[Crate root] ==> foo + root ==> baz + foo ==> bar + bar ==> foo3[foo] + baz ==> foo2[foo] + foo2 ==> bar2[bar] + bar --> baz + baz --> root + subgraph n[" "] + foo2 + bar2 + end + subgraph n2[" "] + foo3 + end +``` + +Resolution would follow first by going down the path where the bar +module lies at. This would attempt to resolve the super-relative +declaration that points towards the items of the baz module. That would +yield no items whatsoever, because the baz module is still empty. Then +resolution would reach the baz module, where it would attempt to resolve +the crate root. It would resolve the super-relative module, then get rid +of the baz module in the subtree rooted at the crate root, and bring +over all remaining items. We're only considering modules in the graph, +so that would bring over the foo module with the bar module nested +within it. The use declaration in the baz module would bring over the +foo module with the baz module within it, and, of course, all cleansed +of use declarations. + +That would just about finish the first resolution pass, after which the +resolution driver would conclude that another pass would be in order. + +The second pass would go through the bar module's use declaration again, +but this time we would find that it would not remove itself off of the +foo module child that is part of the items in baz. In other words, the +graph for the ideal end-state would not be reached; The bar module would +end up with itself nested, which means we actually haven't solved just +yet the non-termination issue. Or does it? Recall how the proposed +super-relative resolution proceeds. We munch through all super segments +of the use declaration, and upon hitting the "last" ancestor module that +the use declaration points at, we traverse the entire subtree rooted at +that module to remove all occurrences of the stateful subtree in the +single-use resolution function. This would mean that both the "original" +bar and the one imported in the baz module as part of its use +declaration would be removed. Then, single-use resolution would follow +up with the last non-super segment of the path, which would point at +module baz and then at all items in module baz. This would yield only +module foo, because at this point we've been following resolution using +as stateful subtree the crate root without any ocurrences of module bar. + +This does imply there is no need to change anything from the strategy +followed thus far. So long as the super munching actually works, then we +should be gold even in cases where the super-relative use declaration +doesn't immediately glob-import all items from the ancestor module. From 52366709bd0b85f163d1525574a5884a78a963cb Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:25:35 +0200 Subject: [PATCH 073/100] chore: wip --- ctest/src/ast/constant.rs | 11 ++++------- ctest/src/ast/function.rs | 11 ++++------- ctest/src/ast/module.rs | 17 ++++++----------- ctest/src/ast/static_variable.rs | 11 ++++------- ctest/src/ast/structure.rs | 12 +++++------- ctest/src/ast/type_alias.rs | 12 +++++------- ctest/src/ast/union.rs | 11 ++++------- ctest/src/ffi_items.rs | 21 ++++++++++++++++++--- ctest/src/generator.rs | 12 ++++++------ 9 files changed, 56 insertions(+), 62 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index 7eb1c56f2e009..a146949cfa204 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) cached_path: BoxStr, + pub(crate) cached_ident: BoxStr, pub(crate) path: syn::Path, pub(crate) ty: syn::Type, } @@ -20,12 +21,8 @@ impl Const { &self.cached_path } - /// 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() + /// Returns the last segment of the constant's path; Namely, its identifier. + pub fn ident(&self) -> &str { + &self.cached_ident } } diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index 176c06ea6d1ad..56f22acf671e9 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) cached_path: BoxStr, + pub(crate) cached_ident: BoxStr, pub(crate) path: syn::Path, pub(crate) link_name: Option, #[expect(unused)] @@ -32,13 +33,9 @@ impl Fn { &self.cached_path } - /// 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() + /// Returns the last segment of the function's path; Namely, its identifier. + pub fn ident(&self) -> &str { + &self.cached_ident } /// Return the name of the function to be linked C side with. diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index b807a7561b8ae..6a9c600ccdec3 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,10 +1,7 @@ use std::cmp::Ordering; +use syn::Path; use syn::punctuated::Punctuated; -use syn::{ - Path, - PathSegment, -}; use crate::BoxStr; use crate::ffi_items::FfiItems; @@ -15,6 +12,7 @@ use crate::ffi_items::FfiItems; pub struct Module { pub(crate) public: bool, pub(crate) cached_path: BoxStr, + pub(crate) cached_ident: BoxStr, pub(crate) path: Path, pub(crate) items: FfiItems, } @@ -30,13 +28,9 @@ impl Module { &self.cached_path } - /// Returns the last path of the identifier, from the absolute path returned - /// by [`Module::path`]. - pub fn ident(&self) -> String { - let Some(PathSegment { ident, .. }) = self.path.segments.last() else { - unreachable!("all parsed items have at least one element in their path") - }; - ident.to_string() + /// Returns the last segment of the module's path; Namely, its identifier. + pub fn ident(&self) -> &str { + &self.cached_ident } /// Comparison function for use when sorting modules in [`FfiItems::dedup`]. @@ -56,6 +50,7 @@ impl Default for Module { Self { public: bool::default(), cached_path: String::new().into_boxed_str(), + cached_ident: String::new().into_boxed_str(), path: Path { leading_colon: None, segments: Punctuated::new(), diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index d82c1d997e302..653b2e1e407a2 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) cached_path: BoxStr, + pub(crate) cached_ident: BoxStr, pub(crate) path: syn::Path, pub(crate) link_name: Option, pub(crate) ty: syn::Type, @@ -29,13 +30,9 @@ impl Static { &self.cached_path } - /// 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() + /// Returns the last segment of the static's path; Namely, its identifier. + pub fn ident(&self) -> &str { + &self.cached_ident } /// Return the name of the function to be linked C side with. diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index d5f840eb933f7..211dbd05b2c81 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -11,6 +11,7 @@ use crate::{ pub struct Struct { pub(crate) public: bool, pub(crate) cached_path: BoxStr, + pub(crate) cached_ident: BoxStr, pub(crate) path: syn::Path, pub(crate) fields: Vec, } @@ -26,13 +27,9 @@ impl Struct { &self.cached_path } - /// 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() + /// Returns the last segment of the struct's path; Namely, its identifier. + pub fn ident(&self) -> &str { + &self.cached_ident } } @@ -41,6 +38,7 @@ impl Default for Struct { Self { public: bool::default(), cached_path: String::new().into_boxed_str(), + cached_ident: String::new().into_boxed_str(), path: Path { leading_colon: None, segments: Punctuated::new(), diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index 31445cad15ee9..d4cc901037445 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) cached_path: BoxStr, + pub(crate) cached_ident: BoxStr, pub(crate) path: syn::Path, pub(crate) ty: syn::Type, } @@ -20,12 +21,9 @@ impl Type { &self.cached_path } - /// 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() + /// Returns the last segment of the type alias's path; Namely, its + /// identifier. + pub fn ident(&self) -> &str { + &self.cached_ident } } diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index 4e83d06c34f04..1fa9e8166cc17 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) cached_path: BoxStr, + pub(crate) cached_ident: BoxStr, pub(crate) path: syn::Path, pub(crate) fields: Vec, } @@ -23,12 +24,8 @@ impl Union { &self.cached_path } - /// 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() + /// Returns the last segment of the union's path; Namely, its identifier. + pub fn ident(&self) -> &str { + &self.cached_ident } } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 4c8e2d965ea82..6bf26dd293878 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1145,6 +1145,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi let abi = abi.clone(); let path = append_path(table.current_module.clone(), i.sig.ident.clone()); let cached_path = path_to_string(path.clone()); + let cached_ident = i.sig.ident.to_string().into_boxed_str(); let parameters = i .sig .inputs @@ -1180,6 +1181,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi public, abi, cached_path, + cached_ident, path, link_name, parameters, @@ -1192,6 +1194,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a let abi = abi.clone(); let path = append_path(table.current_module.clone(), i.ident.clone()); let cached_path = path_to_string(path.clone()); + let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); let link_name = extract_single_link_name(&i.attrs); @@ -1199,6 +1202,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a public, abi, cached_path, + cached_ident, path, link_name, ty, @@ -1209,11 +1213,13 @@ fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); let cached_path = path_to_string(path.clone()); + let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); table.aliases.push(Type { public, cached_path, + cached_ident, path, ty, }); @@ -1223,6 +1229,7 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); let cached_path = path_to_string(path.clone()); + let cached_ident = i.ident.to_string().into_boxed_str(); let fields = match &i.fields { syn::Fields::Named(fields) => collect_fields(&fields.named), syn::Fields::Unnamed(fields) => collect_fields(&fields.unnamed), @@ -1232,6 +1239,7 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { table.structs.push(Struct { public, cached_path, + cached_ident, path, fields, }); @@ -1241,11 +1249,13 @@ fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); let cached_path = path_to_string(path.clone()); + let cached_ident = i.ident.to_string().into_boxed_str(); let fields = collect_fields(&i.fields.named); table.unions.push(Union { public, cached_path, + cached_ident, path, fields, }); @@ -1255,11 +1265,13 @@ fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); let cached_path = path_to_string(path.clone()); + let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); table.constants.push(Const { public, cached_path, + cached_ident, path, ty, }); @@ -1302,7 +1314,8 @@ impl<'ast> Visit<'ast> for FfiItems { ); }; let is_crate_root = ident == "__ctest_root_mod"; - let running_items = if is_crate_root { + + let mut running_items = if is_crate_root { self.clone() } else { FfiItems { @@ -1310,7 +1323,7 @@ impl<'ast> Visit<'ast> for FfiItems { ..Default::default() } }; - let mut running_items = running_items.clone(); + let uses: Vec<_> = mod_items .iter() .cloned() @@ -1346,15 +1359,17 @@ impl<'ast> Visit<'ast> for FfiItems { }) .flatten() .collect(); + visit::visit_item_mod(&mut running_items, i); running_items.uses = uses.clone(); - let running_items = running_items.clone(); + if is_crate_root { *self = running_items.clone(); } else { self.modules.push(Module { public: matches!(vis, Visibility::Public(_)), cached_path: path_to_string(running_items.current_module.clone()), + cached_ident: ident.to_string().into_boxed_str(), path: running_items.current_module.clone(), items: running_items.clone(), }); diff --git a/ctest/src/generator.rs b/ctest/src/generator.rs index 6faf4182b23e9..717cb1166b062 100644 --- a/ctest/src/generator.rs +++ b/ctest/src/generator.rs @@ -1192,12 +1192,12 @@ impl TestGenerator { return mapped; } match item { - 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::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::StructField(_, f) => f.ident().to_string(), MapInput::UnionField(_, f) => f.ident().to_string(), MapInput::StructType(ty) => format!("struct {ty}"), From cd6739162ea2fdc60efbf1a6b0b617f522d6c3f4 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:04:47 +0200 Subject: [PATCH 074/100] chore: wip --- ctest/src/ast/constant.rs | 10 ++++++++-- ctest/src/ast/function.rs | 5 ++++- ctest/src/ast/module.rs | 11 +++++++---- ctest/src/ast/static_variable.rs | 10 ++++++++-- ctest/src/ast/structure.rs | 13 ++++++++----- ctest/src/ast/type_alias.rs | 5 ++++- ctest/src/ast/union.rs | 5 ++++- ctest/src/ffi_items.rs | 22 +++++++++++++++------- 8 files changed, 58 insertions(+), 23 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index a146949cfa204..4eead0c3d5d75 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -1,3 +1,8 @@ +use syn::{ + Path, + Type, +}; + use crate::BoxStr; /// Represents a constant variable defined in Rust. @@ -6,8 +11,9 @@ pub struct Const { pub(crate) public: bool, pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, - pub(crate) path: syn::Path, - pub(crate) ty: syn::Type, + pub(crate) path: Path, + pub(crate) addressable_paths: Vec, + pub(crate) ty: Type, } impl Const { diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index 56f22acf671e9..27fd0529ef119 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -1,3 +1,5 @@ +use syn::Path; + use crate::{ Abi, BoxStr, @@ -14,7 +16,8 @@ pub struct Fn { pub(crate) abi: Abi, pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, - pub(crate) path: syn::Path, + pub(crate) path: Path, + pub(crate) addressable_paths: Vec, pub(crate) link_name: Option, #[expect(unused)] pub(crate) parameters: Vec, diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 6a9c600ccdec3..894f8b7ac9f4e 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -14,6 +14,7 @@ pub struct Module { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, + pub(crate) addressable_paths: Vec, pub(crate) items: FfiItems, } @@ -47,14 +48,16 @@ impl Module { impl Default for Module { fn default() -> Self { + let dpath = Path { + leading_colon: None, + segments: Punctuated::new(), + }; Self { public: bool::default(), cached_path: String::new().into_boxed_str(), cached_ident: String::new().into_boxed_str(), - path: Path { - leading_colon: None, - segments: Punctuated::new(), - }, + path: dpath.clone(), + addressable_paths: vec![dpath.clone()], items: FfiItems::default(), } } diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index 653b2e1e407a2..f71dbe2c10ab4 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -1,3 +1,8 @@ +use syn::{ + Path, + Type, +}; + use crate::{ Abi, BoxStr, @@ -14,9 +19,10 @@ pub struct Static { pub(crate) abi: Abi, pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, - pub(crate) path: syn::Path, + pub(crate) path: Path, + pub(crate) addressable_paths: Vec, pub(crate) link_name: Option, - pub(crate) ty: syn::Type, + pub(crate) ty: Type, } impl Static { diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index 211dbd05b2c81..a942b11663f22 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -12,7 +12,8 @@ pub struct Struct { pub(crate) public: bool, pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, - pub(crate) path: syn::Path, + pub(crate) path: Path, + pub(crate) addressable_paths: Vec, pub(crate) fields: Vec, } @@ -35,14 +36,16 @@ impl Struct { impl Default for Struct { fn default() -> Self { + let dpath = Path { + leading_colon: None, + segments: Punctuated::new(), + }; Self { public: bool::default(), cached_path: String::new().into_boxed_str(), cached_ident: String::new().into_boxed_str(), - path: Path { - leading_colon: None, - segments: Punctuated::new(), - }, + path: dpath.clone(), + addressable_paths: vec![dpath.clone()], fields: vec![], } } diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index d4cc901037445..43b6880defedf 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -1,3 +1,5 @@ +use syn::Path; + use crate::BoxStr; /// Represents a type alias defined in Rust. @@ -6,7 +8,8 @@ pub struct Type { pub(crate) public: bool, pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, - pub(crate) path: syn::Path, + pub(crate) path: Path, + pub(crate) addressable_paths: Vec, pub(crate) ty: syn::Type, } diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index 1fa9e8166cc17..8eb0896de766d 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -1,3 +1,5 @@ +use syn::Path; + use crate::{ BoxStr, Field, @@ -9,7 +11,8 @@ pub struct Union { pub(crate) public: bool, pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, - pub(crate) path: syn::Path, + pub(crate) path: Path, + pub(crate) addressable_paths: Vec, pub(crate) fields: Vec, } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 6bf26dd293878..6c5a485300cc1 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1182,7 +1182,8 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi abi, cached_path, cached_ident, - path, + path: path.clone(), + addressable_paths: vec![path.clone()], link_name, parameters, return_type, @@ -1203,7 +1204,8 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a abi, cached_path, cached_ident, - path, + path: path.clone(), + addressable_paths: vec![path.clone()], link_name, ty, }); @@ -1220,7 +1222,8 @@ fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { public, cached_path, cached_ident, - path, + path: path.clone(), + addressable_paths: vec![path.clone()], ty, }); } @@ -1240,7 +1243,8 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { public, cached_path, cached_ident, - path, + path: path.clone(), + addressable_paths: vec![path.clone()], fields, }); } @@ -1256,7 +1260,8 @@ fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { public, cached_path, cached_ident, - path, + path: path.clone(), + addressable_paths: vec![path.clone()], fields, }); } @@ -1272,7 +1277,8 @@ fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { public, cached_path, cached_ident, - path, + path: path.clone(), + addressable_paths: vec![path.clone()], ty, }); } @@ -1366,11 +1372,13 @@ impl<'ast> Visit<'ast> for FfiItems { if is_crate_root { *self = running_items.clone(); } else { + let path = running_items.current_module.clone(); self.modules.push(Module { public: matches!(vis, Visibility::Public(_)), cached_path: path_to_string(running_items.current_module.clone()), cached_ident: ident.to_string().into_boxed_str(), - path: running_items.current_module.clone(), + path: path.clone(), + addressable_paths: vec![path.clone()], items: running_items.clone(), }); } From 6b1e4a5b3827370aeb3a30b93f75899641e2fb00 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:55:17 +0200 Subject: [PATCH 075/100] chore: wip --- ctest/src/template.rs | 82 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/ctest/src/template.rs b/ctest/src/template.rs index 7bb48285152f4..31aa9332536e9 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -13,11 +13,18 @@ use crate::translator::{ }; use crate::{ BoxStr, + Const, Field, + Fn, MapInput, + Module, Result, + Static, + Struct, TestGenerator, TranslationError, + Type, + Union, VolatileItemKind, ast, cdecl, @@ -694,10 +701,20 @@ impl<'a> TranslateHelper<'a> { fn filter_ffi_items(&mut self) { fn skipper(items: &mut FfiItems, generator: &TestGenerator) { let skipped = items.aliases.extract_if(.., |alias| { - generator - .skips - .iter() - .any(|f| f(&MapInput::CEnumType(alias.path()))) + // [NOTE]: the point here is to check whether a given alias, + // as an item that may exist in multiple places across the + // crate, has a skip set up using one of those possible paths + // (which differ for an item that exists in multiple places in + // the crate.) + let possible_aliases = alias.addressable_paths.clone().into_iter().map(|p| Type { + path: p.clone(), + ..alias.clone() + }); + generator.skips.iter().any(|f| { + possible_aliases + .clone() + .any(|it| f(&MapInput::CEnumType(it.path()))) + }) }); for item in skipped { @@ -707,10 +724,20 @@ impl<'a> TranslateHelper<'a> { } let skipped = items.constants.extract_if(.., |constant| { + // [NOTE]: ibid. + let possible_constants = + constant + .addressable_paths + .clone() + .into_iter() + .map(|p| Const { + path: p.clone(), + ..constant.clone() + }); generator.skips.iter().any(|f| { - f(&MapInput::CEnumType( - &constant.ty.to_token_stream().to_string(), - )) + possible_constants + .clone() + .any(|it| f(&MapInput::CEnumType(&it.ty.to_token_stream().to_string()))) }) }); @@ -721,10 +748,29 @@ impl<'a> TranslateHelper<'a> { } macro_rules! filter { - ($field:ident, $variant:ident, $label:literal) => {{ + ($field:ident: $ty:tt, $variant:ident, $label:literal) => {{ + // [NOTE]: ibid. let skipped = items.$field.extract_if(.., |item| { + let possible = item.addressable_paths.clone().into_iter().map(|p| $ty { + path: p.clone(), + ..item.clone() + }); + // [NOTE]: when checking whether the item is public or + // not, we need not traverse all possible variants of + // the item as imported throughout the crate's module + // tree. The visibility of the item remains the same no + // matter where it appears at. + // [TODO]: checking whether the individual occurrence of + // an item in a given module is public is correct. Now, + // we don't modify the visibility of copies of an item + // that exist thanks to a reexport, when we should + // because the visibility of the item is affected by the + // visibility of the use declaration. (generator.skip_private && !item.public) - || generator.skips.iter().any(|f| f(&MapInput::$variant(item))) + || generator + .skips + .iter() + .any(|f| possible.clone().any(|it| f(&MapInput::$variant(&it)))) }); for item in skipped { if generator.verbose_skip { @@ -734,13 +780,17 @@ impl<'a> TranslateHelper<'a> { }}; } - 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"); + // [FIXME]: it may be preferable to rename MapInput::Alias to + // MapInput::Type, and so avoid having to include another "argument" + // to the macro. All other item types use the same identifier for + // both the variant in MapInput as well as their own type. + filter!(aliases: Type, Alias, "alias"); + filter!(constants: Const, Const, "const"); + filter!(structs: Struct, Struct, "struct"); + filter!(unions: Union, Union, "union"); + filter!(foreign_functions: Fn, Fn, "fn"); + filter!(foreign_statics: Static, Static, "static"); + filter!(modules: Module, Module, "module"); // [NOTE]: after dropping the modules that should be skipped from // `items`, we can safely iterate through whichever ones remain. From bbb379b126b82d4d254004b433c31df397196a32 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:00:28 +0200 Subject: [PATCH 076/100] chore: wip --- ctest/src/ffi_items.rs | 78 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 6c5a485300cc1..969368f485e43 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -575,6 +575,49 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { }) }; } + // [NOTE]: this ensures any returned item has the visibility of the use + // declaration, and doesn't preserve its own visibility. + macro_rules! change_vis { + ($it:ident : $ty:tt) => { + $ty { + public: r#use.is_public, + ..$it.clone() + } + }; + } + let change_vis_in_mod = |m: Module| { + macro_rules! c { + ($($field:ident: $ty:tt),+ $(,)?) => {$( + let $field: Vec<_> = m.items.$field + .clone() + .into_iter() + .map(|it| change_vis!(it: $ty)) + .collect(); + )+}; + } + c! { + aliases: Type, + constants: Const, + structs: Struct, + unions: Union, + foreign_functions: Fn, + foreign_statics: Static, + modules: Module, + } + Module { + items: FfiItems { + aliases, + constants, + structs, + unions, + foreign_functions, + foreign_statics, + modules, + ..m.items.clone() + }, + ..m.clone() + } + }; match r#use.tree.clone() { RefinedUseTree::Name(UseName { ident }) => { println!( @@ -593,7 +636,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { // declaration under no circumstances, so that case is not // considered. if is_self(ident.clone()) { - single_item!(modules: state.subtree.clone()); + single_item!(modules: change_vis_in_mod(state.subtree.clone())); } else if is_super(ident.clone()) { let parent_path = get_parent_path(state.subtree.clone()); let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) @@ -610,16 +653,18 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { considered malformed input." ); }; - single_item!(modules: parent.clone()); + single_item!(modules: change_vis_in_mod(parent.clone())); } match state.subtree.items.search(ident.clone()) { - Some(GenericItem::Type(t)) => single_item!(aliases: t.clone()), - Some(GenericItem::Struct(s)) => single_item!(structs: s.clone()), - Some(GenericItem::Union(u)) => single_item!(unions: u.clone()), - Some(GenericItem::Const(c)) => single_item!(constants: c.clone()), - Some(GenericItem::Fn(f)) => single_item!(foreign_functions: f.clone()), - Some(GenericItem::Static(s)) => single_item!(foreign_statics: s.clone()), - Some(GenericItem::Module(m)) => single_item!(modules: m.clone()), + Some(GenericItem::Type(t)) => single_item!(aliases: change_vis!(t: Type)), + Some(GenericItem::Struct(s)) => single_item!(structs: change_vis!(s: Struct)), + Some(GenericItem::Union(u)) => single_item!(unions: change_vis!(u: Union)), + Some(GenericItem::Const(c)) => single_item!(constants: change_vis!(c: Const)), + Some(GenericItem::Fn(f)) => single_item!(foreign_functions: change_vis!(f: Fn)), + Some(GenericItem::Static(s)) => { + single_item!(foreign_statics: change_vis!(s: Static)) + } + Some(GenericItem::Module(m)) => single_item!(modules: change_vis!(m: Module)), None => Resolution::Unresolved, } } @@ -632,14 +677,14 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { state.subtree.cached_path.clone() }, ); - Resolution::Resolved(state.subtree.items.clone()) + Resolution::Resolved(change_vis_in_mod(state.subtree.clone()).items) } RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { println!( "entered rename resolution case with {:?} in module {}", UseRename { ident: ident.clone(), - as_token: syn::Token![as](Span::call_site()), + as_token: syn::parse_quote! {as}, rename: rename.clone(), }, state.subtree.cached_path.clone() @@ -663,6 +708,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { ($field:ident , $ty:tt : $it:expr) => {{ let new_path = new_path!($it.clone()); let new_item = $ty { + public: r#use.is_public, cached_path: path_to_string(new_path.clone()), path: new_path.clone(), ..$it.clone() @@ -689,7 +735,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { }) }; if is_self(ident.clone()) { - single_item!(modules: renamer(state.subtree.clone())); + single_item!(modules: change_vis_in_mod(renamer(state.subtree.clone()))); } else if is_super(ident.clone()) { let parent_path = get_parent_path(state.subtree.clone()); let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) @@ -699,12 +745,14 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { applies here." ); }; - single_item!(modules: renamer(parent.clone())); + single_item!(modules: change_vis_in_mod(renamer(parent.clone()))); } else if is_crate(ident.clone()) { - single_item!(modules: renamer(state.whole_crate.clone())); + single_item!(modules: change_vis_in_mod(renamer(state.whole_crate.clone()))); } match state.subtree.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => single_item!(modules: renamer(m.clone())), + Some(GenericItem::Module(m)) => { + single_item!(modules: change_vis_in_mod(renamer(m.clone()))) + } Some(GenericItem::Type(t)) => renamed_single_item!(aliases, Type: t.clone()), Some(GenericItem::Struct(s)) => renamed_single_item!(structs, Struct: s.clone()), Some(GenericItem::Union(u)) => renamed_single_item!(unions, Union: u.clone()), From 2f0d41fefb7b9254ffb3768f8e8bde0b735bb79d Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:05:22 +0200 Subject: [PATCH 077/100] chore: wip --- ctest/src/ast/mod.rs | 16 +++++- ctest/src/ast/module.rs | 34 ++++++++++++- ctest/src/ffi_items.rs | 110 ++++++++++++++++++---------------------- ctest/src/template.rs | 9 +--- 4 files changed, 99 insertions(+), 70 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 816357f10971e..7b7550ec0d5a6 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -15,12 +15,17 @@ pub use field::Field; pub use function::Fn; pub use module::Module; pub use parameter::Parameter; +use quote::ToTokens; pub use static_variable::Static; pub use structure::Struct; +use syn::Path; pub use type_alias::Type; pub use union::Union; -use crate::MapInput; +use crate::{ + BoxStr, + MapInput, +}; /// Transforms an item's absolute path to use `_` as path separator instead of /// `::`. @@ -38,6 +43,15 @@ pub(crate) fn escape_item_path<'a>(item: impl Into>) -> String { path.replace("::", "_") } +/// Returns a stringified `syn::Path` that is meant to match one-to-one the path +/// to the item from the crate root. +pub(crate) fn path_to_string(path: Path) -> BoxStr { + path.into_token_stream() + .to_string() + .replace(|c: char| c.is_ascii_whitespace(), "") + .into_boxed_str() +} + /// The ABI as defined by the extern block. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Abi { diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 894f8b7ac9f4e..a1de9cfd712ff 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,10 +1,14 @@ use std::cmp::Ordering; +use proc_macro2::Punct; use syn::Path; use syn::punctuated::Punctuated; -use crate::BoxStr; use crate::ffi_items::FfiItems; +use crate::{ + BoxStr, + ast, +}; /// Represents a Rust module. `ctest` only considers items for which there is a /// corresponding type in this crate. @@ -34,6 +38,34 @@ impl Module { &self.cached_ident } + /// Renames the receiver's path with `new_path`, ensuring that both the + /// caches and the aggregate of addressable paths are updated. + // [TODO]: use this throughout the ffi_items module to replace the repeated + // occurrences of manual renaming implementations. + pub(crate) fn rename_path(self, new_path: Path) -> Self { + let Module { + addressable_paths, .. + } = self.clone(); + Self { + cached_path: ast::path_to_string(new_path.clone()), + cached_ident: new_path + .segments + .clone() + .last() + .expect("msg") + .ident + .to_string() + .into_boxed_str(), + path: new_path.clone(), + addressable_paths: addressable_paths + .clone() + .into_iter() + .chain([new_path]) + .collect(), + ..self.clone() + } + } + /// Comparison function for use when sorting modules in [`FfiItems::dedup`]. /// /// This is not implemented as part of [`Ord`] because the implementation is diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 969368f485e43..473cbf93c6e21 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -9,7 +9,6 @@ use std::ops::{ }; use proc_macro2::Span; -use quote::ToTokens; use syn::punctuated::Punctuated; use syn::token::{ Brace, @@ -46,6 +45,7 @@ use crate::{ Struct, Type, Union, + ast, }; enum GenericItem { @@ -709,7 +709,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { let new_path = new_path!($it.clone()); let new_item = $ty { public: r#use.is_public, - cached_path: path_to_string(new_path.clone()), + cached_path: ast::path_to_string(new_path.clone()), path: new_path.clone(), ..$it.clone() }; @@ -824,18 +824,17 @@ fn get_module(target: Path, root: Module) -> Option { /// returned path will be empty. This follows from the parsing strategy for /// paths in `FfiItems`. The absolute paths are all `crate`-_unprepended_. fn get_parent_path(root: Module) -> Path { - // [NOTE]: this performs a `saturating_sub` because there is one and only - // one item in the crate's module tree that has an empty path; The crate - // root. That is only wrapped in a `Module` of its own only after parsing. - // This function currently only gets used during parsing, so there is a - // chance this gets called with the virtual module that wraps the crate - // root's `FfiItems`. That is not logically possible, because well-formed - // input can not have a `use` declaration with `super` in the path at the - // crate root. This function is used when stumbling upon `super` in function - // `resolve_use`. + // [NOTE]: this performs a saturating_sub because there is one and only one + // item in the crate's module tree that has an empty path; The crate root. + // That is only wrapped in a Module of its own only after parsing. This + // function currently only gets used during parsing, so there is a chance + // this gets called with the virtual module that wraps the crate root's + // FfiItems. That is not logically possible, because well-formed input can + // not have a use declaration with super in the path at the crate root. This + // function is used when stumbling upon super in function resolve_use. // // The above assumption does not change the way this is implemented, except - // you could very well change the `saturating_sub` for overflowing + // you could very well change the saturating_sub for overflowing // subtraction. It is best if this function is protected against what would // currently be misuse. let new_segments = root @@ -847,8 +846,7 @@ fn get_parent_path(root: Module) -> Path { .fold(Punctuated::new(), |p, s| { let mut p = p.clone(); p.push(s); - let p = p.clone(); - p + p.clone() }); Path { leading_colon: None, @@ -903,34 +901,35 @@ fn update_tree(src: Module, dst: Module) -> Module { /// /// The subtree root module itself also gets the transform applied to its path. /// The `cached_path` field of each item is updated accordingly. -fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> Path + Clone) -> Module { - let new_root_path = f(root.path.clone()); - let new_root_cache = path_to_string(new_root_path.clone()); - macro_rules! map_items { - ($field:ident: $ty:tt) => {{ - root.items +fn manipulate_path(root: Module, f: impl ops::Fn(Path) -> Path + Clone) -> Module { + let root_path = f(root.path.clone()); + let root_cache = ast::path_to_string(root_path.clone()); + macro_rules! map_non_modules { + ($($field:ident: $ty:tt),+ $(,)?) => {$( + let $field = root.items .$field .clone() .into_iter() .map(|it| { let new_path = f(it.path.clone()); - let new_cache = path_to_string(new_path.clone()); $ty { - cached_path: new_cache.clone(), + cached_path: ast::path_to_string(new_path.clone()), path: new_path.clone(), ..it.clone() } }) - .collect::>() - }}; + .collect::>(); + )+}; } - let new_aliases = map_items!(aliases: Type); - let new_structs = map_items!(structs: Struct); - let new_unions = map_items!(unions: Union); - let new_constants = map_items!(constants: Const); - let new_foreign_functions = map_items!(foreign_functions: Fn); - let new_foreign_statics = map_items!(foreign_statics: Static); - let new_modules: Vec<_> = root + map_non_modules! { + aliases: Type, + structs: Struct, + unions: Union, + constants: Const, + foreign_functions: Fn, + foreign_statics: Static, + } + let modules: Vec<_> = root .items .modules .clone() @@ -938,18 +937,18 @@ fn manipulate_path(root: Module, f: impl ops::Fn(syn::Path) -> Path + Clone) -> .map(|m| manipulate_path(m.clone(), f.clone())) .collect(); let new_items = FfiItems { - aliases: new_aliases.clone(), - structs: new_structs.clone(), - unions: new_unions.clone(), - constants: new_constants.clone(), - foreign_functions: new_foreign_functions.clone(), - foreign_statics: new_foreign_statics.clone(), - modules: new_modules.clone(), + aliases: aliases.clone(), + structs: structs.clone(), + unions: unions.clone(), + constants: constants.clone(), + foreign_functions: foreign_functions.clone(), + foreign_statics: foreign_statics.clone(), + modules: modules.clone(), ..Default::default() }; Module { - cached_path: new_root_cache.clone(), - path: new_root_path.clone(), + cached_path: root_cache.clone(), + path: root_path.clone(), items: new_items.clone(), ..root.clone() } @@ -1025,8 +1024,7 @@ fn merge_module(dst: Module, src: Vec) -> Module { ) }; let mut new_path = new_path.clone(); - new_path.segments.push(ident); - let new_path = new_path.clone(); + new_path.segments.push(ident.clone()); new_path.clone() }); let nested_modules_with_new_paths: Vec<_> = modules @@ -1046,8 +1044,7 @@ fn merge_module(dst: Module, src: Vec) -> Module { .skip_while(|s| *s != base_path) .fold(new_path.clone(), |p, s| { let mut p = p.clone(); - p.segments.push(s); - let p = p.clone(); + p.segments.push(s.clone()); p.clone() }) }) @@ -1073,7 +1070,7 @@ fn merge_module(dst: Module, src: Vec) -> Module { // [NOTE]: this removes all use declarations in all descendant // modules of the subtrees to merge because otherwise merged // modules can increase the number of use declarations in the - // crate's module tree, which causes non-termination. + // crate's module tree, which is one cause of non-termination. let new_nested_modules: Vec<_> = nested_modules_with_new_paths .clone() .into_iter() @@ -1127,15 +1124,6 @@ fn append_path(base: Path, new: Ident) -> Path { } } -/// 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: Path) -> BoxStr { - path.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 @@ -1192,7 +1180,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi let public = is_visible(&i.vis); let abi = abi.clone(); let path = append_path(table.current_module.clone(), i.sig.ident.clone()); - let cached_path = path_to_string(path.clone()); + let cached_path = ast::path_to_string(path.clone()); let cached_ident = i.sig.ident.to_string().into_boxed_str(); let parameters = i .sig @@ -1242,7 +1230,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a let public = is_visible(&i.vis); let abi = abi.clone(); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = path_to_string(path.clone()); + let cached_path = ast::path_to_string(path.clone()); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); let link_name = extract_single_link_name(&i.attrs); @@ -1262,7 +1250,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = path_to_string(path.clone()); + let cached_path = ast::path_to_string(path.clone()); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); @@ -1279,7 +1267,7 @@ fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = path_to_string(path.clone()); + let cached_path = ast::path_to_string(path.clone()); let cached_ident = i.ident.to_string().into_boxed_str(); let fields = match &i.fields { syn::Fields::Named(fields) => collect_fields(&fields.named), @@ -1300,7 +1288,7 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = path_to_string(path.clone()); + let cached_path = ast::path_to_string(path.clone()); let cached_ident = i.ident.to_string().into_boxed_str(); let fields = collect_fields(&i.fields.named); @@ -1317,7 +1305,7 @@ fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = path_to_string(path.clone()); + let cached_path = ast::path_to_string(path.clone()); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); @@ -1423,7 +1411,7 @@ impl<'ast> Visit<'ast> for FfiItems { let path = running_items.current_module.clone(); self.modules.push(Module { public: matches!(vis, Visibility::Public(_)), - cached_path: path_to_string(running_items.current_module.clone()), + cached_path: ast::path_to_string(running_items.current_module.clone()), cached_ident: ident.to_string().into_boxed_str(), path: path.clone(), addressable_paths: vec![path.clone()], diff --git a/ctest/src/template.rs b/ctest/src/template.rs index 31aa9332536e9..ced62533ec727 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -760,12 +760,6 @@ impl<'a> TranslateHelper<'a> { // the item as imported throughout the crate's module // tree. The visibility of the item remains the same no // matter where it appears at. - // [TODO]: checking whether the individual occurrence of - // an item in a given module is public is correct. Now, - // we don't modify the visibility of copies of an item - // that exist thanks to a reexport, when we should - // because the visibility of the item is affected by the - // visibility of the use declaration. (generator.skip_private && !item.public) || generator .skips @@ -783,7 +777,8 @@ impl<'a> TranslateHelper<'a> { // [FIXME]: it may be preferable to rename MapInput::Alias to // MapInput::Type, and so avoid having to include another "argument" // to the macro. All other item types use the same identifier for - // both the variant in MapInput as well as their own type. + // both the variant in MapInput as well as their own type. Or maybe + // go the other way around, and change Type to Alias. filter!(aliases: Type, Alias, "alias"); filter!(constants: Const, Const, "const"); filter!(structs: Struct, Struct, "struct"); From 979518f3d2bc0bfc83a9b3a20780ed1200ebe9a9 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:00:52 +0200 Subject: [PATCH 078/100] chore: wip --- ctest/src/ast/constant.rs | 27 +++++ ctest/src/ast/function.rs | 27 +++++ ctest/src/ast/mod.rs | 49 ++++++++- ctest/src/ast/module.rs | 173 ++++++++++++++++++++++++++----- ctest/src/ast/static_variable.rs | 27 +++++ ctest/src/ast/structure.rs | 27 +++++ ctest/src/ast/type_alias.rs | 27 +++++ ctest/src/ast/union.rs | 27 +++++ ctest/src/ffi_items.rs | 92 +++------------- 9 files changed, 372 insertions(+), 104 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index 4eead0c3d5d75..9e738df867913 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -4,6 +4,7 @@ use syn::{ }; use crate::BoxStr; +use crate::ast::RenameUtils; /// Represents a constant variable defined in Rust. #[derive(Debug, Clone, PartialEq)] @@ -32,3 +33,29 @@ impl Const { &self.cached_ident } } + +impl RenameUtils for Const { + fn addressable_paths(self) -> Vec { + self.addressable_paths.clone() + } + + fn path(self) -> Path { + self.path.clone() + } + + fn mk( + self, + cached_path: BoxStr, + cached_ident: BoxStr, + path: Path, + addressable_paths: Vec, + ) -> Self { + Self { + cached_path, + cached_ident, + path, + addressable_paths, + ..self.clone() + } + } +} diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index 27fd0529ef119..256e598c73e61 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -1,5 +1,6 @@ use syn::Path; +use crate::ast::RenameUtils; use crate::{ Abi, BoxStr, @@ -46,3 +47,29 @@ impl Fn { self.link_name.as_deref() } } + +impl RenameUtils for Fn { + fn addressable_paths(self) -> Vec { + self.addressable_paths.clone() + } + + fn path(self) -> Path { + self.path.clone() + } + + fn mk( + self, + cached_path: BoxStr, + cached_ident: BoxStr, + path: Path, + addressable_paths: Vec, + ) -> Self { + Self { + cached_path, + cached_ident, + path, + addressable_paths, + ..self.clone() + } + } +} diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 7b7550ec0d5a6..02701b8d6c1dd 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -8,7 +8,10 @@ mod structure; mod type_alias; mod union; -use std::fmt; +use std::{ + fmt, + ops, +}; pub use constant::Const; pub use field::Field; @@ -18,7 +21,10 @@ pub use parameter::Parameter; use quote::ToTokens; pub use static_variable::Static; pub use structure::Struct; -use syn::Path; +use syn::{ + Path, + PathSegment, +}; pub use type_alias::Type; pub use union::Union; @@ -27,6 +33,45 @@ use crate::{ MapInput, }; +/// Utility interface for keeping a single renaming function for all parsed +/// items. +/// +/// They all share the same set of fields relative to their identifier and path, +/// all of which need updating whenever the path is renamed. We ensure that the +/// only place where this happens is in this function for convenience's sake. +pub(crate) trait RenameUtils: Clone { + fn path(self) -> Path; + fn addressable_paths(self) -> Vec; + fn mk( + self, + cached_path: BoxStr, + cached_ident: BoxStr, + path: Path, + addressable_paths: Vec, + ) -> Self; + + fn rename_with(self, new_path: impl ops::Fn(Path) -> Path + Clone) -> Self { + self.clone().rename(new_path(self.clone().path())) + } + + fn rename(self, new_path: Path) -> Self { + self.clone().mk( + path_to_string(new_path.clone()), + new_path + .segments + .last() + .map(|PathSegment { ident, .. }| ident.to_string().into_boxed_str()) + .unwrap_or_default(), + new_path.clone(), + self.addressable_paths() + .clone() + .into_iter() + .chain([new_path.clone()]) + .collect(), + ) + } +} + /// Transforms an item's absolute path to use `_` as path separator instead of /// `::`. pub(crate) fn escape_item_path<'a>(item: impl Into>) -> String { diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index a1de9cfd712ff..81520eda07c51 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,12 +1,18 @@ use std::cmp::Ordering; -use proc_macro2::Punct; use syn::Path; use syn::punctuated::Punctuated; +use crate::ast::RenameUtils; use crate::ffi_items::FfiItems; use crate::{ BoxStr, + Const, + Fn, + Static, + Struct, + Type, + Union, ast, }; @@ -38,42 +44,161 @@ impl Module { &self.cached_ident } - /// Renames the receiver's path with `new_path`, ensuring that both the - /// caches and the aggregate of addressable paths are updated. - // [TODO]: use this throughout the ffi_items module to replace the repeated - // occurrences of manual renaming implementations. - pub(crate) fn rename_path(self, new_path: Path) -> Self { - let Module { - addressable_paths, .. - } = self.clone(); + /// Comparison function for use when sorting modules in [`FfiItems::dedup`]. + /// + /// This is not implemented as part of [`Ord`] because the implementation is + /// inconsistent with the [`PartialEq`] implementation. + pub(crate) fn custom_cmp(&self, other: &Self) -> Ordering { + match self.cached_path.cmp(&other.cached_path) { + Ordering::Equal => self.items.custom_cmp(&other.items), + other => other, + } + } +} + +impl RenameUtils for Module { + fn addressable_paths(self) -> Vec { + self.addressable_paths.clone() + } + + fn path(self) -> Path { + self.path.clone() + } + + fn mk( + self, + cached_path: BoxStr, + cached_ident: BoxStr, + path: Path, + addressable_paths: Vec, + ) -> Self { Self { + cached_path, + cached_ident, + path, + addressable_paths, + ..self.clone() + } + } + + fn rename(self, new_path: Path) -> Self { + let new_path_len = new_path.segments.len(); + macro_rules! rename_non_modules { + ($($field:ident),+ $(,)?) => {$( + let $field = self.items + .$field + .clone() + .into_iter() + .map(|it| { + let mut new_path = new_path.clone(); + new_path + .segments + .extend(it.path.segments.clone().into_iter().skip(new_path_len)); + it.clone().rename(new_path) + }) + .collect(); + )+}; + } + rename_non_modules! { + aliases, + structs, + constants, + foreign_functions, + foreign_statics, + unions, + } + let modules = self + .items + .modules + .clone() + .into_iter() + .map(|it| { + it.rename_with(|it| { + let mut new_path = new_path.clone(); + new_path + .segments + .extend(it.segments.clone().into_iter().skip(new_path_len)); + new_path + }) + }) + .collect(); + Module { cached_path: ast::path_to_string(new_path.clone()), cached_ident: new_path .segments - .clone() .last() - .expect("msg") - .ident - .to_string() - .into_boxed_str(), + .map(|s| s.ident.to_string().into_boxed_str()) + .unwrap_or_default(), path: new_path.clone(), - addressable_paths: addressable_paths + addressable_paths: self + .addressable_paths .clone() .into_iter() - .chain([new_path]) + .chain([new_path.clone()]) .collect(), + items: FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + ..self.items.clone() + }, ..self.clone() } } - /// Comparison function for use when sorting modules in [`FfiItems::dedup`]. - /// - /// This is not implemented as part of [`Ord`] because the implementation is - /// inconsistent with the [`PartialEq`] implementation. - pub(crate) fn custom_cmp(&self, other: &Self) -> Ordering { - match self.cached_path.cmp(&other.cached_path) { - Ordering::Equal => self.items.custom_cmp(&other.items), - other => other, + fn rename_with(self, f: impl std::ops::Fn(Path) -> Path + Clone) -> Self { + let root_path = f(self.path.clone()); + let root_cache = ast::path_to_string(root_path.clone()); + macro_rules! map_non_modules { + ($($field:ident: $ty:tt),+ $(,)?) => {$( + let $field = self.items + .$field + .clone() + .into_iter() + .map(|it| it.clone().rename(f(it.path.clone()))) + .collect::>(); + )+}; + } + map_non_modules! { + aliases: Type, + structs: Struct, + unions: Union, + constants: Const, + foreign_functions: Fn, + foreign_statics: Static, + } + let modules: Vec<_> = self + .items + .modules + .clone() + .into_iter() + .map(|m| m.rename_with(f.clone())) + .collect(); + let new_items = FfiItems { + aliases: aliases.clone(), + structs: structs.clone(), + unions: unions.clone(), + constants: constants.clone(), + foreign_functions: foreign_functions.clone(), + foreign_statics: foreign_statics.clone(), + modules: modules.clone(), + ..Default::default() + }; + Module { + cached_path: root_cache.clone(), + path: root_path.clone(), + addressable_paths: self + .addressable_paths + .clone() + .into_iter() + .chain([root_path.clone()]) + .collect(), + items: new_items.clone(), + ..self.clone() } } } diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index f71dbe2c10ab4..58fb2cb6780d1 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -3,6 +3,7 @@ use syn::{ Type, }; +use crate::ast::RenameUtils; use crate::{ Abi, BoxStr, @@ -46,3 +47,29 @@ impl Static { self.link_name.as_deref() } } + +impl RenameUtils for Static { + fn addressable_paths(self) -> Vec { + self.addressable_paths.clone() + } + + fn path(self) -> Path { + self.path.clone() + } + + fn mk( + self, + cached_path: BoxStr, + cached_ident: BoxStr, + path: Path, + addressable_paths: Vec, + ) -> Self { + Self { + cached_path, + cached_ident, + path, + addressable_paths, + ..self.clone() + } + } +} diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index a942b11663f22..dd58d427bc432 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -1,6 +1,7 @@ use syn::Path; use syn::punctuated::Punctuated; +use crate::ast::RenameUtils; use crate::{ BoxStr, Field, @@ -34,6 +35,32 @@ impl Struct { } } +impl RenameUtils for Struct { + fn addressable_paths(self) -> Vec { + self.addressable_paths.clone() + } + + fn path(self) -> Path { + self.path.clone() + } + + fn mk( + self, + cached_path: BoxStr, + cached_ident: BoxStr, + path: Path, + addressable_paths: Vec, + ) -> Self { + Self { + cached_path, + cached_ident, + path, + addressable_paths, + ..self.clone() + } + } +} + impl Default for Struct { fn default() -> Self { let dpath = Path { diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index 43b6880defedf..1a1b12b6cb045 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -1,6 +1,7 @@ use syn::Path; use crate::BoxStr; +use crate::ast::RenameUtils; /// Represents a type alias defined in Rust. #[derive(Debug, Clone, PartialEq)] @@ -30,3 +31,29 @@ impl Type { &self.cached_ident } } + +impl RenameUtils for Type { + fn addressable_paths(self) -> Vec { + self.addressable_paths.clone() + } + + fn path(self) -> Path { + self.path.clone() + } + + fn mk( + self, + cached_path: BoxStr, + cached_ident: BoxStr, + path: Path, + addressable_paths: Vec, + ) -> Self { + Self { + cached_path, + cached_ident, + path, + addressable_paths, + ..self.clone() + } + } +} diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index 8eb0896de766d..22199bb61ea74 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -1,5 +1,6 @@ use syn::Path; +use crate::ast::RenameUtils; use crate::{ BoxStr, Field, @@ -32,3 +33,29 @@ impl Union { &self.cached_ident } } + +impl RenameUtils for Union { + fn addressable_paths(self) -> Vec { + self.addressable_paths.clone() + } + + fn path(self) -> Path { + self.path.clone() + } + + fn mk( + self, + cached_path: BoxStr, + cached_ident: BoxStr, + path: Path, + addressable_paths: Vec, + ) -> Self { + Self { + cached_path, + cached_ident, + path, + addressable_paths, + ..self.clone() + } + } +} diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 473cbf93c6e21..8c06977758d4d 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -3,7 +3,6 @@ use std::cmp::Ordering; use std::iter; use std::ops::{ - self, ControlFlow, Deref, }; @@ -33,6 +32,7 @@ use syn::{ Visibility, }; +use crate::ast::RenameUtils; use crate::{ Abi, BoxStr, @@ -697,7 +697,6 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { ident: rename.clone(), arguments: PathArguments::None, }); - let new_segments = new_segments.clone(); Path { segments: new_segments.clone(), ..$it.path.clone() @@ -707,19 +706,14 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! renamed_single_item { ($field:ident , $ty:tt : $it:expr) => {{ let new_path = new_path!($it.clone()); - let new_item = $ty { - public: r#use.is_public, - cached_path: ast::path_to_string(new_path.clone()), - path: new_path.clone(), - ..$it.clone() - }; + let new_item = $it.rename(new_path); single_item!($field: new_item.clone()); }}; } let renamer = |m: Module| { let new_path = new_path!(m.clone()); let new_path_len = new_path.segments.len(); - manipulate_path(m.clone(), |p| { + m.rename_with(|p| { let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( new_path.clone(), |p, s| { @@ -897,63 +891,6 @@ fn update_tree(src: Module, dst: Module) -> Module { } } -/// Applies a transform `f` to the path of each item in subtree `root`. -/// -/// The subtree root module itself also gets the transform applied to its path. -/// The `cached_path` field of each item is updated accordingly. -fn manipulate_path(root: Module, f: impl ops::Fn(Path) -> Path + Clone) -> Module { - let root_path = f(root.path.clone()); - let root_cache = ast::path_to_string(root_path.clone()); - macro_rules! map_non_modules { - ($($field:ident: $ty:tt),+ $(,)?) => {$( - let $field = root.items - .$field - .clone() - .into_iter() - .map(|it| { - let new_path = f(it.path.clone()); - $ty { - cached_path: ast::path_to_string(new_path.clone()), - path: new_path.clone(), - ..it.clone() - } - }) - .collect::>(); - )+}; - } - map_non_modules! { - aliases: Type, - structs: Struct, - unions: Union, - constants: Const, - foreign_functions: Fn, - foreign_statics: Static, - } - let modules: Vec<_> = root - .items - .modules - .clone() - .into_iter() - .map(|m| manipulate_path(m.clone(), f.clone())) - .collect(); - let new_items = FfiItems { - aliases: aliases.clone(), - structs: structs.clone(), - unions: unions.clone(), - constants: constants.clone(), - foreign_functions: foreign_functions.clone(), - foreign_statics: foreign_statics.clone(), - modules: modules.clone(), - ..Default::default() - }; - Module { - cached_path: root_cache.clone(), - path: root_path.clone(), - items: new_items.clone(), - ..root.clone() - } -} - /// Integrates the resolution items of `src` into `dst`. /// /// Each resolution item is either one of an `Unresolved` or a `Resolved` @@ -985,6 +922,15 @@ fn merge_module(dst: Module, src: Vec) -> Module { modules, .. }| { + let items_without_modules = FfiItems { + aliases: aliases.clone(), + structs: structs.clone(), + unions: unions.clone(), + constants: constants.clone(), + foreign_functions: foreign_functions.clone(), + foreign_statics: foreign_statics.clone(), + ..Default::default() + }; // [NOTE]: this sequence must have at least one segment, because // the invariant assumed in the closure for path manipulation of // the items to merge assumes so. This is true for all items to @@ -998,16 +944,6 @@ fn merge_module(dst: Module, src: Vec) -> Module { ident: Ident::new("dummy", Span::call_site()), arguments: PathArguments::None, }); - let dummy_segment = dummy_segment.clone(); - let items_without_modules = FfiItems { - aliases: aliases.clone(), - structs: structs.clone(), - unions: unions.clone(), - constants: constants.clone(), - foreign_functions: foreign_functions.clone(), - foreign_statics: foreign_statics.clone(), - ..Default::default() - }; let module = Module { path: Path { leading_colon: None, @@ -1016,7 +952,7 @@ fn merge_module(dst: Module, src: Vec) -> Module { items: items_without_modules.clone(), ..Default::default() }; - let new_module = manipulate_path(module.clone(), |p| { + let new_module = module.rename_with(|p| { let Some(ident) = p.segments.last().cloned() else { unreachable!( "All parsed items have at least one segment in \ @@ -1037,7 +973,7 @@ fn merge_module(dst: Module, src: Vec) -> Module { segment in their paths." ); }; - manipulate_path(m.clone(), |p| { + m.rename_with(|p| { p.segments .clone() .into_iter() From cd78d96e2204d78ac87cbd27647aad39933a9b1a Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:13:10 +0200 Subject: [PATCH 079/100] chore: wip --- ctest/src/ast/module.rs | 114 ++++++++++----------------------- ctest/src/ffi_items.rs | 138 +++++++++++++++++++--------------------- 2 files changed, 101 insertions(+), 151 deletions(-) diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 81520eda07c51..e960f31245c26 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,18 +1,16 @@ use std::cmp::Ordering; +use std::ops; -use syn::Path; use syn::punctuated::Punctuated; +use syn::{ + Path, + PathSegment, +}; use crate::ast::RenameUtils; use crate::ffi_items::FfiItems; use crate::{ BoxStr, - Const, - Fn, - Static, - Struct, - Type, - Union, ast, }; @@ -81,78 +79,28 @@ impl RenameUtils for Module { } } + // [NOTE]: this is implemented in terms of rename_with but it doesn't just + // yield the passed path, because that would mean all items would end up + // with the same path. Instead, it calls into rename_with and skips as many + // segments of all paths in the subtree as the current module has. Then it + // appends whatever segments are left to the new path (new_path) we got + // passed here. In the case of the module on which this was called, that + // means all segments will be skipped and nothing will be appended to the + // new path. In all other cases of items within its subtree, they will have + // the previous identifier of the ancestor in which this rename request was + // originally called trimmed, and whatever segments are left will be + // appended to the new path of that (potentially grand-) ancestor. fn rename(self, new_path: Path) -> Self { - let new_path_len = new_path.segments.len(); - macro_rules! rename_non_modules { - ($($field:ident),+ $(,)?) => {$( - let $field = self.items - .$field - .clone() - .into_iter() - .map(|it| { - let mut new_path = new_path.clone(); - new_path - .segments - .extend(it.path.segments.clone().into_iter().skip(new_path_len)); - it.clone().rename(new_path) - }) - .collect(); - )+}; - } - rename_non_modules! { - aliases, - structs, - constants, - foreign_functions, - foreign_statics, - unions, - } - let modules = self - .items - .modules - .clone() - .into_iter() - .map(|it| { - it.rename_with(|it| { - let mut new_path = new_path.clone(); - new_path - .segments - .extend(it.segments.clone().into_iter().skip(new_path_len)); - new_path - }) - }) - .collect(); - Module { - cached_path: ast::path_to_string(new_path.clone()), - cached_ident: new_path - .segments - .last() - .map(|s| s.ident.to_string().into_boxed_str()) - .unwrap_or_default(), - path: new_path.clone(), - addressable_paths: self - .addressable_paths - .clone() - .into_iter() - .chain([new_path.clone()]) - .collect(), - items: FfiItems { - aliases, - structs, - unions, - constants, - foreign_functions, - foreign_statics, - modules, - ..self.items.clone() - }, - ..self.clone() - } + let old_path_len = self.path.segments.len(); + self.clone().rename_with(|p| { + let segments_post_ancestor = p.segments.clone().into_iter().skip(old_path_len); + let mut new_path = new_path.clone(); + new_path.segments.extend(segments_post_ancestor); + new_path.clone() + }) } - fn rename_with(self, f: impl std::ops::Fn(Path) -> Path + Clone) -> Self { - let root_path = f(self.path.clone()); - let root_cache = ast::path_to_string(root_path.clone()); + fn rename_with(self, f: impl ops::Fn(Path) -> Path + Clone) -> Self { macro_rules! map_non_modules { ($($field:ident: $ty:tt),+ $(,)?) => {$( let $field = self.items @@ -176,7 +124,7 @@ impl RenameUtils for Module { .modules .clone() .into_iter() - .map(|m| m.rename_with(f.clone())) + .map(|m| m.clone().rename_with(f.clone())) .collect(); let new_items = FfiItems { aliases: aliases.clone(), @@ -188,14 +136,20 @@ impl RenameUtils for Module { modules: modules.clone(), ..Default::default() }; + let new_path = f(self.path.clone()); Module { - cached_path: root_cache.clone(), - path: root_path.clone(), + cached_path: ast::path_to_string(new_path.clone()), + cached_ident: new_path + .segments + .last() + .map(|PathSegment { ident, .. }| ident.to_string().into_boxed_str()) + .unwrap_or_default(), + path: new_path.clone(), addressable_paths: self .addressable_paths .clone() .into_iter() - .chain([root_path.clone()]) + .chain([new_path.clone()]) .collect(), items: new_items.clone(), ..self.clone() diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 8c06977758d4d..3277b94a2abb7 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -12,6 +12,7 @@ use syn::punctuated::Punctuated; use syn::token::{ Brace, Mod, + PathSep, }; use syn::visit::{ self, @@ -478,8 +479,7 @@ fn resolve_pass(src: PassPair) -> PassPair { }); let mut new_children = children.clone(); new_children.push(pair.subtree.clone()); - let new_children = new_children.clone(); - (pair, new_children) + (pair.clone(), new_children.clone()) }, ); let root_with_resolved_children = Module { @@ -691,12 +691,20 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { ); macro_rules! new_path { ($it:expr) => {{ - let mut new_segments = $it.path.segments.clone(); - new_segments.pop(); - new_segments.push(PathSegment { - ident: rename.clone(), - arguments: PathArguments::None, - }); + let new_segments = $it + .path + .segments + .clone() + .into_iter() + .skip($it.path.segments.len() - 1) + .fold(Punctuated::new(), |p, _| { + let mut p = p.clone(); + p.push(PathSegment { + ident: rename.clone(), + arguments: PathArguments::None, + }); + p.clone() + }); Path { segments: new_segments.clone(), ..$it.path.clone() @@ -706,14 +714,21 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! renamed_single_item { ($field:ident , $ty:tt : $it:expr) => {{ let new_path = new_path!($it.clone()); - let new_item = $it.rename(new_path); + let new_item = $it.rename(new_path.clone()); single_item!($field: new_item.clone()); }}; } let renamer = |m: Module| { let new_path = new_path!(m.clone()); + // [NOTE]: it may seem better to use the segment length of the + // old path of the module, much like we do in function + // RenameUtils::rename, but that is not necessary here. We know + // the segment length will be the same before and after the + // rename because use declarations can only rename the module's + // identifier. The module identifier only accounts for one + // segment of the item's path. let new_path_len = new_path.segments.len(); - m.rename_with(|p| { + m.clone().rename_with(|p| { let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( new_path.clone(), |p, s| { @@ -787,8 +802,8 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { }; resolve_use( RefinedUse { - is_public: r#use.is_public, tree: *tree.clone(), + ..r#use }, PassPair { subtree: new_subtree.clone(), @@ -922,70 +937,49 @@ fn merge_module(dst: Module, src: Vec) -> Module { modules, .. }| { - let items_without_modules = FfiItems { - aliases: aliases.clone(), - structs: structs.clone(), - unions: unions.clone(), - constants: constants.clone(), - foreign_functions: foreign_functions.clone(), - foreign_statics: foreign_statics.clone(), - ..Default::default() + macro_rules! seek_parent { + ($field:ident) => {{ + $field.first().map(|it| { + it.path + .segments + .clone() + .into_iter() + .take(it.path.segments.len() - 1) + .collect::>() + }) + }}; + } + // [NOTE]: this ensures we find the immediate parent to any one + // of the items about to be merged. If there really are no items + // about to be merged, then we can simply return an empty + // FfiItems from the mapping, which would essentially merge no + // items down in the fold that finishes up the merging function. + // We don't invoke unreachable! in the diverging branch because + // a resolution item could pack an empty item container if a use + // declaration consists a glob import of all items in a module + // with no items. + let Some(source_module_path) = seek_parent!(aliases) + .or_else(|| seek_parent!(structs)) + .or_else(|| seek_parent!(unions)) + .or_else(|| seek_parent!(constants)) + .or_else(|| seek_parent!(foreign_functions)) + .or_else(|| seek_parent!(foreign_statics)) + .or_else(|| seek_parent!(modules)) + else { + return FfiItems::default(); }; - // [NOTE]: this sequence must have at least one segment, because - // the invariant assumed in the closure for path manipulation of - // the items to merge assumes so. This is true for all items to - // merge, but this module wrapping all those items is only - // virtual in nature; The containig items (post-path-renaming) - // will be extracted, and the module trashed. But the module, - // even though a dummy, will also have the closure further down - // below applied to its path, so it needs to be non-empty. - let mut dummy_segment = Punctuated::new(); - dummy_segment.push(PathSegment { - ident: Ident::new("dummy", Span::call_site()), - arguments: PathArguments::None, - }); - let module = Module { + let dummy_module = Module { path: Path { leading_colon: None, - segments: dummy_segment.clone(), + segments: { + let mut base = Punctuated::new(); + base.extend(source_module_path); + base.clone() + }, }, - items: items_without_modules.clone(), ..Default::default() }; - let new_module = module.rename_with(|p| { - let Some(ident) = p.segments.last().cloned() else { - unreachable!( - "All parsed items have at least one segment in \ - their paths." - ) - }; - let mut new_path = new_path.clone(); - new_path.segments.push(ident.clone()); - new_path.clone() - }); - let nested_modules_with_new_paths: Vec<_> = modules - .clone() - .into_iter() - .map(|m| { - let Some(base_path) = m.path.segments.last().cloned() else { - unreachable!( - "All items post-parsing have at least one \ - segment in their paths." - ); - }; - m.rename_with(|p| { - p.segments - .clone() - .into_iter() - .skip_while(|s| *s != base_path) - .fold(new_path.clone(), |p, s| { - let mut p = p.clone(); - p.segments.push(s.clone()); - p.clone() - }) - }) - }) - .collect(); + let new_module = dummy_module.clone().rename(new_path.clone()); fn remove_uses(m: Module) -> Module { let new_modules: Vec<_> = m .items @@ -1007,13 +1001,15 @@ fn merge_module(dst: Module, src: Vec) -> Module { // modules of the subtrees to merge because otherwise merged // modules can increase the number of use declarations in the // crate's module tree, which is one cause of non-termination. - let new_nested_modules: Vec<_> = nested_modules_with_new_paths + let new_modules_without_uses: Vec<_> = new_module + .items + .modules .clone() .into_iter() .map(remove_uses) .collect(); FfiItems { - modules: new_nested_modules.clone(), + modules: new_modules_without_uses.clone(), ..new_module.items.clone() } }, From b8b9d7db6cc6e85496c5bd1a02816f0a3c97d9d2 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:33:55 +0200 Subject: [PATCH 080/100] chore: wip --- ctest/src/ast/mod.rs | 23 +++++++++++++++ ctest/src/ffi_items.rs | 23 ++++++++++++--- ctest/src/template.rs | 66 +++++++++++++++++++----------------------- ctest/src/tests.rs | 1 - 4 files changed, 72 insertions(+), 41 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 02701b8d6c1dd..0e9dec3a39832 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -50,6 +50,29 @@ pub(crate) trait RenameUtils: Clone { addressable_paths: Vec, ) -> Self; + /// Provided some parsed type, this method will run the given closure on + /// each variation of a term of that type (`self`.) + /// + /// The variations of a given term are given by their addressable paths. + /// This is an append-only aggregate all implementors of this interface have + /// as one of their projections. The variations are themselves the possible + /// paths by which an item may be referred to as a consequence of having + /// been imported over to other modules of the crate. + /// + /// This interface gets access to it through the + /// [`RenameUtils::addressable_paths`] projection. + /// + /// This is a utility to avoid having to go through the variations, as well + /// as updating the identifier and paths caches whenever some operation + /// needs to be performed on all variations. + fn with_variations(&self, f: impl ops::Fn(Self) -> T + Clone) -> Vec { + self.clone() + .addressable_paths() + .into_iter() + .map(|p| f(self.clone().rename(p.clone()))) + .collect() + } + fn rename_with(self, new_path: impl ops::Fn(Path) -> Path + Clone) -> Self { self.clone().rename(new_path(self.clone().path())) } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 3277b94a2abb7..0eb4e1efcd745 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -977,6 +977,16 @@ fn merge_module(dst: Module, src: Vec) -> Module { base.clone() }, }, + items: FfiItems { + aliases: aliases.clone(), + structs: structs.clone(), + unions: unions.clone(), + constants: constants.clone(), + foreign_functions: foreign_functions.clone(), + foreign_statics: foreign_statics.clone(), + modules: modules.clone(), + ..Default::default() + }, ..Default::default() }; let new_module = dummy_module.clone().rename(new_path.clone()); @@ -1358,7 +1368,9 @@ fn tmp() { let source = r#" use test2::test3; -use test1::{*, test2::test3::*}; +use test2::*; + +use test1::{*, test3::*}; mod test1 { use test2::{*, test3::*}; @@ -1391,6 +1403,7 @@ mod test1 { } #[test] +#[ignore] fn tmp1() { let source = r#" pub struct Foo; @@ -1404,6 +1417,7 @@ mod test1 { use super::*; } } #[test] +#[ignore] fn tmp2() { let m1 = Module { cached_path: String::from("a").into_boxed_str(), @@ -1424,6 +1438,7 @@ fn tmp2() { } #[test] +#[ignore] fn tmp3() { let dst_child_path = Path { leading_colon: None, @@ -1440,7 +1455,7 @@ fn tmp3() { items: FfiItems { modules: Vec::from([Module { path: dst_child_path.clone(), - cached_path: path_to_string(dst_child_path.clone()), + cached_path: ast::path_to_string(dst_child_path.clone()), items: FfiItems { structs: Vec::from([{ let path = Path { @@ -1449,7 +1464,7 @@ fn tmp3() { }; Struct { path: path.clone(), - cached_path: path_to_string(path.clone()), + cached_path: ast::path_to_string(path.clone()), ..Default::default() } }]), @@ -1464,7 +1479,7 @@ fn tmp3() { let src = Vec::from([Resolution::Resolved(FfiItems { modules: Vec::from([Module { path: dst_child_path.clone(), - cached_path: path_to_string(dst_child_path.clone()), + cached_path: ast::path_to_string(dst_child_path.clone()), items: FfiItems { modules: Vec::from([Module::default()]), uses: Vec::from([RefinedUse::default()]), diff --git a/ctest/src/template.rs b/ctest/src/template.rs index ced62533ec727..7b5c11a153a32 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -1,10 +1,13 @@ //! Generation of tests from templates for both Rust and C. +use std::convert; + use askama::Template; use proc_macro2::Span; use quote::ToTokens; use syn::spanned::Spanned; +use crate::ast::RenameUtils; use crate::cdecl::Constness; use crate::ffi_items::FfiItems; use crate::translator::{ @@ -701,49 +704,40 @@ impl<'a> TranslateHelper<'a> { fn filter_ffi_items(&mut self) { fn skipper(items: &mut FfiItems, generator: &TestGenerator) { let skipped = items.aliases.extract_if(.., |alias| { - // [NOTE]: the point here is to check whether a given alias, - // as an item that may exist in multiple places across the - // crate, has a skip set up using one of those possible paths - // (which differ for an item that exists in multiple places in - // the crate.) - let possible_aliases = alias.addressable_paths.clone().into_iter().map(|p| Type { - path: p.clone(), - ..alias.clone() - }); + // [NOTE]: the point here is to check whether a given alias (as + // an item that may exist in multiple places across the crate) + // has a skip set up using one of those possible paths (which + // differ for an item that exists in multiple places in the + // crate.) generator.skips.iter().any(|f| { - possible_aliases - .clone() - .any(|it| f(&MapInput::CEnumType(it.path()))) + alias + .with_variations(|it| f(&MapInput::CEnumType(Type::path(&it)))) + .into_iter() + .any(convert::identity) }) }); for item in skipped { if generator.verbose_skip { - eprintln!("Skipping C enum type {}", item.path()); + eprintln!("Skipping C enum type {}", Type::path(&item)); } } let skipped = items.constants.extract_if(.., |constant| { // [NOTE]: ibid. - let possible_constants = + generator.skips.iter().any(|f| { constant - .addressable_paths - .clone() + .with_variations(|it| { + f(&MapInput::CEnumType(&it.ty.to_token_stream().to_string())) + }) .into_iter() - .map(|p| Const { - path: p.clone(), - ..constant.clone() - }); - generator.skips.iter().any(|f| { - possible_constants - .clone() - .any(|it| f(&MapInput::CEnumType(&it.ty.to_token_stream().to_string()))) + .any(convert::identity) }) }); for item in skipped { if generator.verbose_skip { - eprintln!("Skipping C enum constant {}", item.path()); + eprintln!("Skipping C enum constant {}", Const::path(&item)); } } @@ -751,24 +745,24 @@ impl<'a> TranslateHelper<'a> { ($field:ident: $ty:tt, $variant:ident, $label:literal) => {{ // [NOTE]: ibid. let skipped = items.$field.extract_if(.., |item| { - let possible = item.addressable_paths.clone().into_iter().map(|p| $ty { - path: p.clone(), - ..item.clone() - }); // [NOTE]: when checking whether the item is public or // not, we need not traverse all possible variants of // the item as imported throughout the crate's module - // tree. The visibility of the item remains the same no - // matter where it appears at. + // tree. The visibility of the item differs across its + // appearances in the module tree, so skipping an item + // because of its visibility in one module should not + // affect the same (possibly public) item in another + // module. (generator.skip_private && !item.public) - || generator - .skips - .iter() - .any(|f| possible.clone().any(|it| f(&MapInput::$variant(&it)))) + || generator.skips.iter().any(|f| { + item.with_variations(|it| f(&MapInput::$variant(&it))) + .into_iter() + .any(convert::identity) + }) }); for item in skipped { if generator.verbose_skip { - eprintln!("Skipping {} \"{}\"", $label, item.path()) + eprintln!("Skipping {} \"{}\"", $label, $ty::path(&item)) } } }}; diff --git a/ctest/src/tests.rs b/ctest/src/tests.rs index ce07d696e556c..5e4a09e5a7265 100644 --- a/ctest/src/tests.rs +++ b/ctest/src/tests.rs @@ -1,5 +1,4 @@ use syn::spanned::Spanned; -use syn::visit::Visit; use crate::ffi_items::FfiItems; use crate::translator::{ From f0fd893ac81be040b3e8a0e16e403aeaa3fa918a Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:00:52 +0200 Subject: [PATCH 081/100] chore: wip --- ctest/src/ffi_items.rs | 222 +++++++++++++++++++++++++---------------- 1 file changed, 134 insertions(+), 88 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 0eb4e1efcd745..50a1b073d243d 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -493,13 +493,24 @@ fn resolve_pass(src: PassPair) -> PassPair { subtree: root_with_resolved_children.clone(), whole_crate: whole_crate_with_resolved_children.clone(), }); - let new_root_with_duplicates = - merge_module(root_with_resolved_children.clone(), resolved_uses.clone()); + let PassPair { + subtree: new_root_with_duplicates, + whole_crate: new_whole_crate_with_resolved_children, + } = merge_module( + PassPair { + subtree: root_with_resolved_children.clone(), + whole_crate: whole_crate_with_resolved_children.clone(), + }, + resolved_uses.clone(), + ); let new_root = Module { items: new_root_with_duplicates.items.clone().dedup(), ..new_root_with_duplicates.clone() }; - let new_whole_crate = update_tree(new_root.clone(), whole_crate_with_resolved_children.clone()); + let new_whole_crate = update_tree( + new_root.clone(), + new_whole_crate_with_resolved_children.clone(), + ); println!( "finished module pass on module {}", if &*new_root.cached_path == "" { @@ -915,8 +926,8 @@ fn update_tree(src: Module, dst: Module) -> Module { /// Note that, during merging, the items to be merged may end up duplicating /// existing items on `dst`. It is call-site's responsibility to deduplicate /// those through [`FfiItems::dedup`]. -fn merge_module(dst: Module, src: Vec) -> Module { - let new_path = dst.path.clone(); +fn merge_module(state: PassPair, src: Vec) -> PassPair { + let new_path = state.subtree.path.clone(); src.clone() .into_iter() .filter_map(|r| { @@ -938,7 +949,16 @@ fn merge_module(dst: Module, src: Vec) -> Module { .. }| { macro_rules! seek_parent { - ($field:ident) => {{ + ($($it:tt),+ $(,)?) => { + seek_parent!(@@ $($it)+) + }; + (@@ $head:tt $($tail:tt)*) => { + seek_parent!(@ $head).or_else(|| seek_parent!(@@ $($tail)*)) + }; + (@@) => { + None + }; + (@ $field:tt) => {{ $field.first().map(|it| { it.path .segments @@ -957,15 +977,18 @@ fn merge_module(dst: Module, src: Vec) -> Module { // We don't invoke unreachable! in the diverging branch because // a resolution item could pack an empty item container if a use // declaration consists a glob import of all items in a module - // with no items. - let Some(source_module_path) = seek_parent!(aliases) - .or_else(|| seek_parent!(structs)) - .or_else(|| seek_parent!(unions)) - .or_else(|| seek_parent!(constants)) - .or_else(|| seek_parent!(foreign_functions)) - .or_else(|| seek_parent!(foreign_statics)) - .or_else(|| seek_parent!(modules)) - else { + // with no items. We can be sure that all items about to be + // merged share the same parent because use declarations are + // cleansed of groups at parse-time. + let Some(source_module_path) = seek_parent!( + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + ) else { return FfiItems::default(); }; let dummy_module = Module { @@ -989,6 +1012,13 @@ fn merge_module(dst: Module, src: Vec) -> Module { }, ..Default::default() }; + let new_whole_crate = todo!( + "Use the whole crate module tree in the state to look up \ + all of the above items' path aggregates, and then, for \ + each item found with some path in their aggregate that is \ + equal to any path in the aggregate of the items about to \ + be merged, synchronize their path aggregates." + ); let new_module = dummy_module.clone().rename(new_path.clone()); fn remove_uses(m: Module) -> Module { let new_modules: Vec<_> = m @@ -1018,26 +1048,32 @@ fn merge_module(dst: Module, src: Vec) -> Module { .into_iter() .map(remove_uses) .collect(); - FfiItems { - modules: new_modules_without_uses.clone(), - ..new_module.items.clone() - } + ( + FfiItems { + modules: new_modules_without_uses.clone(), + ..new_module.items.clone() + }, + new_whole_crate.clone(), + ) }, ) .fold( - dst.clone(), - |dst, - FfiItems { - aliases, - structs, - unions, - constants, - foreign_functions, - foreign_statics, - modules, - .. - }| { - let mut dst = dst.clone(); + state.clone(), + |state, + ( + FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + .. + }, + new_whole_crate, + )| { + let mut dst = state.subtree.clone(); dst.items.aliases.extend_from_slice(&aliases); dst.items.structs.extend_from_slice(&structs); dst.items.unions.extend_from_slice(&unions); @@ -1049,11 +1085,21 @@ fn merge_module(dst: Module, src: Vec) -> Module { .foreign_statics .extend_from_slice(&foreign_statics); dst.items.modules.extend_from_slice(&modules); - dst.clone() + PassPair { + subtree: dst.clone(), + whole_crate: todo!( + "Use the whole-crate state that is returned in the map \ + once that is implemented." + ), + } }, ) } +fn find_same_path_aggregates(root: Module, path: Vec) -> Vec { + todo!() +} + /// Appends a new module-local item to an absolute path that does _not_ start /// with `crate`. /// @@ -1437,58 +1483,58 @@ fn tmp2() { println!("{:#?}", ms.clone()); } -#[test] -#[ignore] -fn tmp3() { - let dst_child_path = Path { - leading_colon: None, - segments: { - let mut out = Punctuated::new(); - out.push(PathSegment { - ident: Ident::new("foo", Span::call_site()), - arguments: PathArguments::None, - }); - out.clone() - }, - }; - let dst = Module { - items: FfiItems { - modules: Vec::from([Module { - path: dst_child_path.clone(), - cached_path: ast::path_to_string(dst_child_path.clone()), - items: FfiItems { - structs: Vec::from([{ - let path = Path { - leading_colon: None, - segments: Punctuated::new(), - }; - Struct { - path: path.clone(), - cached_path: ast::path_to_string(path.clone()), - ..Default::default() - } - }]), - ..Default::default() - }, - ..Default::default() - }]), - ..Default::default() - }, - ..Default::default() - }; - let src = Vec::from([Resolution::Resolved(FfiItems { - modules: Vec::from([Module { - path: dst_child_path.clone(), - cached_path: ast::path_to_string(dst_child_path.clone()), - items: FfiItems { - modules: Vec::from([Module::default()]), - uses: Vec::from([RefinedUse::default()]), - ..Default::default() - }, - ..Default::default() - }]), - ..Default::default() - })]); - let merged = merge_module(dst.clone(), src.clone()); - println!("{:#?}", merged.clone()); -} +// #[test] +// #[ignore] +// fn tmp3() { +// let dst_child_path = Path { +// leading_colon: None, +// segments: { +// let mut out = Punctuated::new(); +// out.push(PathSegment { +// ident: Ident::new("foo", Span::call_site()), +// arguments: PathArguments::None, +// }); +// out.clone() +// }, +// }; +// let dst = Module { +// items: FfiItems { +// modules: Vec::from([Module { +// path: dst_child_path.clone(), +// cached_path: ast::path_to_string(dst_child_path.clone()), +// items: FfiItems { +// structs: Vec::from([{ +// let path = Path { +// leading_colon: None, +// segments: Punctuated::new(), +// }; +// Struct { +// path: path.clone(), +// cached_path: ast::path_to_string(path.clone()), +// ..Default::default() +// } +// }]), +// ..Default::default() +// }, +// ..Default::default() +// }]), +// ..Default::default() +// }, +// ..Default::default() +// }; +// let src = Vec::from([Resolution::Resolved(FfiItems { +// modules: Vec::from([Module { +// path: dst_child_path.clone(), +// cached_path: ast::path_to_string(dst_child_path.clone()), +// items: FfiItems { +// modules: Vec::from([Module::default()]), +// uses: Vec::from([RefinedUse::default()]), +// ..Default::default() +// }, +// ..Default::default() +// }]), +// ..Default::default() +// })]); +// let merged = merge_module(dst.clone(), src.clone()); +// println!("{:#?}", merged.clone()); +// } From 80ad720ed19fdce54fcbb1917d6495a03b14088b Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:17:27 +0200 Subject: [PATCH 082/100] chore: wip --- ctest/src/ast/mod.rs | 12 +++ ctest/src/ffi_items.rs | 175 +++++++++++++++++++++++++++-------------- 2 files changed, 127 insertions(+), 60 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 0e9dec3a39832..1f3665224e488 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -50,6 +50,18 @@ pub(crate) trait RenameUtils: Clone { addressable_paths: Vec, ) -> Self; + /// Checks whether two implementors of the interface contain some common + /// path in their path aggregate. + fn akin_aggregates(self, other: Self) -> bool { + self.clone().addressable_paths().into_iter().any(|p| { + other + .clone() + .addressable_paths() + .into_iter() + .any(|p2| p == p2) + }) + } + /// Provided some parsed type, this method will run the given closure on /// each variation of a term of that type (`self`.) /// diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 50a1b073d243d..52fa79829c917 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -49,16 +49,6 @@ use crate::{ ast, }; -enum GenericItem { - Type(Type), - Struct(Struct), - Union(Union), - Const(Const), - Fn(Fn), - Static(Static), - Module(Module), -} - /// Represents a collected set of top-level Rust items relevant to FFI /// generation or analysis. /// @@ -158,6 +148,34 @@ impl FfiItems { *self = resolve_all(root_module.clone()).items.clone(); } + /// Yields an iterator over all items in the calling `FfiItems`. + /// + /// The items in the item container do not include `use` declarations. + fn items(&self) -> ItemIterator { + let base = self + .aliases + .clone() + .into_iter() + .map(GenericItem::Type) + .chain(self.structs.clone().into_iter().map(GenericItem::Struct)) + .chain(self.unions.clone().into_iter().map(GenericItem::Union)) + .chain(self.constants.clone().into_iter().map(GenericItem::Const)) + .chain( + self.foreign_functions + .clone() + .into_iter() + .map(GenericItem::Fn), + ) + .chain( + self.foreign_statics + .clone() + .into_iter() + .map(GenericItem::Static), + ) + .chain(self.modules.clone().into_iter().map(GenericItem::Module)); + Box::new(base) + } + /// Performs a comparison for equality between FfiItems without taking field /// current_module into consideration. /// @@ -219,49 +237,25 @@ impl FfiItems { /// /// The generic item may be pattern-matched to find out specifically which /// item was found. - fn search(&self, ident: syn::Ident) -> Option { - let aliases = self.aliases.clone().into_iter().map(GenericItem::Type); - let structs = self.structs.clone().into_iter().map(GenericItem::Struct); - let unions = self.unions.clone().into_iter().map(GenericItem::Union); - let constants = self.constants.clone().into_iter().map(GenericItem::Const); - let foreign_functions = self - .foreign_functions - .clone() - .into_iter() - .map(GenericItem::Fn); - let foreign_statics = self - .foreign_statics - .clone() - .into_iter() - .map(GenericItem::Static); - self.modules - .clone() - .into_iter() - .map(GenericItem::Module) - .chain(foreign_statics) - .chain(foreign_functions) - .chain(constants) - .chain(unions) - .chain(structs) - .chain(aliases) - .find(|it| { - macro_rules! cmp { - ($($var:tt),+ $(,)?) => { - match it {$( - GenericItem::$var(it) => it.ident() == ident.to_string(), - )+} - }; - } - cmp! { - Type, - Struct, - Union, - Const, - Fn, - Static, - Module, - } - }) + fn search(&self, ident: Ident) -> Option { + self.items().find(|it| { + macro_rules! cmp { + ($($var:tt),+ $(,)?) => { + match it {$( + GenericItem::$var(it) => it.ident() == ident.to_string(), + )+} + }; + } + cmp! { + Type, + Struct, + Union, + Const, + Fn, + Static, + Module, + } + }) } /// Deduplicates the items in an item container by their path. @@ -300,7 +294,7 @@ impl FfiItems { ); let mut new_modules = self.modules.clone(); new_modules.sort_unstable_by(Module::custom_cmp); - new_modules.dedup_by(|lhs, rhs| lhs.path == rhs.path); + new_modules.dedup_by(|Module { path: lhs, .. }, Module { path: rhs, .. }| *lhs == *rhs); FfiItems { modules: new_modules.clone(), ..new_items.clone() @@ -327,6 +321,21 @@ impl Default for FfiItems { } } +/// Sum over any item held within an `FfiItems` term. +#[derive(Clone)] +enum GenericItem { + Type(Type), + Struct(Struct), + Union(Union), + Const(Const), + Fn(Fn), + Static(Static), + Module(Module), +} + +/// An iterator over all items but `use` declarations in an `FfiItems` term. +type ItemIterator = Box>; + /// Refined type for [`syn::UsePath`] that does not consider group paths. /// /// This is a lot like [`RefinedUse`] in that it only exists as replacement for @@ -875,17 +884,17 @@ fn get_parent_path(root: Module) -> Path { } /// Checks if the given identifier was parsed as the Rust keyword `self`. -fn is_self(ident: syn::Ident) -> bool { +fn is_self(ident: Ident) -> bool { ident.to_string() == "self" } /// Checks if the given identifier was parsed as the Rust keyword `crate`. -fn is_crate(ident: syn::Ident) -> bool { +fn is_crate(ident: Ident) -> bool { ident.to_string() == "crate" } /// Checks if the given identifier was parsed as the Rust keyword `super`. -fn is_super(ident: syn::Ident) -> bool { +fn is_super(ident: Ident) -> bool { ident.to_string() == "super" } @@ -989,7 +998,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { foreign_statics, modules, ) else { - return FfiItems::default(); + return (FfiItems::default(), Module::default()); }; let dummy_module = Module { path: Path { @@ -1096,8 +1105,54 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { ) } -fn find_same_path_aggregates(root: Module, path: Vec) -> Vec { - todo!() +fn of_akin_aggregate( + root: Module, + reference: GenericItem, + matches: Vec, +) -> Vec { + let new_nested_matches: Vec<_> = root + .items + .modules + .clone() + .into_iter() + .flat_map(|it| of_akin_aggregate(it.clone(), reference.clone(), matches.clone())) + .collect(); + let new_matches = root + .items + .items() + .filter_map(|it| match (it.clone(), reference.clone()) { + (GenericItem::Type(_), GenericItem::Type(_)) + | (GenericItem::Struct(_), GenericItem::Struct(_)) + | (GenericItem::Union(_), GenericItem::Union(_)) + | (GenericItem::Const(_), GenericItem::Const(_)) + | (GenericItem::Fn(_), GenericItem::Fn(_)) + | (GenericItem::Static(_), GenericItem::Static(_)) + | (GenericItem::Module(_), GenericItem::Module(_)) => it.clone().into(), + (GenericItem::Type(_), _) + | (GenericItem::Struct(_), _) + | (GenericItem::Union(_), _) + | (GenericItem::Const(_), _) + | (GenericItem::Fn(_), _) + | (GenericItem::Static(_), _) + | (GenericItem::Module(_), _) => None, + }) + .filter(|it| match (it.clone(), reference.clone()) { + (GenericItem::Type(t), GenericItem::Type(r)) => r.akin_aggregates(t), + (GenericItem::Struct(s), GenericItem::Struct(r)) => r.akin_aggregates(s), + (GenericItem::Union(u), GenericItem::Union(r)) => r.akin_aggregates(u), + (GenericItem::Const(c), GenericItem::Const(r)) => r.akin_aggregates(c), + (GenericItem::Fn(f), GenericItem::Fn(r)) => r.akin_aggregates(f), + (GenericItem::Static(s), GenericItem::Static(r)) => r.akin_aggregates(s), + (GenericItem::Module(m), GenericItem::Module(r)) => r.akin_aggregates(m), + _ => unreachable!( + "The prior filter_map already ensured that the items in the \ + iterator match the reference item" + ), + }); + let mut matches = matches.clone(); + matches.extend(new_nested_matches); + matches.extend(new_matches); + matches.clone() } /// Appends a new module-local item to an absolute path that does _not_ start From c849ceeef166115c7ea50c627283b382483d5348 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:00:07 +0200 Subject: [PATCH 083/100] chore: wip --- ctest/src/ast/mod.rs | 27 ++++++++++++++++++++++++--- ctest/src/ffi_items.rs | 9 +++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 1f3665224e488..9e840a8eb514f 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -8,9 +8,10 @@ mod structure; mod type_alias; mod union; -use std::{ - fmt, - ops, +use std::fmt; +use std::ops::{ + self, + Not, }; pub use constant::Const; @@ -50,6 +51,26 @@ pub(crate) trait RenameUtils: Clone { addressable_paths: Vec, ) -> Self; + /// Diffs the path aggregates of both `self` and `other`, and yields a + /// unified path aggregate integrating the paths from both. + fn merge_aggregates(self, other: Self) -> Vec { + let we_need = other + .clone() + .addressable_paths() + .into_iter() + .filter(|a| { + self.clone() + .addressable_paths() + .into_iter() + .any(|b| *a == b) + .not() + }) + .collect::>(); + let mut ours = self.clone().addressable_paths(); + ours.extend(we_need); + ours + } + /// Checks whether two implementors of the interface contain some common /// path in their path aggregate. fn akin_aggregates(self, other: Self) -> bool { diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 52fa79829c917..21a82c0e7a6c3 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1105,20 +1105,21 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { ) } +/// Provided an item container, it searches for all items of the same kind, and +/// with a path aggregate that contains some matching path as the aggregate of +/// `reference`. fn of_akin_aggregate( - root: Module, + root: FfiItems, reference: GenericItem, matches: Vec, ) -> Vec { let new_nested_matches: Vec<_> = root - .items .modules .clone() .into_iter() - .flat_map(|it| of_akin_aggregate(it.clone(), reference.clone(), matches.clone())) + .flat_map(|it| of_akin_aggregate(it.items.clone(), reference.clone(), matches.clone())) .collect(); let new_matches = root - .items .items() .filter_map(|it| match (it.clone(), reference.clone()) { (GenericItem::Type(_), GenericItem::Type(_)) From 88895c9d2b6748d4f92f9fc3343c7bbd8153bd13 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:38:02 +0200 Subject: [PATCH 084/100] chore: wip --- ctest/src/ast/constant.rs | 8 +-- ctest/src/ast/function.rs | 8 +-- ctest/src/ast/mod.rs | 27 ++++----- ctest/src/ast/module.rs | 14 ++--- ctest/src/ast/static_variable.rs | 8 +-- ctest/src/ast/structure.rs | 10 ++-- ctest/src/ast/type_alias.rs | 8 +-- ctest/src/ast/union.rs | 8 +-- ctest/src/ffi_items.rs | 99 ++++++++++++++++++++------------ 9 files changed, 106 insertions(+), 84 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index 9e738df867913..c83ffd8349d1e 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -13,7 +13,7 @@ pub struct Const { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) addressable_paths: Vec, + pub(crate) path_aggregate: Vec, pub(crate) ty: Type, } @@ -35,8 +35,8 @@ impl Const { } impl RenameUtils for Const { - fn addressable_paths(self) -> Vec { - self.addressable_paths.clone() + fn aggregate_path(self) -> Vec { + self.path_aggregate.clone() } fn path(self) -> Path { @@ -54,7 +54,7 @@ impl RenameUtils for Const { cached_path, cached_ident, path, - addressable_paths, + path_aggregate: addressable_paths, ..self.clone() } } diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index 256e598c73e61..f406fee3a8a9d 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -18,7 +18,7 @@ pub struct Fn { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) addressable_paths: Vec, + pub(crate) path_aggregate: Vec, pub(crate) link_name: Option, #[expect(unused)] pub(crate) parameters: Vec, @@ -49,8 +49,8 @@ impl Fn { } impl RenameUtils for Fn { - fn addressable_paths(self) -> Vec { - self.addressable_paths.clone() + fn aggregate_path(self) -> Vec { + self.path_aggregate.clone() } fn path(self) -> Path { @@ -68,7 +68,7 @@ impl RenameUtils for Fn { cached_path, cached_ident, path, - addressable_paths, + path_aggregate: addressable_paths, ..self.clone() } } diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 9e840a8eb514f..5c87ce5e9043c 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -42,7 +42,7 @@ use crate::{ /// only place where this happens is in this function for convenience's sake. pub(crate) trait RenameUtils: Clone { fn path(self) -> Path; - fn addressable_paths(self) -> Vec; + fn aggregate_path(self) -> Vec; fn mk( self, cached_path: BoxStr, @@ -56,31 +56,28 @@ pub(crate) trait RenameUtils: Clone { fn merge_aggregates(self, other: Self) -> Vec { let we_need = other .clone() - .addressable_paths() + .aggregate_path() .into_iter() .filter(|a| { self.clone() - .addressable_paths() + .aggregate_path() .into_iter() .any(|b| *a == b) .not() }) .collect::>(); - let mut ours = self.clone().addressable_paths(); + let mut ours = self.clone().aggregate_path(); ours.extend(we_need); ours } - /// Checks whether two implementors of the interface contain some common - /// path in their path aggregate. + /// Checks whether two interface implementors contain some common path in + /// their path aggregate. fn akin_aggregates(self, other: Self) -> bool { - self.clone().addressable_paths().into_iter().any(|p| { - other - .clone() - .addressable_paths() - .into_iter() - .any(|p2| p == p2) - }) + self.clone() + .aggregate_path() + .into_iter() + .any(|p| other.clone().aggregate_path().into_iter().any(|p2| p == p2)) } /// Provided some parsed type, this method will run the given closure on @@ -100,7 +97,7 @@ pub(crate) trait RenameUtils: Clone { /// needs to be performed on all variations. fn with_variations(&self, f: impl ops::Fn(Self) -> T + Clone) -> Vec { self.clone() - .addressable_paths() + .aggregate_path() .into_iter() .map(|p| f(self.clone().rename(p.clone()))) .collect() @@ -119,7 +116,7 @@ pub(crate) trait RenameUtils: Clone { .map(|PathSegment { ident, .. }| ident.to_string().into_boxed_str()) .unwrap_or_default(), new_path.clone(), - self.addressable_paths() + self.aggregate_path() .clone() .into_iter() .chain([new_path.clone()]) diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index e960f31245c26..5433922eb72cf 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -22,7 +22,7 @@ pub struct Module { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) addressable_paths: Vec, + pub(crate) path_aggregate: Vec, pub(crate) items: FfiItems, } @@ -55,8 +55,8 @@ impl Module { } impl RenameUtils for Module { - fn addressable_paths(self) -> Vec { - self.addressable_paths.clone() + fn aggregate_path(self) -> Vec { + self.path_aggregate.clone() } fn path(self) -> Path { @@ -74,7 +74,7 @@ impl RenameUtils for Module { cached_path, cached_ident, path, - addressable_paths, + path_aggregate: addressable_paths, ..self.clone() } } @@ -145,8 +145,8 @@ impl RenameUtils for Module { .map(|PathSegment { ident, .. }| ident.to_string().into_boxed_str()) .unwrap_or_default(), path: new_path.clone(), - addressable_paths: self - .addressable_paths + path_aggregate: self + .path_aggregate .clone() .into_iter() .chain([new_path.clone()]) @@ -168,7 +168,7 @@ impl Default for Module { cached_path: String::new().into_boxed_str(), cached_ident: String::new().into_boxed_str(), path: dpath.clone(), - addressable_paths: vec![dpath.clone()], + path_aggregate: vec![dpath.clone()], items: FfiItems::default(), } } diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index 58fb2cb6780d1..c1a3df4df7846 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -21,7 +21,7 @@ pub struct Static { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) addressable_paths: Vec, + pub(crate) path_aggregate: Vec, pub(crate) link_name: Option, pub(crate) ty: Type, } @@ -49,8 +49,8 @@ impl Static { } impl RenameUtils for Static { - fn addressable_paths(self) -> Vec { - self.addressable_paths.clone() + fn aggregate_path(self) -> Vec { + self.path_aggregate.clone() } fn path(self) -> Path { @@ -68,7 +68,7 @@ impl RenameUtils for Static { cached_path, cached_ident, path, - addressable_paths, + path_aggregate: addressable_paths, ..self.clone() } } diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index dd58d427bc432..853e30c74b20d 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -14,7 +14,7 @@ pub struct Struct { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) addressable_paths: Vec, + pub(crate) path_aggregate: Vec, pub(crate) fields: Vec, } @@ -36,8 +36,8 @@ impl Struct { } impl RenameUtils for Struct { - fn addressable_paths(self) -> Vec { - self.addressable_paths.clone() + fn aggregate_path(self) -> Vec { + self.path_aggregate.clone() } fn path(self) -> Path { @@ -55,7 +55,7 @@ impl RenameUtils for Struct { cached_path, cached_ident, path, - addressable_paths, + path_aggregate: addressable_paths, ..self.clone() } } @@ -72,7 +72,7 @@ impl Default for Struct { cached_path: String::new().into_boxed_str(), cached_ident: String::new().into_boxed_str(), path: dpath.clone(), - addressable_paths: vec![dpath.clone()], + path_aggregate: vec![dpath.clone()], fields: vec![], } } diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index 1a1b12b6cb045..da7c62d979b8c 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -10,7 +10,7 @@ pub struct Type { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) addressable_paths: Vec, + pub(crate) path_aggregate: Vec, pub(crate) ty: syn::Type, } @@ -33,8 +33,8 @@ impl Type { } impl RenameUtils for Type { - fn addressable_paths(self) -> Vec { - self.addressable_paths.clone() + fn aggregate_path(self) -> Vec { + self.path_aggregate.clone() } fn path(self) -> Path { @@ -52,7 +52,7 @@ impl RenameUtils for Type { cached_path, cached_ident, path, - addressable_paths, + path_aggregate: addressable_paths, ..self.clone() } } diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index 22199bb61ea74..79b71aad7f9bd 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -13,7 +13,7 @@ pub struct Union { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) addressable_paths: Vec, + pub(crate) path_aggregate: Vec, pub(crate) fields: Vec, } @@ -35,8 +35,8 @@ impl Union { } impl RenameUtils for Union { - fn addressable_paths(self) -> Vec { - self.addressable_paths.clone() + fn aggregate_path(self) -> Vec { + self.path_aggregate.clone() } fn path(self) -> Path { @@ -54,7 +54,7 @@ impl RenameUtils for Union { cached_path, cached_ident, path, - addressable_paths, + path_aggregate: addressable_paths, ..self.clone() } } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 21a82c0e7a6c3..f4c0091632041 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1107,8 +1107,9 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { /// Provided an item container, it searches for all items of the same kind, and /// with a path aggregate that contains some matching path as the aggregate of -/// `reference`. -fn of_akin_aggregate( +/// `reference`, and ensures the returned items contain an updated path +/// aggregate that integrates both `reference`'s and theirs. +fn search_and_merge_aggregates( root: FfiItems, reference: GenericItem, matches: Vec, @@ -1117,38 +1118,62 @@ fn of_akin_aggregate( .modules .clone() .into_iter() - .flat_map(|it| of_akin_aggregate(it.items.clone(), reference.clone(), matches.clone())) + .flat_map(|it| { + search_and_merge_aggregates(it.items.clone(), reference.clone(), matches.clone()) + }) .collect(); let new_matches = root .items() - .filter_map(|it| match (it.clone(), reference.clone()) { - (GenericItem::Type(_), GenericItem::Type(_)) - | (GenericItem::Struct(_), GenericItem::Struct(_)) - | (GenericItem::Union(_), GenericItem::Union(_)) - | (GenericItem::Const(_), GenericItem::Const(_)) - | (GenericItem::Fn(_), GenericItem::Fn(_)) - | (GenericItem::Static(_), GenericItem::Static(_)) - | (GenericItem::Module(_), GenericItem::Module(_)) => it.clone().into(), - (GenericItem::Type(_), _) - | (GenericItem::Struct(_), _) - | (GenericItem::Union(_), _) - | (GenericItem::Const(_), _) - | (GenericItem::Fn(_), _) - | (GenericItem::Static(_), _) - | (GenericItem::Module(_), _) => None, + .filter_map(|it| { + macro_rules! exe { + ($($ty:tt),+ $(,)?) => {{ + match (it.clone(), reference.clone()) {$( + (GenericItem::$ty(iit), GenericItem::$ty(r)) => { + r.clone().akin_aggregates(iit.clone()).then_some(it.clone()) + } + )+ + _ => None, + } + }}; + } + exe! { + Type, + Struct, + Union, + Const, + Fn, + Static, + Module, + } }) - .filter(|it| match (it.clone(), reference.clone()) { - (GenericItem::Type(t), GenericItem::Type(r)) => r.akin_aggregates(t), - (GenericItem::Struct(s), GenericItem::Struct(r)) => r.akin_aggregates(s), - (GenericItem::Union(u), GenericItem::Union(r)) => r.akin_aggregates(u), - (GenericItem::Const(c), GenericItem::Const(r)) => r.akin_aggregates(c), - (GenericItem::Fn(f), GenericItem::Fn(r)) => r.akin_aggregates(f), - (GenericItem::Static(s), GenericItem::Static(r)) => r.akin_aggregates(s), - (GenericItem::Module(m), GenericItem::Module(r)) => r.akin_aggregates(m), - _ => unreachable!( - "The prior filter_map already ensured that the items in the \ - iterator match the reference item" - ), + .map(|it| { + macro_rules! exe { + ($($ty:tt),+ $(,)?) => {{ + match (it.clone(), reference.clone()) {$( + (GenericItem::$ty(iit), GenericItem::$ty(r)) => { + let new_path_aggregate = iit.clone().merge_aggregates(r.clone()); + GenericItem::$ty($ty { + path_aggregate: new_path_aggregate.clone(), + ..iit.clone() + }) + }, + )+ + _ => unreachable!( + "The above filter_map got rid of all items that \ + were not of the same kind as reference." + ), + } + }}; + } + exe! { + Type, + Struct, + Union, + Const, + Fn, + Static, + Module, + } }); let mut matches = matches.clone(); matches.extend(new_nested_matches); @@ -1263,7 +1288,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi cached_path, cached_ident, path: path.clone(), - addressable_paths: vec![path.clone()], + path_aggregate: vec![path.clone()], link_name, parameters, return_type, @@ -1285,7 +1310,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a cached_path, cached_ident, path: path.clone(), - addressable_paths: vec![path.clone()], + path_aggregate: vec![path.clone()], link_name, ty, }); @@ -1303,7 +1328,7 @@ fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { cached_path, cached_ident, path: path.clone(), - addressable_paths: vec![path.clone()], + path_aggregate: vec![path.clone()], ty, }); } @@ -1324,7 +1349,7 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { cached_path, cached_ident, path: path.clone(), - addressable_paths: vec![path.clone()], + path_aggregate: vec![path.clone()], fields, }); } @@ -1341,7 +1366,7 @@ fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { cached_path, cached_ident, path: path.clone(), - addressable_paths: vec![path.clone()], + path_aggregate: vec![path.clone()], fields, }); } @@ -1358,7 +1383,7 @@ fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { cached_path, cached_ident, path: path.clone(), - addressable_paths: vec![path.clone()], + path_aggregate: vec![path.clone()], ty, }); } @@ -1458,7 +1483,7 @@ impl<'ast> Visit<'ast> for FfiItems { cached_path: ast::path_to_string(running_items.current_module.clone()), cached_ident: ident.to_string().into_boxed_str(), path: path.clone(), - addressable_paths: vec![path.clone()], + path_aggregate: vec![path.clone()], items: running_items.clone(), }); } From 72f52fdbc3dd025e0f6b9b756adf08bf17c8b96c Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:54:43 +0200 Subject: [PATCH 085/100] chore: wip --- ctest/src/ffi_items.rs | 139 ++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 29 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index f4c0091632041..30fd0e136af6a 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1,10 +1,15 @@ //! Conversion of Rust code to a simplified abstract syntax tree. use std::cmp::Ordering; -use std::iter; use std::ops::{ + self, ControlFlow, Deref, + Not, +}; +use std::{ + iter, + vec, }; use proc_macro2::Span; @@ -898,31 +903,90 @@ fn is_super(ident: Ident) -> bool { ident.to_string() == "super" } -/// Integrates module subtree `src` into module subtree `dst`. -/// -/// It attempts to find `src` in `dst`. It returns an "updated" snapshot of -/// `dst` with the contents of `src` integrated. Note `src` may or may not have -/// been found within some subtree of `dst`. +/// Attempts to find some item in module tree `root` with path `target`, then +/// runs the passed closure on the item, and replaces it with the result. /// -/// If not found, `dst` is returned unmodified. `src` may also match `dst` -/// itself. That would return `src`. -fn update_tree(src: Module, dst: Module) -> Module { - if src.path == dst.path { - return src.clone().into(); +/// It is key that the generic wrapper around the item returned is the same as +/// the generic item that was passed in. +fn update_tree( + target: Path, + action: impl ops::Fn(GenericItem) -> GenericItem + Clone, + root: Module, +) -> Module { + macro_rules! ignored { + () => { + unimplemented!( + "The action is not meant to return an item of a differing kind \ + than what was provided." + ); + }; } - let new_children: Vec<_> = dst - .items - .modules - .clone() - .into_iter() - .map(|m| update_tree(src.clone(), m.clone())) - .collect(); + if target == root.path { + let GenericItem::Module(new_root) = action(GenericItem::Module(root.clone())) else { + ignored!(); + }; + return new_root.into(); + } + macro_rules! exe { + (@$found:tt $($field:tt: $ty:tt),+ $(,)?) => { + let mut $found = false; + $( + let $field = if !$found { + root + .items + .$field + .clone() + .into_iter() + .filter(|$ty { path, .. }| *path == target) + .map(|it| { + let new = action(GenericItem::$ty(it.clone())); + let GenericItem::$ty(it) = new.clone() else { + ignored!(); + }; + it.clone() + }) + .collect() + } else { + root.items.$field.clone() + }; + if $field.is_empty().not() { + $found = true; + } + )+ + }; + } + exe! { + @found + aliases: Type, + structs: Struct, + unions: Union, + constants: Const, + foreign_functions: Fn, + foreign_statics: Static, + modules: Module, + } + let modules = if !found { + root.items + .modules + .clone() + .into_iter() + .map(|m| update_tree(target.clone(), action.clone(), m.clone())) + .collect() + } else { + root.items.modules.clone() + }; Module { items: FfiItems { - modules: new_children.clone(), - ..dst.items.clone() + aliases: aliases.clone(), + structs: structs.clone(), + unions: unions.clone(), + constants: constants.clone(), + foreign_functions: foreign_functions.clone(), + foreign_statics: foreign_statics.clone(), + modules: modules.clone(), + ..root.items.clone() }, - ..dst.clone() + ..root.clone() } } @@ -1114,14 +1178,9 @@ fn search_and_merge_aggregates( reference: GenericItem, matches: Vec, ) -> Vec { - let new_nested_matches: Vec<_> = root - .modules - .clone() - .into_iter() - .flat_map(|it| { - search_and_merge_aggregates(it.items.clone(), reference.clone(), matches.clone()) - }) - .collect(); + let new_nested_matches = root.modules.clone().into_iter().flat_map(|it| { + search_and_merge_aggregates(it.items.clone(), reference.clone(), matches.clone()) + }); let new_matches = root .items() .filter_map(|it| { @@ -1181,6 +1240,28 @@ fn search_and_merge_aggregates( matches.clone() } +fn sync_aggregates(src: FfiItems, whole_crate: Module) -> Module { + src.clone() + .items() + .fold(whole_crate.clone(), |whole_crate, it| { + let updated_item_occurrences = search_and_merge_aggregates( + FfiItems { + modules: vec![whole_crate.clone()], + ..Default::default() + }, + it.clone(), + vec![], + ); + todo!( + "Update the whole-crate module tree by integrating each of the \ + updated items in updated_item_occurrences. This is blocked on \ + getting the refactor of the module tree update function \ + finished." + ); + }); + todo!(); +} + /// Appends a new module-local item to an absolute path that does _not_ start /// with `crate`. /// From 05a734de225055129ab78b5a98c20b82627287af Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:31:20 +0100 Subject: [PATCH 086/100] chore: wip --- ctest/src/ast/constant.rs | 15 +++--- ctest/src/ast/function.rs | 6 +-- ctest/src/ast/mod.rs | 87 +++++++++++++++++++++++++------- ctest/src/ast/module.rs | 6 +-- ctest/src/ast/static_variable.rs | 6 +-- ctest/src/ast/structure.rs | 6 +-- ctest/src/ast/type_alias.rs | 6 +-- ctest/src/ast/union.rs | 6 +-- ctest/src/ffi_items.rs | 2 +- ctest/src/template.rs | 2 +- 10 files changed, 98 insertions(+), 44 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index c83ffd8349d1e..df73312c96232 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -4,7 +4,10 @@ use syn::{ }; use crate::BoxStr; -use crate::ast::RenameUtils; +use crate::ast::{ + PathAggregateStore, + PathUtils, +}; /// Represents a constant variable defined in Rust. #[derive(Debug, Clone, PartialEq)] @@ -13,7 +16,7 @@ pub struct Const { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) path_aggregate: Vec, + pub(crate) path_aggregate: PathAggregateStore, pub(crate) ty: Type, } @@ -34,8 +37,8 @@ impl Const { } } -impl RenameUtils for Const { - fn aggregate_path(self) -> Vec { +impl PathUtils for Const { + fn path_aggregate(self) -> PathAggregateStore { self.path_aggregate.clone() } @@ -48,13 +51,13 @@ impl RenameUtils for Const { cached_path: BoxStr, cached_ident: BoxStr, path: Path, - addressable_paths: Vec, + path_aggregate: PathAggregateStore, ) -> Self { Self { cached_path, cached_ident, path, - path_aggregate: addressable_paths, + path_aggregate, ..self.clone() } } diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index f406fee3a8a9d..788a491909c27 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -1,6 +1,6 @@ use syn::Path; -use crate::ast::RenameUtils; +use crate::ast::PathUtils; use crate::{ Abi, BoxStr, @@ -48,8 +48,8 @@ impl Fn { } } -impl RenameUtils for Fn { - fn aggregate_path(self) -> Vec { +impl PathUtils for Fn { + fn path_aggregate(self) -> Vec { self.path_aggregate.clone() } diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 5c87ce5e9043c..ea914f1af761d 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -34,50 +34,95 @@ use crate::{ MapInput, }; +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct PathAggregateStore { + value: Vec, +} + +impl PathAggregateStore { + pub(crate) const fn new() -> Self { + Self { value: Vec::new() } + } + + pub(crate) fn push(self, p: Path) -> Self { + let PathAggregateStore { mut value } = self; + value.push(p); + Self { value } + } +} + +impl Default for PathAggregateStore { + fn default() -> Self { + Self::new() + } +} + /// Utility interface for keeping a single renaming function for all parsed /// items. /// /// They all share the same set of fields relative to their identifier and path, /// all of which need updating whenever the path is renamed. We ensure that the /// only place where this happens is in this function for convenience's sake. -pub(crate) trait RenameUtils: Clone { +pub(crate) trait PathUtils: Clone { fn path(self) -> Path; - fn aggregate_path(self) -> Vec; + fn path_aggregate(self) -> PathAggregateStore; fn mk( self, cached_path: BoxStr, cached_ident: BoxStr, path: Path, - addressable_paths: Vec, + addressable_paths: PathAggregateStore, ) -> Self; + // [TODO]: override the default impelemnteation for modules, so as to allow + // the items living within the subtree rooted at the module to also adopt the + // new path in their aggreagtes. + // [TODO]: change all call sites where the path aggregate is modified + // manually with uses of this method. + fn add_path(self, path: Path) -> Self { + let aggregate = self.clone().path_aggregate(); + let new_aggregate = aggregate.push(path); + self.clone().mk( + path_to_string(self.clone().path()), + ident_to_string(self.clone().path()), + self.clone().path(), + new_aggregate, + ) + } + /// Diffs the path aggregates of both `self` and `other`, and yields a /// unified path aggregate integrating the paths from both. - fn merge_aggregates(self, other: Self) -> Vec { + fn merge_aggregates(self, other: Self) -> PathAggregateStore { let we_need = other .clone() - .aggregate_path() + .path_aggregate() + .value .into_iter() .filter(|a| { self.clone() - .aggregate_path() + .path_aggregate() + .value .into_iter() .any(|b| *a == b) .not() }) .collect::>(); - let mut ours = self.clone().aggregate_path(); + let mut ours = self.clone().path_aggregate().value; ours.extend(we_need); - ours + PathAggregateStore { value: ours } } /// Checks whether two interface implementors contain some common path in /// their path aggregate. fn akin_aggregates(self, other: Self) -> bool { - self.clone() - .aggregate_path() - .into_iter() - .any(|p| other.clone().aggregate_path().into_iter().any(|p2| p == p2)) + self.clone().path_aggregate().value.into_iter().any(|p| { + other + .clone() + .path_aggregate() + .value + .into_iter() + .any(|p2| p == p2) + }) } /// Provided some parsed type, this method will run the given closure on @@ -97,7 +142,8 @@ pub(crate) trait RenameUtils: Clone { /// needs to be performed on all variations. fn with_variations(&self, f: impl ops::Fn(Self) -> T + Clone) -> Vec { self.clone() - .aggregate_path() + .path_aggregate() + .value .into_iter() .map(|p| f(self.clone().rename(p.clone()))) .collect() @@ -116,11 +162,7 @@ pub(crate) trait RenameUtils: Clone { .map(|PathSegment { ident, .. }| ident.to_string().into_boxed_str()) .unwrap_or_default(), new_path.clone(), - self.aggregate_path() - .clone() - .into_iter() - .chain([new_path.clone()]) - .collect(), + self.path_aggregate().push(new_path), ) } } @@ -150,6 +192,15 @@ pub(crate) fn path_to_string(path: Path) -> BoxStr { .into_boxed_str() } +/// Returns a stringified representation of the last segment in the passed +/// identifier. +pub(crate) fn ident_to_string(path: Path) -> BoxStr { + path.segments + .last() + .map(|it| it.ident.to_string().into_boxed_str()) + .unwrap_or_default() +} + /// The ABI as defined by the extern block. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Abi { diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 5433922eb72cf..bf3bfa7da1a37 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -7,7 +7,7 @@ use syn::{ PathSegment, }; -use crate::ast::RenameUtils; +use crate::ast::PathUtils; use crate::ffi_items::FfiItems; use crate::{ BoxStr, @@ -54,8 +54,8 @@ impl Module { } } -impl RenameUtils for Module { - fn aggregate_path(self) -> Vec { +impl PathUtils for Module { + fn path_aggregate(self) -> Vec { self.path_aggregate.clone() } diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index c1a3df4df7846..fe9758685b1cd 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -3,7 +3,7 @@ use syn::{ Type, }; -use crate::ast::RenameUtils; +use crate::ast::PathUtils; use crate::{ Abi, BoxStr, @@ -48,8 +48,8 @@ impl Static { } } -impl RenameUtils for Static { - fn aggregate_path(self) -> Vec { +impl PathUtils for Static { + fn path_aggregate(self) -> Vec { self.path_aggregate.clone() } diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index 853e30c74b20d..059be5e68283c 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -1,7 +1,7 @@ use syn::Path; use syn::punctuated::Punctuated; -use crate::ast::RenameUtils; +use crate::ast::PathUtils; use crate::{ BoxStr, Field, @@ -35,8 +35,8 @@ impl Struct { } } -impl RenameUtils for Struct { - fn aggregate_path(self) -> Vec { +impl PathUtils for Struct { + fn path_aggregate(self) -> Vec { self.path_aggregate.clone() } diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index da7c62d979b8c..268b34922cb15 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -1,7 +1,7 @@ use syn::Path; use crate::BoxStr; -use crate::ast::RenameUtils; +use crate::ast::PathUtils; /// Represents a type alias defined in Rust. #[derive(Debug, Clone, PartialEq)] @@ -32,8 +32,8 @@ impl Type { } } -impl RenameUtils for Type { - fn aggregate_path(self) -> Vec { +impl PathUtils for Type { + fn path_aggregate(self) -> Vec { self.path_aggregate.clone() } diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index 79b71aad7f9bd..d7c03eef85e37 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -1,6 +1,6 @@ use syn::Path; -use crate::ast::RenameUtils; +use crate::ast::PathUtils; use crate::{ BoxStr, Field, @@ -34,8 +34,8 @@ impl Union { } } -impl RenameUtils for Union { - fn aggregate_path(self) -> Vec { +impl PathUtils for Union { + fn path_aggregate(self) -> Vec { self.path_aggregate.clone() } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 30fd0e136af6a..d0f237c4ae6e9 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -38,7 +38,7 @@ use syn::{ Visibility, }; -use crate::ast::RenameUtils; +use crate::ast::PathUtils; use crate::{ Abi, BoxStr, diff --git a/ctest/src/template.rs b/ctest/src/template.rs index 7b5c11a153a32..de8c86108eb81 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -7,7 +7,7 @@ use proc_macro2::Span; use quote::ToTokens; use syn::spanned::Spanned; -use crate::ast::RenameUtils; +use crate::ast::PathUtils; use crate::cdecl::Constness; use crate::ffi_items::FfiItems; use crate::translator::{ From abc63f47a4f7a147b3545df2e5d0de95dba7a296 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:41:06 +0100 Subject: [PATCH 087/100] chore: wip --- ctest/src/ast/function.rs | 11 ++- ctest/src/ast/mod.rs | 83 +++++++++++--------- ctest/src/ast/module.rs | 119 +++++++++++++++++++++++------ ctest/src/ast/static_variable.rs | 11 ++- ctest/src/ast/structure.rs | 13 ++-- ctest/src/ast/type_alias.rs | 11 ++- ctest/src/ast/union.rs | 11 ++- ctest/src/ffi_items.rs | 125 ++++++++++++++++++++++++++++++- 8 files changed, 305 insertions(+), 79 deletions(-) diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index 788a491909c27..a0d3d630368bf 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -1,6 +1,9 @@ use syn::Path; -use crate::ast::PathUtils; +use crate::ast::{ + PathAggregateStore, + PathUtils, +}; use crate::{ Abi, BoxStr, @@ -18,7 +21,7 @@ pub struct Fn { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) path_aggregate: Vec, + pub(crate) path_aggregate: PathAggregateStore, pub(crate) link_name: Option, #[expect(unused)] pub(crate) parameters: Vec, @@ -49,7 +52,7 @@ impl Fn { } impl PathUtils for Fn { - fn path_aggregate(self) -> Vec { + fn path_aggregate(self) -> PathAggregateStore { self.path_aggregate.clone() } @@ -62,7 +65,7 @@ impl PathUtils for Fn { cached_path: BoxStr, cached_ident: BoxStr, path: Path, - addressable_paths: Vec, + addressable_paths: PathAggregateStore, ) -> Self { Self { cached_path, diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index ea914f1af761d..2b5d92df487e6 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -13,6 +13,7 @@ use std::ops::{ self, Not, }; +use std::vec::IntoIter; pub use constant::Const; pub use field::Field; @@ -22,10 +23,7 @@ pub use parameter::Parameter; use quote::ToTokens; pub use static_variable::Static; pub use structure::Struct; -use syn::{ - Path, - PathSegment, -}; +use syn::Path; pub use type_alias::Type; pub use union::Union; @@ -51,6 +49,23 @@ impl PathAggregateStore { } } +pub(crate) type AddressablePaths = IntoIter; + +impl IntoIterator for PathAggregateStore { + type IntoIter = AddressablePaths; + type Item = Path; + + fn into_iter(self) -> Self::IntoIter { + self.value.into_iter() + } +} + +impl Extend for PathAggregateStore { + fn extend>(&mut self, iter: I) { + self.value.extend(iter); + } +} + impl Default for PathAggregateStore { fn default() -> Self { Self::new() @@ -74,7 +89,7 @@ pub(crate) trait PathUtils: Clone { addressable_paths: PathAggregateStore, ) -> Self; - // [TODO]: override the default impelemnteation for modules, so as to allow + // [TODO]: override the default impelementation for modules, so as to allow // the items living within the subtree rooted at the module to also adopt the // new path in their aggreagtes. // [TODO]: change all call sites where the path aggregate is modified @@ -90,39 +105,42 @@ pub(crate) trait PathUtils: Clone { ) } + /// Diffs the path aggregates of two interface implementors of the same + /// type. + /// + /// The resulting collection contains the paths in the aggregate of `other` + /// that are missing from the aggregate of the receiver. + fn diff_aggregates(self, other: Self) -> impl Iterator { + other.clone().path_aggregate().into_iter().filter(move |p| { + self.clone() + .path_aggregate() + .into_iter() + .any(|ip| *p == ip) + .not() + }) + } + /// Diffs the path aggregates of both `self` and `other`, and yields a /// unified path aggregate integrating the paths from both. + /// + /// This uses the `diff_aggregates` trait method to compute the differences + /// between the receiver and `other`, and then yields the path aggregate + /// resulting from adding those missing paths to the the receiver's path + /// aggregate. fn merge_aggregates(self, other: Self) -> PathAggregateStore { - let we_need = other - .clone() - .path_aggregate() - .value - .into_iter() - .filter(|a| { - self.clone() - .path_aggregate() - .value - .into_iter() - .any(|b| *a == b) - .not() - }) - .collect::>(); - let mut ours = self.clone().path_aggregate().value; + let we_need = self.clone().diff_aggregates(other.clone()); + let mut ours = self.clone().path_aggregate(); ours.extend(we_need); - PathAggregateStore { value: ours } + ours } /// Checks whether two interface implementors contain some common path in /// their path aggregate. fn akin_aggregates(self, other: Self) -> bool { - self.clone().path_aggregate().value.into_iter().any(|p| { - other - .clone() - .path_aggregate() - .value - .into_iter() - .any(|p2| p == p2) - }) + self.clone() + .path_aggregate() + .into_iter() + .any(|p| other.clone().path_aggregate().into_iter().any(|p2| p == p2)) } /// Provided some parsed type, this method will run the given closure on @@ -143,7 +161,6 @@ pub(crate) trait PathUtils: Clone { fn with_variations(&self, f: impl ops::Fn(Self) -> T + Clone) -> Vec { self.clone() .path_aggregate() - .value .into_iter() .map(|p| f(self.clone().rename(p.clone()))) .collect() @@ -156,11 +173,7 @@ pub(crate) trait PathUtils: Clone { fn rename(self, new_path: Path) -> Self { self.clone().mk( path_to_string(new_path.clone()), - new_path - .segments - .last() - .map(|PathSegment { ident, .. }| ident.to_string().into_boxed_str()) - .unwrap_or_default(), + ident_to_string(new_path.clone()), new_path.clone(), self.path_aggregate().push(new_path), ) diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index bf3bfa7da1a37..0a88d47a69044 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,16 +1,25 @@ use std::cmp::Ordering; use std::ops; +use syn::Path; use syn::punctuated::Punctuated; -use syn::{ - Path, - PathSegment, -}; -use crate::ast::PathUtils; -use crate::ffi_items::FfiItems; +use crate::ast::{ + PathAggregateStore, + PathUtils, +}; +use crate::ffi_items::{ + FfiItems, + GenericItem, +}; use crate::{ BoxStr, + Const, + Fn, + Static, + Struct, + Type, + Union, ast, }; @@ -22,7 +31,7 @@ pub struct Module { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) path_aggregate: Vec, + pub(crate) path_aggregate: PathAggregateStore, pub(crate) items: FfiItems, } @@ -55,7 +64,7 @@ impl Module { } impl PathUtils for Module { - fn path_aggregate(self) -> Vec { + fn path_aggregate(self) -> PathAggregateStore { self.path_aggregate.clone() } @@ -68,7 +77,7 @@ impl PathUtils for Module { cached_path: BoxStr, cached_ident: BoxStr, path: Path, - addressable_paths: Vec, + addressable_paths: PathAggregateStore, ) -> Self { Self { cached_path, @@ -79,6 +88,83 @@ impl PathUtils for Module { } } + fn add_path(self, path: Path) -> Self { + fn handle_item(it: GenericItem, original: Path, new: Path) -> GenericItem { + macro_rules! map { + ($($ty:tt),+ $(,)?) => { + match it.clone() { + $( + GenericItem::$ty(it) => { + let kept_segments = it + .clone() + .path() + .segments + .into_iter() + .skip(original.segments.len()); + let mut new_path = new.clone(); + new_path.segments.extend(kept_segments); + GenericItem::$ty($ty { + path_aggregate: it.clone().path_aggregate(), + ..it.clone() + }) + } + )+ + GenericItem::Module(m) => { + let kept_segments = m + .clone() + .path() + .segments + .into_iter() + .skip(original.segments.len()); + let mut new_path = new.clone(); + new_path.segments.extend(kept_segments); + let new_items: FfiItems = m + .items + .items() + .map(|it| handle_item(it.clone(), original.clone(), new.clone())) + .collect(); + GenericItem::Module(Module { + path_aggregate: m.clone().path_aggregate().push(new_path.clone()), + items: new_items.clone(), + ..m.clone() + }) + } + } + }; + } + map! { + Type, + Struct, + Union, + Const, + Fn, + Static, + } + } + let new_aggregate = self.path_aggregate.clone().push(path.clone()); + let diff = self.clone().diff_aggregates(self.clone().mk( + ast::path_to_string(self.path.clone()), + ast::ident_to_string(self.path.clone()), + self.path.clone(), + new_aggregate.clone(), + )); + let new_module = diff.into_iter().fold(self.clone(), |m, p| { + let new_items: FfiItems = self + .items + .items() + .map(|it| handle_item(it.clone(), m.path.clone(), p.clone())) + .collect(); + Module { + items: new_items.clone(), + ..m.clone() + } + }); + Module { + path_aggregate: new_aggregate.clone(), + ..new_module.clone() + } + } + // [NOTE]: this is implemented in terms of rename_with but it doesn't just // yield the passed path, because that would mean all items would end up // with the same path. Instead, it calls into rename_with and skips as many @@ -139,18 +225,9 @@ impl PathUtils for Module { let new_path = f(self.path.clone()); Module { cached_path: ast::path_to_string(new_path.clone()), - cached_ident: new_path - .segments - .last() - .map(|PathSegment { ident, .. }| ident.to_string().into_boxed_str()) - .unwrap_or_default(), + cached_ident: ast::ident_to_string(new_path.clone()), path: new_path.clone(), - path_aggregate: self - .path_aggregate - .clone() - .into_iter() - .chain([new_path.clone()]) - .collect(), + path_aggregate: self.path_aggregate.clone().push(new_path.clone()), items: new_items.clone(), ..self.clone() } @@ -168,7 +245,7 @@ impl Default for Module { cached_path: String::new().into_boxed_str(), cached_ident: String::new().into_boxed_str(), path: dpath.clone(), - path_aggregate: vec![dpath.clone()], + path_aggregate: PathAggregateStore::new().push(dpath.clone()), items: FfiItems::default(), } } diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index fe9758685b1cd..664f283c14b14 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -3,7 +3,10 @@ use syn::{ Type, }; -use crate::ast::PathUtils; +use crate::ast::{ + PathAggregateStore, + PathUtils, +}; use crate::{ Abi, BoxStr, @@ -21,7 +24,7 @@ pub struct Static { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) path_aggregate: Vec, + pub(crate) path_aggregate: PathAggregateStore, pub(crate) link_name: Option, pub(crate) ty: Type, } @@ -49,7 +52,7 @@ impl Static { } impl PathUtils for Static { - fn path_aggregate(self) -> Vec { + fn path_aggregate(self) -> PathAggregateStore { self.path_aggregate.clone() } @@ -62,7 +65,7 @@ impl PathUtils for Static { cached_path: BoxStr, cached_ident: BoxStr, path: Path, - addressable_paths: Vec, + addressable_paths: PathAggregateStore, ) -> Self { Self { cached_path, diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index 059be5e68283c..8e387c8b50d6a 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -1,7 +1,10 @@ use syn::Path; use syn::punctuated::Punctuated; -use crate::ast::PathUtils; +use crate::ast::{ + PathAggregateStore, + PathUtils, +}; use crate::{ BoxStr, Field, @@ -14,7 +17,7 @@ pub struct Struct { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) path_aggregate: Vec, + pub(crate) path_aggregate: PathAggregateStore, pub(crate) fields: Vec, } @@ -36,7 +39,7 @@ impl Struct { } impl PathUtils for Struct { - fn path_aggregate(self) -> Vec { + fn path_aggregate(self) -> PathAggregateStore { self.path_aggregate.clone() } @@ -49,7 +52,7 @@ impl PathUtils for Struct { cached_path: BoxStr, cached_ident: BoxStr, path: Path, - addressable_paths: Vec, + addressable_paths: PathAggregateStore, ) -> Self { Self { cached_path, @@ -72,7 +75,7 @@ impl Default for Struct { cached_path: String::new().into_boxed_str(), cached_ident: String::new().into_boxed_str(), path: dpath.clone(), - path_aggregate: vec![dpath.clone()], + path_aggregate: PathAggregateStore::new().push(dpath.clone()), fields: vec![], } } diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index 268b34922cb15..ce76b6ff7f414 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -1,7 +1,10 @@ use syn::Path; use crate::BoxStr; -use crate::ast::PathUtils; +use crate::ast::{ + PathAggregateStore, + PathUtils, +}; /// Represents a type alias defined in Rust. #[derive(Debug, Clone, PartialEq)] @@ -10,7 +13,7 @@ pub struct Type { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) path_aggregate: Vec, + pub(crate) path_aggregate: PathAggregateStore, pub(crate) ty: syn::Type, } @@ -33,7 +36,7 @@ impl Type { } impl PathUtils for Type { - fn path_aggregate(self) -> Vec { + fn path_aggregate(self) -> PathAggregateStore { self.path_aggregate.clone() } @@ -46,7 +49,7 @@ impl PathUtils for Type { cached_path: BoxStr, cached_ident: BoxStr, path: Path, - addressable_paths: Vec, + addressable_paths: PathAggregateStore, ) -> Self { Self { cached_path, diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index d7c03eef85e37..e6e2936e820c5 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -1,6 +1,9 @@ use syn::Path; -use crate::ast::PathUtils; +use crate::ast::{ + PathAggregateStore, + PathUtils, +}; use crate::{ BoxStr, Field, @@ -13,7 +16,7 @@ pub struct Union { pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, - pub(crate) path_aggregate: Vec, + pub(crate) path_aggregate: PathAggregateStore, pub(crate) fields: Vec, } @@ -35,7 +38,7 @@ impl Union { } impl PathUtils for Union { - fn path_aggregate(self) -> Vec { + fn path_aggregate(self) -> PathAggregateStore { self.path_aggregate.clone() } @@ -48,7 +51,7 @@ impl PathUtils for Union { cached_path: BoxStr, cached_ident: BoxStr, path: Path, - addressable_paths: Vec, + addressable_paths: PathAggregateStore, ) -> Self { Self { cached_path, diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index d0f237c4ae6e9..fab1366dbdba1 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -156,7 +156,7 @@ impl FfiItems { /// Yields an iterator over all items in the calling `FfiItems`. /// /// The items in the item container do not include `use` declarations. - fn items(&self) -> ItemIterator { + pub(crate) fn items(&self) -> ItemIterator { let base = self .aliases .clone() @@ -326,9 +326,130 @@ impl Default for FfiItems { } } +impl Extend for FfiItems { + fn extend>(&mut self, iter: I) { + let unpartitioned_items: Vec<_> = iter.into_iter().collect(); + let mut partitioned_items = unpartitioned_items.clone(); + partitioned_items.sort_by(|it1, it2| match (it1.clone(), it2.clone()) { + (GenericItem::Type(_), GenericItem::Type(_)) + | (GenericItem::Struct(_), GenericItem::Struct(_)) + | (GenericItem::Union(_), GenericItem::Union(_)) + | (GenericItem::Const(_), GenericItem::Const(_)) + | (GenericItem::Fn(_), GenericItem::Fn(_)) + | (GenericItem::Static(_), GenericItem::Static(_)) + | (GenericItem::Module(_), GenericItem::Module(_)) => Ordering::Equal, + (GenericItem::Type(_), GenericItem::Struct(_)) + | (GenericItem::Type(_), GenericItem::Union(_)) + | (GenericItem::Type(_), GenericItem::Const(_)) + | (GenericItem::Type(_), GenericItem::Fn(_)) + | (GenericItem::Type(_), GenericItem::Static(_)) + | (GenericItem::Type(_), GenericItem::Module(_)) + | (GenericItem::Struct(_), GenericItem::Union(_)) + | (GenericItem::Struct(_), GenericItem::Const(_)) + | (GenericItem::Struct(_), GenericItem::Fn(_)) + | (GenericItem::Struct(_), GenericItem::Static(_)) + | (GenericItem::Struct(_), GenericItem::Module(_)) + | (GenericItem::Union(_), GenericItem::Const(_)) + | (GenericItem::Union(_), GenericItem::Fn(_)) + | (GenericItem::Union(_), GenericItem::Static(_)) + | (GenericItem::Union(_), GenericItem::Module(_)) + | (GenericItem::Const(_), GenericItem::Fn(_)) + | (GenericItem::Const(_), GenericItem::Static(_)) + | (GenericItem::Const(_), GenericItem::Module(_)) + | (GenericItem::Static(_), GenericItem::Module(_)) + | (GenericItem::Fn(_), GenericItem::Static(_)) + | (GenericItem::Fn(_), GenericItem::Module(_)) => Ordering::Less, + (GenericItem::Struct(_), GenericItem::Type(_)) + | (GenericItem::Union(_), GenericItem::Type(_)) + | (GenericItem::Union(_), GenericItem::Struct(_)) + | (GenericItem::Const(_), GenericItem::Type(_)) + | (GenericItem::Const(_), GenericItem::Struct(_)) + | (GenericItem::Const(_), GenericItem::Union(_)) + | (GenericItem::Fn(_), GenericItem::Type(_)) + | (GenericItem::Fn(_), GenericItem::Struct(_)) + | (GenericItem::Fn(_), GenericItem::Union(_)) + | (GenericItem::Fn(_), GenericItem::Const(_)) + | (GenericItem::Static(_), GenericItem::Type(_)) + | (GenericItem::Static(_), GenericItem::Struct(_)) + | (GenericItem::Static(_), GenericItem::Union(_)) + | (GenericItem::Static(_), GenericItem::Const(_)) + | (GenericItem::Static(_), GenericItem::Fn(_)) + | (GenericItem::Module(_), GenericItem::Type(_)) + | (GenericItem::Module(_), GenericItem::Struct(_)) + | (GenericItem::Module(_), GenericItem::Union(_)) + | (GenericItem::Module(_), GenericItem::Const(_)) + | (GenericItem::Module(_), GenericItem::Fn(_)) + | (GenericItem::Module(_), GenericItem::Static(_)) => Ordering::Greater, + }); + macro_rules! get_partitions { + ($([$it:tt: $ty:tt, $start:expr, $end:expr]),+ $(,)?) => {$( + let $it = partitioned_items + .get($start..$end) + .map(ToOwned::to_owned) + .map(|it| { + it + .into_iter() + .map(|it| match it.clone() { + GenericItem::$ty(it) => it, + _ => unreachable!( + concat!( + "Items got sorted out while \ + partitioning. This bucket corresponds \ + with that of ", + stringify!($it), + ), + ), + }) + .collect::>() + }) + .expect( + concat!( + "Items got sorted out while partitioning. This \ + bucket corresponds with that of ", + stringify!($it), + ), + ); + )+}; + } + let aliases_idx = + partitioned_items.partition_point(|it| matches!(it, GenericItem::Type(_))); + let structs_idx = + partitioned_items.partition_point(|it| matches!(it, GenericItem::Struct(_))); + let unions_idx = + partitioned_items.partition_point(|it| matches!(it, GenericItem::Union(_))); + let constants_idx = + partitioned_items.partition_point(|it| matches!(it, GenericItem::Const(_))); + let foreign_functions_idx = + partitioned_items.partition_point(|it| matches!(it, GenericItem::Fn(_))); + let foreign_statics_idx = + partitioned_items.partition_point(|it| matches!(it, GenericItem::Static(_))); + let modules_idx = + partitioned_items.partition_point(|it| matches!(it, GenericItem::Module(_))); + get_partitions! { + [aliases: Type, aliases_idx, structs_idx], + [structs: Struct, structs_idx, unions_idx], + [unions: Union, unions_idx, constants_idx], + [constants: Const, constants_idx, foreign_functions_idx], + [foreign_functions: Fn, foreign_functions_idx, foreign_statics_idx], + [foreign_statics: Static, foreign_statics_idx, modules_idx], + [modules: Module, modules_idx, partitioned_items.len()], + } + *self = FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + ..self.clone() + } + } +} + /// Sum over any item held within an `FfiItems` term. #[derive(Clone)] -enum GenericItem { +pub(crate) enum GenericItem { Type(Type), Struct(Struct), Union(Union), From 407796837d7d559f55e9c5ffbe7b62b773147f07 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:48:04 +0100 Subject: [PATCH 088/100] chore: wip --- ctest/src/ast/mod.rs | 11 ++-- ctest/src/ffi_items.rs | 121 +++++++++++++++++++++-------------------- 2 files changed, 67 insertions(+), 65 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 2b5d92df487e6..3926fb78cf12c 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -89,11 +89,12 @@ pub(crate) trait PathUtils: Clone { addressable_paths: PathAggregateStore, ) -> Self; - // [TODO]: override the default impelementation for modules, so as to allow - // the items living within the subtree rooted at the module to also adopt the - // new path in their aggreagtes. - // [TODO]: change all call sites where the path aggregate is modified - // manually with uses of this method. + /// Provided a path, it ensures the path aggregate of a given item + /// integrates the new path. + /// + /// Unlike methods `rename` and `rename_with`, this does not change the path + /// of the item itself, but rather produces an updated receiver with only + /// the path aggregate store expanded. fn add_path(self, path: Path) -> Self { let aggregate = self.clone().path_aggregate(); let new_aggregate = aggregate.push(path); diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index fab1366dbdba1..35cb01a8bc69c 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -38,7 +38,10 @@ use syn::{ Visibility, }; -use crate::ast::PathUtils; +use crate::ast::{ + PathAggregateStore, + PathUtils, +}; use crate::{ Abi, BoxStr, @@ -326,10 +329,19 @@ impl Default for FfiItems { } } -impl Extend for FfiItems { - fn extend>(&mut self, iter: I) { +impl FromIterator for FfiItems { + fn from_iter>(iter: I) -> FfiItems { let unpartitioned_items: Vec<_> = iter.into_iter().collect(); let mut partitioned_items = unpartitioned_items.clone(); + // [NOTE]: this is expected to sort all items by their kind, such that + // we end up with the following ordering: + // 1. Type aliases. + // 2. Stucts. + // 3. Unions. + // 4. Constants. + // 5. Foreign functions. + // 6. Foreign statics. + // 7. Modules. partitioned_items.sort_by(|it1, it2| match (it1.clone(), it2.clone()) { (GenericItem::Type(_), GenericItem::Type(_)) | (GenericItem::Struct(_), GenericItem::Struct(_)) @@ -382,59 +394,48 @@ impl Extend for FfiItems { | (GenericItem::Module(_), GenericItem::Static(_)) => Ordering::Greater, }); macro_rules! get_partitions { - ($([$it:tt: $ty:tt, $start:expr, $end:expr]),+ $(,)?) => {$( - let $it = partitioned_items - .get($start..$end) - .map(ToOwned::to_owned) - .map(|it| { - it - .into_iter() - .map(|it| match it.clone() { - GenericItem::$ty(it) => it, - _ => unreachable!( - concat!( - "Items got sorted out while \ - partitioning. This bucket corresponds \ - with that of ", - stringify!($it), - ), - ), - }) - .collect::>() - }) - .expect( - concat!( - "Items got sorted out while partitioning. This \ - bucket corresponds with that of ", - stringify!($it), - ), - ); + ($([$field:ident, $ty:tt]),+ $(,)?) => {$( + let start = + partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_)).not()); + let end = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_))); + let $field = if start >= end { + Vec::new() + } else { + partitioned_items + .get(start..end) + .map(|it| { + it.clone() + .into_iter() + .map(|it| match it { + GenericItem::$ty(it) => it.clone(), + _ => unreachable!(concat!( + "Items have been sorted by their kind \ + before. This is expected to \ + correspond with the bucket ", + stringify!($field), + ".", + )), + }) + .collect() + }) + .expect( + "The slice must be valid because the range is \ + logical, and the components of the range are \ + indices into the collection.", + ) + }; )+}; } - let aliases_idx = - partitioned_items.partition_point(|it| matches!(it, GenericItem::Type(_))); - let structs_idx = - partitioned_items.partition_point(|it| matches!(it, GenericItem::Struct(_))); - let unions_idx = - partitioned_items.partition_point(|it| matches!(it, GenericItem::Union(_))); - let constants_idx = - partitioned_items.partition_point(|it| matches!(it, GenericItem::Const(_))); - let foreign_functions_idx = - partitioned_items.partition_point(|it| matches!(it, GenericItem::Fn(_))); - let foreign_statics_idx = - partitioned_items.partition_point(|it| matches!(it, GenericItem::Static(_))); - let modules_idx = - partitioned_items.partition_point(|it| matches!(it, GenericItem::Module(_))); get_partitions! { - [aliases: Type, aliases_idx, structs_idx], - [structs: Struct, structs_idx, unions_idx], - [unions: Union, unions_idx, constants_idx], - [constants: Const, constants_idx, foreign_functions_idx], - [foreign_functions: Fn, foreign_functions_idx, foreign_statics_idx], - [foreign_statics: Static, foreign_statics_idx, modules_idx], - [modules: Module, modules_idx, partitioned_items.len()], + [aliases, Type], + [structs, Struct], + [unions, Union], + [constants, Const], + [foreign_functions, Fn], + [foreign_statics, Static], + [modules, Module], } - *self = FfiItems { + FfiItems { aliases, structs, unions, @@ -442,7 +443,7 @@ impl Extend for FfiItems { foreign_functions, foreign_statics, modules, - ..self.clone() + ..Default::default() } } } @@ -1490,7 +1491,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi cached_path, cached_ident, path: path.clone(), - path_aggregate: vec![path.clone()], + path_aggregate: PathAggregateStore::new().push(path.clone()), link_name, parameters, return_type, @@ -1512,7 +1513,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a cached_path, cached_ident, path: path.clone(), - path_aggregate: vec![path.clone()], + path_aggregate: PathAggregateStore::new().push(path.clone()), link_name, ty, }); @@ -1530,7 +1531,7 @@ fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { cached_path, cached_ident, path: path.clone(), - path_aggregate: vec![path.clone()], + path_aggregate: PathAggregateStore::new().push(path.clone()), ty, }); } @@ -1551,7 +1552,7 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { cached_path, cached_ident, path: path.clone(), - path_aggregate: vec![path.clone()], + path_aggregate: PathAggregateStore::new().push(path.clone()), fields, }); } @@ -1568,7 +1569,7 @@ fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { cached_path, cached_ident, path: path.clone(), - path_aggregate: vec![path.clone()], + path_aggregate: PathAggregateStore::new().push(path.clone()), fields, }); } @@ -1585,7 +1586,7 @@ fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { cached_path, cached_ident, path: path.clone(), - path_aggregate: vec![path.clone()], + path_aggregate: PathAggregateStore::new().push(path.clone()), ty, }); } @@ -1685,7 +1686,7 @@ impl<'ast> Visit<'ast> for FfiItems { cached_path: ast::path_to_string(running_items.current_module.clone()), cached_ident: ident.to_string().into_boxed_str(), path: path.clone(), - path_aggregate: vec![path.clone()], + path_aggregate: PathAggregateStore::new().push(path.clone()), items: running_items.clone(), }); } From 9df2270c50727fe7a6d4a4a100d67f56e9761d4a Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:19:09 +0100 Subject: [PATCH 089/100] chore: wip --- ctest/src/ast/mod.rs | 80 +++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 3926fb78cf12c..591f627bb7511 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -47,6 +47,48 @@ impl PathAggregateStore { value.push(p); Self { value } } + + // [TODO]: change previous uses of the below methods through the generic + // renaming trait to use the inherent methods. + + /// Checks if any two path aggregates are known to correspond with those of + /// the same item. + /// + /// In other words, it checks if the item owning the receiver path aggregate + /// and the item owning the `other` path aggregate are different occurrences + /// of the same item across the crate's module tree. + pub(crate) fn akin(&self, other: &Self) -> bool { + let ours = &self.value; + let theirs = &other.value; + ours.iter().any(|p1| theirs.iter().any(|p2| p1 == p2)) + } + + /// Diffs any two given path aggregates and returns the set of paths that + /// were present in `other` but missing in the receiver. + /// + /// Note that if the receiver contains more paths than `other`, the returned + /// collection will be empty. In other words, the operation is not + /// reflexive (`self.diff(other) != other.diff(self)`.) + pub(crate) fn diff(&self, other: &Self) -> Vec { + let ours = &self.value; + let theirs = &other.value; + theirs + .clone() + .into_iter() + .filter(|p1| ours.iter().any(|p2| p1 == p2)) + .collect() + } + + /// Yields a new path aggregate that integrates the paths of the receiver + /// with those of `other`. + /// + /// Unlike diffing, the resulting path aggregate store unifies the paths + /// from both parties; The receiver and `other`. + pub(crate) fn merge(&self, other: &Self) -> Self { + let diff = self.diff(other); + diff.into_iter() + .fold(self.clone(), PathAggregateStore::push) + } } pub(crate) type AddressablePaths = IntoIter; @@ -106,44 +148,6 @@ pub(crate) trait PathUtils: Clone { ) } - /// Diffs the path aggregates of two interface implementors of the same - /// type. - /// - /// The resulting collection contains the paths in the aggregate of `other` - /// that are missing from the aggregate of the receiver. - fn diff_aggregates(self, other: Self) -> impl Iterator { - other.clone().path_aggregate().into_iter().filter(move |p| { - self.clone() - .path_aggregate() - .into_iter() - .any(|ip| *p == ip) - .not() - }) - } - - /// Diffs the path aggregates of both `self` and `other`, and yields a - /// unified path aggregate integrating the paths from both. - /// - /// This uses the `diff_aggregates` trait method to compute the differences - /// between the receiver and `other`, and then yields the path aggregate - /// resulting from adding those missing paths to the the receiver's path - /// aggregate. - fn merge_aggregates(self, other: Self) -> PathAggregateStore { - let we_need = self.clone().diff_aggregates(other.clone()); - let mut ours = self.clone().path_aggregate(); - ours.extend(we_need); - ours - } - - /// Checks whether two interface implementors contain some common path in - /// their path aggregate. - fn akin_aggregates(self, other: Self) -> bool { - self.clone() - .path_aggregate() - .into_iter() - .any(|p| other.clone().path_aggregate().into_iter().any(|p2| p == p2)) - } - /// Provided some parsed type, this method will run the given closure on /// each variation of a term of that type (`self`.) /// From 75eb6ffc5777a3c8af70511d54eeed2abfb8bb38 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:50:43 +0100 Subject: [PATCH 090/100] chore: wip --- ctest/src/ast/function.rs | 3 - ctest/src/ast/mod.rs | 27 +-- ctest/src/ast/module.rs | 22 +- ctest/src/ast/parameter.rs | 1 - ctest/src/ast/static_variable.rs | 1 - ctest/src/ffi_items.rs | 351 +++++++++++++++---------------- 6 files changed, 199 insertions(+), 206 deletions(-) diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index a0d3d630368bf..7469168ec16af 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -16,16 +16,13 @@ use crate::{ #[derive(Debug, Clone, PartialEq)] pub struct Fn { pub(crate) public: bool, - #[expect(unused)] pub(crate) abi: Abi, pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, pub(crate) path: Path, pub(crate) path_aggregate: PathAggregateStore, pub(crate) link_name: Option, - #[expect(unused)] pub(crate) parameters: Vec, - #[expect(unused)] pub(crate) return_type: Option, } diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 591f627bb7511..7e02e39febd5c 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -8,12 +8,11 @@ mod structure; mod type_alias; mod union; -use std::fmt; -use std::ops::{ - self, - Not, -}; use std::vec::IntoIter; +use std::{ + fmt, + ops, +}; pub use constant::Const; pub use field::Field; @@ -32,6 +31,12 @@ use crate::{ MapInput, }; +/// Container for all paths by which a given item may be addressed throughout +/// the crate. +/// +/// This is used to store all paths by which an item may be referred to, after +/// it has potentially been imported to modules other than the one it was +/// declared in. #[derive(Clone, Debug, PartialEq)] pub(crate) struct PathAggregateStore { value: Vec, @@ -42,15 +47,13 @@ impl PathAggregateStore { Self { value: Vec::new() } } - pub(crate) fn push(self, p: Path) -> Self { - let PathAggregateStore { mut value } = self; + /// Appends a new path to the path aggregate store. + pub(crate) fn push(&self, p: Path) -> Self { + let PathAggregateStore { mut value } = self.clone(); value.push(p); Self { value } } - // [TODO]: change previous uses of the below methods through the generic - // renaming trait to use the inherent methods. - /// Checks if any two path aggregates are known to correspond with those of /// the same item. /// @@ -84,10 +87,10 @@ impl PathAggregateStore { /// /// Unlike diffing, the resulting path aggregate store unifies the paths /// from both parties; The receiver and `other`. + #[expect(unused)] pub(crate) fn merge(&self, other: &Self) -> Self { let diff = self.diff(other); - diff.into_iter() - .fold(self.clone(), PathAggregateStore::push) + diff.into_iter().fold(self.clone(), |agg, p| agg.push(p)) } } diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 0a88d47a69044..b425a10a9b86d 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -141,13 +141,8 @@ impl PathUtils for Module { Static, } } - let new_aggregate = self.path_aggregate.clone().push(path.clone()); - let diff = self.clone().diff_aggregates(self.clone().mk( - ast::path_to_string(self.path.clone()), - ast::ident_to_string(self.path.clone()), - self.path.clone(), - new_aggregate.clone(), - )); + let new_aggregate = self.path_aggregate.push(path.clone()); + let diff = self.path_aggregate.diff(&new_aggregate); let new_module = diff.into_iter().fold(self.clone(), |m, p| { let new_items: FfiItems = self .items @@ -178,11 +173,14 @@ impl PathUtils for Module { // appended to the new path of that (potentially grand-) ancestor. fn rename(self, new_path: Path) -> Self { let old_path_len = self.path.segments.len(); - self.clone().rename_with(|p| { - let segments_post_ancestor = p.segments.clone().into_iter().skip(old_path_len); - let mut new_path = new_path.clone(); - new_path.segments.extend(segments_post_ancestor); - new_path.clone() + self.rename_with(|p| { + p.segments + .into_iter() + .skip(old_path_len) + .fold(new_path.clone(), |new_path, s| Path { + segments: new_path.segments.clone().into_iter().chain([s]).collect(), + ..new_path + }) }) } diff --git a/ctest/src/ast/parameter.rs b/ctest/src/ast/parameter.rs index 9d8b8146e14da..a7553992e94af 100644 --- a/ctest/src/ast/parameter.rs +++ b/ctest/src/ast/parameter.rs @@ -4,7 +4,6 @@ use crate::BoxStr; #[derive(Debug, Clone, PartialEq)] pub struct Parameter { pub(crate) ident: BoxStr, - #[expect(unused)] pub(crate) ty: syn::Type, } diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index 664f283c14b14..a19ab2dabc6c1 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -19,7 +19,6 @@ use crate::{ #[derive(Debug, Clone, PartialEq)] pub struct Static { pub(crate) public: bool, - #[expect(unused)] pub(crate) abi: Abi, pub(crate) cached_path: BoxStr, pub(crate) cached_ident: BoxStr, diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 35cb01a8bc69c..4b9ab0e461863 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -394,7 +394,8 @@ impl FromIterator for FfiItems { | (GenericItem::Module(_), GenericItem::Static(_)) => Ordering::Greater, }); macro_rules! get_partitions { - ($([$field:ident, $ty:tt]),+ $(,)?) => {$( + ($([$field:ident, $ty:tt]),+ $(,)?) => { + $( let start = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_)).not()); let end = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_))); @@ -404,8 +405,7 @@ impl FromIterator for FfiItems { partitioned_items .get(start..end) .map(|it| { - it.clone() - .into_iter() + it.into_iter() .map(|it| match it { GenericItem::$ty(it) => it.clone(), _ => unreachable!(concat!( @@ -424,7 +424,8 @@ impl FromIterator for FfiItems { indices into the collection.", ) }; - )+}; + )+ + }; } get_partitions! { [aliases, Type], @@ -644,7 +645,8 @@ fn resolve_pass(src: PassPair) -> PassPair { ..new_root_with_duplicates.clone() }; let new_whole_crate = update_tree( - new_root.clone(), + new_root.path.clone(), + |_| GenericItem::Module(new_root.clone()), new_whole_crate_with_resolved_children.clone(), ); println!( @@ -1050,35 +1052,29 @@ fn update_tree( return new_root.into(); } macro_rules! exe { - (@$found:tt $($field:tt: $ty:tt),+ $(,)?) => { - let mut $found = false; + ($($field:tt: $ty:tt),+ $(,)?) => { $( - let $field = if !$found { - root - .items - .$field - .clone() - .into_iter() - .filter(|$ty { path, .. }| *path == target) - .map(|it| { - let new = action(GenericItem::$ty(it.clone())); - let GenericItem::$ty(it) = new.clone() else { - ignored!(); - }; - it.clone() - }) - .collect() - } else { - root.items.$field.clone() - }; - if $field.is_empty().not() { - $found = true; - } + let $field: Vec<_> = root + .items + .$field + .clone() + .into_iter() + .map(|it| { + if it.path == target { + let new = action(GenericItem::$ty(it.clone())); + let GenericItem::$ty(it) = new.clone() else { + ignored!(); + }; + it + } else { + it + } + }) + .collect(); )+ }; } exe! { - @found aliases: Type, structs: Struct, unions: Union, @@ -1087,16 +1083,14 @@ fn update_tree( foreign_statics: Static, modules: Module, } - let modules = if !found { - root.items - .modules - .clone() - .into_iter() - .map(|m| update_tree(target.clone(), action.clone(), m.clone())) - .collect() - } else { - root.items.modules.clone() - }; + // [NOTE]: this additionally processes modules if no matching item has been + // found in the current module, by recursing with the same function on each + // of the child modules. + let modules: Vec<_> = modules + .clone() + .into_iter() + .map(|m| update_tree(target.clone(), action.clone(), m.clone())) + .collect(); Module { items: FfiItems { aliases: aliases.clone(), @@ -1123,7 +1117,7 @@ fn update_tree( /// those through [`FfiItems::dedup`]. fn merge_module(state: PassPair, src: Vec) -> PassPair { let new_path = state.subtree.path.clone(); - src.clone() + let processed_items: Vec<_> = src .into_iter() .filter_map(|r| { if let Resolution::Resolved(items) = r.clone() { @@ -1184,16 +1178,16 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { foreign_statics, modules, ) else { - return (FfiItems::default(), Module::default()); + return FfiItems::default(); }; + // [NOTE]: the dummy module is only in charge of wrapping all + // items we want to rename with a path that makes up some + // initial portion of the path segments of all descendant items' + // path. let dummy_module = Module { path: Path { leading_colon: None, - segments: { - let mut base = Punctuated::new(); - base.extend(source_module_path); - base.clone() - }, + segments: source_module_path, }, items: FfiItems { aliases: aliases.clone(), @@ -1207,14 +1201,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { }, ..Default::default() }; - let new_whole_crate = todo!( - "Use the whole crate module tree in the state to look up \ - all of the above items' path aggregates, and then, for \ - each item found with some path in their aggregate that is \ - equal to any path in the aggregate of the items about to \ - be merged, synchronize their path aggregates." - ); - let new_module = dummy_module.clone().rename(new_path.clone()); + let new_dummy_module = dummy_module.rename(new_path.clone()); fn remove_uses(m: Module) -> Module { let new_modules: Vec<_> = m .items @@ -1226,96 +1213,126 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { Module { items: FfiItems { uses: vec![], - modules: new_modules.clone(), + modules: new_modules, ..m.items.clone() }, - ..m.clone() + ..m } } // [NOTE]: this removes all use declarations in all descendant // modules of the subtrees to merge because otherwise merged // modules can increase the number of use declarations in the // crate's module tree, which is one cause of non-termination. - let new_modules_without_uses: Vec<_> = new_module + let new_modules_without_uses: Vec<_> = new_dummy_module .items .modules .clone() .into_iter() .map(remove_uses) .collect(); - ( - FfiItems { - modules: new_modules_without_uses.clone(), - ..new_module.items.clone() - }, - new_whole_crate.clone(), - ) - }, - ) - .fold( - state.clone(), - |state, - ( FfiItems { - aliases, - structs, - unions, - constants, - foreign_functions, - foreign_statics, - modules, - .. - }, - new_whole_crate, - )| { - let mut dst = state.subtree.clone(); - dst.items.aliases.extend_from_slice(&aliases); - dst.items.structs.extend_from_slice(&structs); - dst.items.unions.extend_from_slice(&unions); - dst.items.constants.extend_from_slice(&constants); - dst.items - .foreign_functions - .extend_from_slice(&foreign_functions); - dst.items - .foreign_statics - .extend_from_slice(&foreign_statics); - dst.items.modules.extend_from_slice(&modules); - PassPair { - subtree: dst.clone(), - whole_crate: todo!( - "Use the whole-crate state that is returned in the map \ - once that is implemented." - ), + modules: new_modules_without_uses, + ..new_dummy_module.items } }, ) + .collect(); + + processed_items.into_iter().fold( + state.clone(), + |PassPair { + subtree, + whole_crate, + }, + it| { + // [TODO]: this is still wrong but it also seems to be headed in the + // right direction. + let new_whole_crate = sync_driver(it.clone(), whole_crate); + let Some(new_subtree) = get_module(subtree.path, new_whole_crate.clone()) else { + unreachable!( + "The subtree is bound to be found in the whole-crate \ + module tree because it was sourced from it, and syncing \ + the path aggregates of items should not suddently remove \ + modules." + ); + }; + PassPair { + subtree: Module { + items: new_subtree.items.items().chain(it.items()).collect(), + ..new_subtree + }, + whole_crate: new_whole_crate, + } + }, + ) } -/// Provided an item container, it searches for all items of the same kind, and -/// with a path aggregate that contains some matching path as the aggregate of -/// `reference`, and ensures the returned items contain an updated path -/// aggregate that integrates both `reference`'s and theirs. -fn search_and_merge_aggregates( - root: FfiItems, - reference: GenericItem, - matches: Vec, -) -> Vec { - let new_nested_matches = root.modules.clone().into_iter().flat_map(|it| { - search_and_merge_aggregates(it.items.clone(), reference.clone(), matches.clone()) - }); - let new_matches = root +/// Provided an item container with the most up-to-date occurrences of some +/// item(s,) this function will propagate the state of the path aggregate of +/// each of those items throughout the entirety of the subtree rooted at the +/// `whole_crate` module tree. +fn sync_driver(items: FfiItems, tree: Module) -> Module { + items.items().fold(tree, |m, it| { + macro_rules! exe { + ($($ty:tt),+ $(,)?) => { + match it {$( + GenericItem::$ty(it) => sync_aggregates(&it.path_aggregate, m), + )+} + }; + } + exe! { + Type, + Struct, + Union, + Const, + Fn, + Static, + Module, + } + }) +} + +/// Provided a reference path aggregate corresponding with that of an item, this +/// function traverses module `m` in search of all occurrences of such an item +/// and "updates" such ocurrences to integrate the reference path aggregate +/// store. +fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { + let akin = |agg: &PathAggregateStore| agg.akin(reference); + let new_m = if akin(&m.path_aggregate) { + // [NOTE]: this we diff instead of merging because merging will get us a + // set of path aggregates that contain both the ones already in m, as + // well as the ones that were part of reference but not part of m. + // Diffing gets us only the paths in reference that are not part of m's + // path aggregate. + let new_agg = m.path_aggregate.diff(reference); + new_agg.into_iter().fold(m, Module::add_path) + } else { + m + }; + let new_items: FfiItems = new_m + .items .items() - .filter_map(|it| { + .map(|it| { macro_rules! exe { - ($($ty:tt),+ $(,)?) => {{ - match (it.clone(), reference.clone()) {$( - (GenericItem::$ty(iit), GenericItem::$ty(r)) => { - r.clone().akin_aggregates(iit.clone()).then_some(it.clone()) + ($($ty:tt),+ $(,)?) => { + match it { + $( + GenericItem::$ty(it) => { + if akin(&it.path_aggregate) { + GenericItem::$ty($ty { + path_aggregate: reference.clone(), + ..it + }) + } else { + GenericItem::$ty(it) + } + } + )+ + GenericItem::Module(it) => { + GenericItem::Module(sync_aggregates(reference, it)) } - )+ - _ => None, } - }}; + }; } exe! { Type, @@ -1324,64 +1341,13 @@ fn search_and_merge_aggregates( Const, Fn, Static, - Module, } }) - .map(|it| { - macro_rules! exe { - ($($ty:tt),+ $(,)?) => {{ - match (it.clone(), reference.clone()) {$( - (GenericItem::$ty(iit), GenericItem::$ty(r)) => { - let new_path_aggregate = iit.clone().merge_aggregates(r.clone()); - GenericItem::$ty($ty { - path_aggregate: new_path_aggregate.clone(), - ..iit.clone() - }) - }, - )+ - _ => unreachable!( - "The above filter_map got rid of all items that \ - were not of the same kind as reference." - ), - } - }}; - } - exe! { - Type, - Struct, - Union, - Const, - Fn, - Static, - Module, - } - }); - let mut matches = matches.clone(); - matches.extend(new_nested_matches); - matches.extend(new_matches); - matches.clone() -} - -fn sync_aggregates(src: FfiItems, whole_crate: Module) -> Module { - src.clone() - .items() - .fold(whole_crate.clone(), |whole_crate, it| { - let updated_item_occurrences = search_and_merge_aggregates( - FfiItems { - modules: vec![whole_crate.clone()], - ..Default::default() - }, - it.clone(), - vec![], - ); - todo!( - "Update the whole-crate module tree by integrating each of the \ - updated items in updated_item_occurrences. This is blocked on \ - getting the refactor of the module tree update function \ - finished." - ); - }); - todo!(); + .collect(); + Module { + items: new_items, + ..new_m + } } /// Appends a new module-local item to an absolute path that does _not_ start @@ -1695,6 +1661,21 @@ impl<'ast> Visit<'ast> for FfiItems { #[test] fn tmp() { + /* + * time should exist with the following paths: + * [time, test1::time, test1::test2::time] + * Foo should exist with the following paths: + * [ + * Foo, + * test2::test3::Foo, + * test2::Foo, + * test3::Foo, + * test1::Foo, + * test1::test3::Foo, + * test1::test2::Foo, + * test1::test2::test3::Foo, + * ] + */ let source = r#" use test2::test3; @@ -1724,11 +1705,27 @@ mod test1 { } } "#; + fn remove_uses(it: FfiItems) -> FfiItems { + let modules: Vec<_> = it + .modules + .clone() + .into_iter() + .map(|m| { + let items = remove_uses(m.items.clone()); + Module { items, ..m.clone() } + }) + .collect(); + FfiItems { + modules, + uses: vec![], + ..it.clone() + } + } let file = syn::parse_file(source).unwrap(); let items = FfiItems::default(); let mut items = items.clone(); items.visit_file(&file); - let items = items.clone(); + let items = remove_uses(items.clone()); println!("{:#?}", items); } From 6786cd3f2fdb60014c88f41ae3f84833e6952b42 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:30:33 +0100 Subject: [PATCH 091/100] chore: wip --- ctest/src/ffi_items.rs | 341 ++++++++++++++++++++--------------------- 1 file changed, 163 insertions(+), 178 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 4b9ab0e461863..cc485ab55ebd9 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -77,6 +77,8 @@ pub(crate) struct FfiItems { /// 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. + // [FIXME]: get rid of this and refactor parsing into using a helper that + // stores this ancilliary state. pub(crate) current_module: syn::Path, } @@ -153,7 +155,7 @@ impl FfiItems { items: self.clone(), ..Default::default() }; - *self = resolve_all(root_module.clone()).items.clone(); + *self = resolve_all(root_module).items; } /// Yields an iterator over all items in the calling `FfiItems`. @@ -223,19 +225,17 @@ impl FfiItems { // way around with "smaller" subtrees that we would like to have appear // later in the sorted list of modules. match ours.cmp(&theirs) { - Ordering::Equal => self - .modules - .clone() - .into_iter() - .zip(other.modules.clone()) - .fold(Ordering::Equal, |_, (our_m, their_m)| { + Ordering::Equal => self.modules.iter().zip(&other.modules).fold( + Ordering::Equal, + |_, (our_m, their_m)| { // [NOTE]: this is intended to not start anew a "regular" // module comparison through Module::custom_cmp, but to // rather have all subsequent comparisons be based off of // the sizes of module subtrees rather than the // lexicographical comparison of their paths. our_m.items.custom_cmp(&their_m.items) - }), + }, + ), other => other.reverse(), } } @@ -245,7 +245,7 @@ impl FfiItems { /// /// The generic item may be pattern-matched to find out specifically which /// item was found. - fn search(&self, ident: Ident) -> Option { + fn search(&self, ident: &Ident) -> Option { self.items().find(|it| { macro_rules! cmp { ($($var:tt),+ $(,)?) => { @@ -281,12 +281,12 @@ impl FfiItems { ($($it:tt),+ $(,)?) => {{ let items = self.clone(); $( - let mut $it = self.$it.clone(); + let mut $it = self.$it; $it.sort_unstable_by_key(key_fn!()); $it.dedup_by_key(key_fn!()); let items = FfiItems { - $it: $it.clone(), - ..items.clone() + $it: $it, + ..items }; )+ items @@ -300,12 +300,12 @@ impl FfiItems { foreign_functions, foreign_statics, ); - let mut new_modules = self.modules.clone(); + let mut new_modules = self.modules; new_modules.sort_unstable_by(Module::custom_cmp); new_modules.dedup_by(|Module { path: lhs, .. }, Module { path: rhs, .. }| *lhs == *rhs); FfiItems { - modules: new_modules.clone(), - ..new_items.clone() + modules: new_modules, + ..new_items } } } @@ -331,8 +331,7 @@ impl Default for FfiItems { impl FromIterator for FfiItems { fn from_iter>(iter: I) -> FfiItems { - let unpartitioned_items: Vec<_> = iter.into_iter().collect(); - let mut partitioned_items = unpartitioned_items.clone(); + let mut partitioned_items: Vec<_> = iter.into_iter().collect(); // [NOTE]: this is expected to sort all items by their kind, such that // we end up with the following ordering: // 1. Type aliases. @@ -342,6 +341,9 @@ impl FromIterator for FfiItems { // 5. Foreign functions. // 6. Foreign statics. // 7. Modules. + // This is then used to partition over the resulting collection and get + // "buckets" of GenericItem that can be mapped to a concrete type + // through partial mapping. partitioned_items.sort_by(|it1, it2| match (it1.clone(), it2.clone()) { (GenericItem::Type(_), GenericItem::Type(_)) | (GenericItem::Struct(_), GenericItem::Struct(_)) @@ -394,8 +396,7 @@ impl FromIterator for FfiItems { | (GenericItem::Module(_), GenericItem::Static(_)) => Ordering::Greater, }); macro_rules! get_partitions { - ($([$field:ident, $ty:tt]),+ $(,)?) => { - $( + ($([$field:ident, $ty:tt]),+ $(,)?) => {$( let start = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_)).not()); let end = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_))); @@ -424,8 +425,7 @@ impl FromIterator for FfiItems { indices into the collection.", ) }; - )+ - }; + )+}; } get_partitions! { [aliases, Type], @@ -560,7 +560,7 @@ struct PassPair { /// termination. fn resolve_all(root: Module) -> Module { let (ControlFlow::Break(m) | ControlFlow::Continue(m)) = - iter::repeat(()).try_fold(root.clone(), |root, _| { + iter::repeat(()).try_fold(root, |root, _| { // [NOTE]: it holds that a pass over the crate root will yield the // crate root as its subtree. It is thus safe to ignore the // whole-crate module tree. They are the same at this point. @@ -571,8 +571,8 @@ fn resolve_all(root: Module) -> Module { whole_crate: root.clone(), }); match new_root == root { - true => ControlFlow::Break(new_root.clone()), - false => ControlFlow::Continue(new_root.clone()), + true => ControlFlow::Break(new_root), + false => ControlFlow::Continue(new_root), } }); m @@ -611,20 +611,20 @@ fn resolve_pass(src: PassPair) -> PassPair { (src.clone(), Vec::new()), |(PassPair { whole_crate, .. }, children), m| { let pair = resolve_pass(PassPair { - subtree: m.clone(), - whole_crate: whole_crate.clone(), + subtree: m, + whole_crate: whole_crate, }); - let mut new_children = children.clone(); + let mut new_children = children; new_children.push(pair.subtree.clone()); - (pair.clone(), new_children.clone()) + (pair, new_children) }, ); let root_with_resolved_children = Module { items: FfiItems { - modules: resolved_children.clone(), - ..src.subtree.items.clone() + modules: resolved_children, + ..src.subtree.items }, - ..src.subtree.clone() + ..src.subtree }; let resolved_uses = resolve_one(PassPair { subtree: root_with_resolved_children.clone(), @@ -635,31 +635,31 @@ fn resolve_pass(src: PassPair) -> PassPair { whole_crate: new_whole_crate_with_resolved_children, } = merge_module( PassPair { - subtree: root_with_resolved_children.clone(), - whole_crate: whole_crate_with_resolved_children.clone(), + subtree: root_with_resolved_children, + whole_crate: whole_crate_with_resolved_children, }, - resolved_uses.clone(), + resolved_uses, ); let new_root = Module { - items: new_root_with_duplicates.items.clone().dedup(), - ..new_root_with_duplicates.clone() + items: new_root_with_duplicates.items.dedup(), + ..new_root_with_duplicates }; let new_whole_crate = update_tree( - new_root.path.clone(), + &new_root.path, |_| GenericItem::Module(new_root.clone()), - new_whole_crate_with_resolved_children.clone(), + new_whole_crate_with_resolved_children, ); println!( "finished module pass on module {}", if &*new_root.cached_path == "" { "root".into() } else { - new_root.cached_path.clone() + &*new_root.cached_path }, ); PassPair { - subtree: new_root.clone(), - whole_crate: new_whole_crate.clone(), + subtree: new_root, + whole_crate: new_whole_crate, } } @@ -675,9 +675,9 @@ fn resolve_one(state: PassPair) -> Vec { "found {} use declarations in module {}", state.subtree.items.uses.len(), if &*state.subtree.cached_path == "" { - "root".into() + "root" } else { - state.subtree.cached_path.clone() + &*state.subtree.cached_path }, ); state @@ -686,7 +686,7 @@ fn resolve_one(state: PassPair) -> Vec { .uses .clone() .into_iter() - .map(|u| resolve_use(u.clone(), state.clone())) + .map(|u| resolve_use(u, state.clone())) .collect() } @@ -719,7 +719,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! single_item { ($field:ident: $it:expr) => { return Resolution::Resolved(FfiItems { - $field: Vec::from([$it.clone()]), + $field: Vec::from([$it]), ..Default::default() }) }; @@ -730,7 +730,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { ($it:ident : $ty:tt) => { $ty { public: r#use.is_public, - ..$it.clone() + ..$it } }; } @@ -738,7 +738,6 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! c { ($($field:ident: $ty:tt),+ $(,)?) => {$( let $field: Vec<_> = m.items.$field - .clone() .into_iter() .map(|it| change_vis!(it: $ty)) .collect(); @@ -762,17 +761,16 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { foreign_functions, foreign_statics, modules, - ..m.items.clone() + ..m.items }, - ..m.clone() + ..m } }; match r#use.tree.clone() { RefinedUseTree::Name(UseName { ident }) => { println!( "entered name resolution case with {} in module {}", - ident.clone(), - state.subtree.cached_path.clone() + ident, state.subtree.cached_path ); // [NOTE]: self may not appear as the name basecase of a well-formed // path. And yet it is still considered here. That is because of @@ -784,12 +782,11 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { // [NOTE]: crate does not appear as the tail end of a path in a use // declaration under no circumstances, so that case is not // considered. - if is_self(ident.clone()) { - single_item!(modules: change_vis_in_mod(state.subtree.clone())); - } else if is_super(ident.clone()) { - let parent_path = get_parent_path(state.subtree.clone()); - let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) - else { + if is_self(&ident) { + single_item!(modules: change_vis_in_mod(state.subtree)); + } else if is_super(&ident) { + let parent_path = get_parent_path(state.subtree); + let Some(parent) = get_module(parent_path, state.whole_crate) else { unreachable!( "The module path is guaranteed to be found in the \ whole crate module tree. It is the path of the \ @@ -804,7 +801,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { }; single_item!(modules: change_vis_in_mod(parent.clone())); } - match state.subtree.items.search(ident.clone()) { + match state.subtree.items.search(&ident) { Some(GenericItem::Type(t)) => single_item!(aliases: change_vis!(t: Type)), Some(GenericItem::Struct(s)) => single_item!(structs: change_vis!(s: Struct)), Some(GenericItem::Union(u)) => single_item!(unions: change_vis!(u: Union)), @@ -821,12 +818,12 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { println!( "entered glob resolution case in module {}", if &*state.subtree.cached_path == "" { - "root".into() + "root" } else { - state.subtree.cached_path.clone() + &*state.subtree.cached_path }, ); - Resolution::Resolved(change_vis_in_mod(state.subtree.clone()).items) + Resolution::Resolved(change_vis_in_mod(state.subtree).items) } RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { println!( @@ -836,7 +833,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { as_token: syn::parse_quote! {as}, rename: rename.clone(), }, - state.subtree.cached_path.clone() + state.subtree.cached_path ); macro_rules! new_path { ($it:expr) => {{ @@ -845,30 +842,27 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { .segments .clone() .into_iter() - .skip($it.path.segments.len() - 1) - .fold(Punctuated::new(), |p, _| { - let mut p = p.clone(); - p.push(PathSegment { - ident: rename.clone(), - arguments: PathArguments::None, - }); - p.clone() - }); + .skip($it.path.segments.len().saturating_sub(1)) + .map(|_| PathSegment { + ident: rename.clone(), + arguments: PathArguments::None, + }) + .collect(); Path { - segments: new_segments.clone(), - ..$it.path.clone() + segments: new_segments, + ..$it.path } }}; } - macro_rules! renamed_single_item { + macro_rules! rename_single_item { ($field:ident , $ty:tt : $it:expr) => {{ - let new_path = new_path!($it.clone()); - let new_item = $it.rename(new_path.clone()); - single_item!($field: new_item.clone()); + let new_path = new_path!($it); + let new_item = $it.rename(new_path); + single_item!($field: new_item); }}; } let renamer = |m: Module| { - let new_path = new_path!(m.clone()); + let new_path = new_path!(m); // [NOTE]: it may seem better to use the segment length of the // old path of the module, much like we do in function // RenameUtils::rename, but that is not necessary here. We know @@ -878,46 +872,44 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { // segment of the item's path. let new_path_len = new_path.segments.len(); m.clone().rename_with(|p| { - let new_path = p.segments.clone().into_iter().skip(new_path_len).fold( - new_path.clone(), - |p, s| { - let mut p = p.clone(); - p.segments.push(s); - p.clone() - }, - ); + let segments_we_keep = p.segments.clone().into_iter().skip(new_path_len); + let new_segments = new_path + .segments + .clone() + .into_iter() + .chain(segments_we_keep) + .collect(); Path { leading_colon: p.leading_colon, - ..new_path.clone() + segments: new_segments, } }) }; - if is_self(ident.clone()) { - single_item!(modules: change_vis_in_mod(renamer(state.subtree.clone()))); - } else if is_super(ident.clone()) { - let parent_path = get_parent_path(state.subtree.clone()); - let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) - else { + if is_self(&ident) { + single_item!(modules: change_vis_in_mod(renamer(state.subtree))); + } else if is_super(&ident) { + let parent_path = get_parent_path(state.subtree); + let Some(parent) = get_module(parent_path, state.whole_crate) else { unreachable!( "See the note on super in the name basecase. The same \ applies here." ); }; - single_item!(modules: change_vis_in_mod(renamer(parent.clone()))); - } else if is_crate(ident.clone()) { - single_item!(modules: change_vis_in_mod(renamer(state.whole_crate.clone()))); + single_item!(modules: change_vis_in_mod(renamer(parent))); + } else if is_crate(&ident) { + single_item!(modules: change_vis_in_mod(renamer(state.whole_crate))); } - match state.subtree.items.search(ident.clone()) { + match state.subtree.items.search(&ident) { Some(GenericItem::Module(m)) => { - single_item!(modules: change_vis_in_mod(renamer(m.clone()))) + single_item!(modules: change_vis_in_mod(renamer(m))) } - Some(GenericItem::Type(t)) => renamed_single_item!(aliases, Type: t.clone()), - Some(GenericItem::Struct(s)) => renamed_single_item!(structs, Struct: s.clone()), - Some(GenericItem::Union(u)) => renamed_single_item!(unions, Union: u.clone()), - Some(GenericItem::Const(c)) => renamed_single_item!(constants, Const: c.clone()), - Some(GenericItem::Fn(f)) => renamed_single_item!(foreign_functions, Fn: f.clone()), + Some(GenericItem::Type(t)) => rename_single_item!(aliases, Type: t), + Some(GenericItem::Struct(s)) => rename_single_item!(structs, Struct: s), + Some(GenericItem::Union(u)) => rename_single_item!(unions, Union: u), + Some(GenericItem::Const(c)) => rename_single_item!(constants, Const: c), + Some(GenericItem::Fn(f)) => rename_single_item!(foreign_functions, Fn: f), Some(GenericItem::Static(s)) => { - renamed_single_item!(foreign_statics, Static: s.clone()) + rename_single_item!(foreign_statics, Static: s) } None => Resolution::Unresolved, } @@ -926,12 +918,14 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { println!( "entered path resolution case with {} in module {}", - ident.clone(), - state.subtree.cached_path.clone() + ident, state.subtree.cached_path ); - let new_subtree = if is_self(ident.clone()) { - state.subtree.clone() - } else if is_super(ident.clone()) { + let new_state = if is_self(&ident) { + PassPair { + subtree: state.subtree, + whole_crate: state.whole_crate, + } + } else if is_super(&ident) { let parent_path = get_parent_path(state.subtree.clone()); let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) else { @@ -940,40 +934,45 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { applies here." ); }; - parent.clone() - } else if is_crate(ident.clone()) { - state.whole_crate.clone() + PassPair { + subtree: parent, + whole_crate: state.whole_crate, + } + } else if is_crate(&ident) { + PassPair { + subtree: state.whole_crate.clone(), + whole_crate: state.whole_crate, + } } else { - match state.subtree.items.search(ident.clone()) { - Some(GenericItem::Module(m)) => m.clone(), + match state.subtree.items.search(&ident) { + Some(GenericItem::Module(m)) => PassPair { + subtree: m, + whole_crate: state.whole_crate, + }, Some(_) | None => return Resolution::Unresolved, } }; resolve_use( RefinedUse { - tree: *tree.clone(), + tree: *tree, ..r#use }, - PassPair { - subtree: new_subtree.clone(), - ..state.clone() - }, + new_state, ) } } } -/// Attempts to find a given module path (`target`) within a module subtree -/// (`root`.) +/// Attempts to find a given module, provided its path (`target`,) within a +/// module subtree (`root`.) fn get_module(target: Path, root: Module) -> Option { if root.path == target { - return root.clone().into(); + return root.into(); } root.items .modules - .clone() .into_iter() - .find_map(|m| get_module(target.clone(), m.clone())) + .find_map(|m| get_module(target.clone(), m)) } /// Fetches the parent path to a given module. @@ -984,7 +983,7 @@ fn get_module(target: Path, root: Module) -> Option { fn get_parent_path(root: Module) -> Path { // [NOTE]: this performs a saturating_sub because there is one and only one // item in the crate's module tree that has an empty path; The crate root. - // That is only wrapped in a Module of its own only after parsing. This + // That is only wrapped in a Module of its own only after parsing. This // function currently only gets used during parsing, so there is a chance // this gets called with the virtual module that wraps the crate root's // FfiItems. That is not logically possible, because well-formed input can @@ -995,35 +994,30 @@ fn get_parent_path(root: Module) -> Path { // you could very well change the saturating_sub for overflowing // subtraction. It is best if this function is protected against what would // currently be misuse. - let new_segments = root - .path - .segments - .clone() - .into_iter() - .take(root.path.segments.len().saturating_sub(1)) - .fold(Punctuated::new(), |p, s| { - let mut p = p.clone(); - p.push(s); - p.clone() - }); + let segments_before_ident = root.path.segments.len().saturating_sub(1); Path { leading_colon: None, - segments: new_segments.clone(), + segments: root + .path + .segments + .into_iter() + .take(segments_before_ident) + .collect(), } } /// Checks if the given identifier was parsed as the Rust keyword `self`. -fn is_self(ident: Ident) -> bool { +fn is_self(ident: &Ident) -> bool { ident.to_string() == "self" } /// Checks if the given identifier was parsed as the Rust keyword `crate`. -fn is_crate(ident: Ident) -> bool { +fn is_crate(ident: &Ident) -> bool { ident.to_string() == "crate" } /// Checks if the given identifier was parsed as the Rust keyword `super`. -fn is_super(ident: Ident) -> bool { +fn is_super(ident: &Ident) -> bool { ident.to_string() == "super" } @@ -1033,7 +1027,7 @@ fn is_super(ident: Ident) -> bool { /// It is key that the generic wrapper around the item returned is the same as /// the generic item that was passed in. fn update_tree( - target: Path, + target: &Path, action: impl ops::Fn(GenericItem) -> GenericItem + Clone, root: Module, ) -> Module { @@ -1045,11 +1039,11 @@ fn update_tree( ); }; } - if target == root.path { - let GenericItem::Module(new_root) = action(GenericItem::Module(root.clone())) else { + if *target == root.path { + let GenericItem::Module(new_root) = action(GenericItem::Module(root)) else { ignored!(); }; - return new_root.into(); + return new_root; } macro_rules! exe { ($($field:tt: $ty:tt),+ $(,)?) => { @@ -1060,9 +1054,9 @@ fn update_tree( .clone() .into_iter() .map(|it| { - if it.path == target { - let new = action(GenericItem::$ty(it.clone())); - let GenericItem::$ty(it) = new.clone() else { + if it.path == *target { + let new = action(GenericItem::$ty(it)); + let GenericItem::$ty(it) = new else { ignored!(); }; it @@ -1087,22 +1081,21 @@ fn update_tree( // found in the current module, by recursing with the same function on each // of the child modules. let modules: Vec<_> = modules - .clone() .into_iter() - .map(|m| update_tree(target.clone(), action.clone(), m.clone())) + .map(|m| update_tree(target, action.clone(), m)) .collect(); Module { items: FfiItems { - aliases: aliases.clone(), - structs: structs.clone(), - unions: unions.clone(), - constants: constants.clone(), - foreign_functions: foreign_functions.clone(), - foreign_statics: foreign_statics.clone(), - modules: modules.clone(), + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, ..root.items.clone() }, - ..root.clone() + ..root } } @@ -1116,12 +1109,11 @@ fn update_tree( /// existing items on `dst`. It is call-site's responsibility to deduplicate /// those through [`FfiItems::dedup`]. fn merge_module(state: PassPair, src: Vec) -> PassPair { - let new_path = state.subtree.path.clone(); let processed_items: Vec<_> = src .into_iter() .filter_map(|r| { - if let Resolution::Resolved(items) = r.clone() { - Some(items.clone()) + if let Resolution::Resolved(items) = r { + Some(items) } else { None } @@ -1190,31 +1182,26 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { segments: source_module_path, }, items: FfiItems { - aliases: aliases.clone(), - structs: structs.clone(), - unions: unions.clone(), - constants: constants.clone(), - foreign_functions: foreign_functions.clone(), - foreign_statics: foreign_statics.clone(), - modules: modules.clone(), + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, ..Default::default() }, ..Default::default() }; - let new_dummy_module = dummy_module.rename(new_path.clone()); + let new_dummy_module = dummy_module.rename(state.subtree.path.clone()); fn remove_uses(m: Module) -> Module { - let new_modules: Vec<_> = m - .items - .modules - .clone() - .into_iter() - .map(remove_uses) - .collect(); + let new_modules: Vec<_> = + m.items.modules.into_iter().map(remove_uses).collect(); Module { items: FfiItems { uses: vec![], modules: new_modules, - ..m.items.clone() + ..m.items }, ..m } @@ -1226,7 +1213,6 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { let new_modules_without_uses: Vec<_> = new_dummy_module .items .modules - .clone() .into_iter() .map(remove_uses) .collect(); @@ -1237,7 +1223,6 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { }, ) .collect(); - processed_items.into_iter().fold( state.clone(), |PassPair { From acd903b625b0a7edfa2f3ccac49d2fb28bae6a25 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:53:07 +0100 Subject: [PATCH 092/100] chore: wip --- ctest/src/ast/constant.rs | 10 ++--- ctest/src/ast/function.rs | 10 ++--- ctest/src/ast/mod.rs | 66 +++++++++++++++++++------------- ctest/src/ast/module.rs | 79 ++++++++++++++++++++------------------- ctest/src/ffi_items.rs | 69 ++++++++++++++++++---------------- 5 files changed, 126 insertions(+), 108 deletions(-) diff --git a/ctest/src/ast/constant.rs b/ctest/src/ast/constant.rs index df73312c96232..cb0f18934d868 100644 --- a/ctest/src/ast/constant.rs +++ b/ctest/src/ast/constant.rs @@ -38,12 +38,12 @@ impl Const { } impl PathUtils for Const { - fn path_aggregate(self) -> PathAggregateStore { - self.path_aggregate.clone() + fn path_aggregate(&self) -> &PathAggregateStore { + &self.path_aggregate } - fn path(self) -> Path { - self.path.clone() + fn path(&self) -> &Path { + &self.path } fn mk( @@ -58,7 +58,7 @@ impl PathUtils for Const { cached_ident, path, path_aggregate, - ..self.clone() + ..self } } } diff --git a/ctest/src/ast/function.rs b/ctest/src/ast/function.rs index 7469168ec16af..b13bacc5c51a0 100644 --- a/ctest/src/ast/function.rs +++ b/ctest/src/ast/function.rs @@ -49,12 +49,12 @@ impl Fn { } impl PathUtils for Fn { - fn path_aggregate(self) -> PathAggregateStore { - self.path_aggregate.clone() + fn path_aggregate(&self) -> &PathAggregateStore { + &self.path_aggregate } - fn path(self) -> Path { - self.path.clone() + fn path(&self) -> &Path { + &self.path } fn mk( @@ -69,7 +69,7 @@ impl PathUtils for Fn { cached_ident, path, path_aggregate: addressable_paths, - ..self.clone() + ..self } } } diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 7e02e39febd5c..46dfbf4f21331 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -8,6 +8,7 @@ mod structure; mod type_alias; mod union; +use std::slice::Iter; use std::vec::IntoIter; use std::{ fmt, @@ -48,8 +49,8 @@ impl PathAggregateStore { } /// Appends a new path to the path aggregate store. - pub(crate) fn push(&self, p: Path) -> Self { - let PathAggregateStore { mut value } = self.clone(); + pub(crate) fn push(self, p: Path) -> Self { + let PathAggregateStore { mut value } = self; value.push(p); Self { value } } @@ -88,13 +89,15 @@ impl PathAggregateStore { /// Unlike diffing, the resulting path aggregate store unifies the paths /// from both parties; The receiver and `other`. #[expect(unused)] - pub(crate) fn merge(&self, other: &Self) -> Self { - let diff = self.diff(other); - diff.into_iter().fold(self.clone(), |agg, p| agg.push(p)) + pub(crate) fn merge(self, other: &Self) -> Self { + self.diff(other) + .into_iter() + .fold(self, PathAggregateStore::push) } } pub(crate) type AddressablePaths = IntoIter; +pub(crate) type RefAddressablePaths<'a> = Iter<'a, Path>; impl IntoIterator for PathAggregateStore { type IntoIter = AddressablePaths; @@ -105,6 +108,15 @@ impl IntoIterator for PathAggregateStore { } } +impl<'a> IntoIterator for &'a PathAggregateStore { + type IntoIter = RefAddressablePaths<'a>; + type Item = &'a Path; + + fn into_iter(self) -> Self::IntoIter { + self.value.iter() + } +} + impl Extend for PathAggregateStore { fn extend>(&mut self, iter: I) { self.value.extend(iter); @@ -124,8 +136,8 @@ impl Default for PathAggregateStore { /// all of which need updating whenever the path is renamed. We ensure that the /// only place where this happens is in this function for convenience's sake. pub(crate) trait PathUtils: Clone { - fn path(self) -> Path; - fn path_aggregate(self) -> PathAggregateStore; + fn path(&self) -> &Path; + fn path_aggregate(&self) -> &PathAggregateStore; fn mk( self, cached_path: BoxStr, @@ -141,13 +153,13 @@ pub(crate) trait PathUtils: Clone { /// of the item itself, but rather produces an updated receiver with only /// the path aggregate store expanded. fn add_path(self, path: Path) -> Self { - let aggregate = self.clone().path_aggregate(); - let new_aggregate = aggregate.push(path); - self.clone().mk( - path_to_string(self.clone().path()), - ident_to_string(self.clone().path()), - self.clone().path(), - new_aggregate, + let old_path = self.path().clone(); + let old_aggregate = self.path_aggregate().clone(); + self.mk( + path_to_string(&old_path), + ident_to_string(&old_path), + old_path, + old_aggregate.push(path), ) } @@ -166,24 +178,26 @@ pub(crate) trait PathUtils: Clone { /// This is a utility to avoid having to go through the variations, as well /// as updating the identifier and paths caches whenever some operation /// needs to be performed on all variations. - fn with_variations(&self, f: impl ops::Fn(Self) -> T + Clone) -> Vec { - self.clone() - .path_aggregate() + fn with_variations(&self, f: impl ops::Fn(&Self) -> T) -> Vec { + self.path_aggregate() .into_iter() - .map(|p| f(self.clone().rename(p.clone()))) + .cloned() + .map(|p| f(&self.clone().rename(p))) .collect() } - fn rename_with(self, new_path: impl ops::Fn(Path) -> Path + Clone) -> Self { - self.clone().rename(new_path(self.clone().path())) + fn rename_with(self, f: impl ops::Fn(&Path) -> Path) -> Self { + let path = self.path().clone(); + self.rename(f(&path)) } fn rename(self, new_path: Path) -> Self { - self.clone().mk( - path_to_string(new_path.clone()), - ident_to_string(new_path.clone()), + let old_aggregate = self.path_aggregate().clone(); + self.mk( + path_to_string(&new_path), + ident_to_string(&new_path), new_path.clone(), - self.path_aggregate().push(new_path), + old_aggregate.push(new_path), ) } } @@ -206,7 +220,7 @@ pub(crate) fn escape_item_path<'a>(item: impl Into>) -> String { /// Returns a stringified `syn::Path` that is meant to match one-to-one the path /// to the item from the crate root. -pub(crate) fn path_to_string(path: Path) -> BoxStr { +pub(crate) fn path_to_string(path: &Path) -> BoxStr { path.into_token_stream() .to_string() .replace(|c: char| c.is_ascii_whitespace(), "") @@ -215,7 +229,7 @@ pub(crate) fn path_to_string(path: Path) -> BoxStr { /// Returns a stringified representation of the last segment in the passed /// identifier. -pub(crate) fn ident_to_string(path: Path) -> BoxStr { +pub(crate) fn ident_to_string(path: &Path) -> BoxStr { path.segments .last() .map(|it| it.ident.to_string().into_boxed_str()) diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index b425a10a9b86d..172929ffdf44d 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -64,12 +64,12 @@ impl Module { } impl PathUtils for Module { - fn path_aggregate(self) -> PathAggregateStore { - self.path_aggregate.clone() + fn path_aggregate(&self) -> &PathAggregateStore { + &self.path_aggregate } - fn path(self) -> Path { - self.path.clone() + fn path(&self) -> &Path { + &self.path } fn mk( @@ -84,54 +84,60 @@ impl PathUtils for Module { cached_ident, path, path_aggregate: addressable_paths, - ..self.clone() + ..self } } fn add_path(self, path: Path) -> Self { - fn handle_item(it: GenericItem, original: Path, new: Path) -> GenericItem { + fn handle_item(it: GenericItem, original: &Path, new: &Path) -> GenericItem { macro_rules! map { ($($ty:tt),+ $(,)?) => { - match it.clone() { + match it { $( GenericItem::$ty(it) => { - let kept_segments = it - .clone() - .path() + let kept_segments = <$ty as PathUtils>::path(&it) .segments + .clone() .into_iter() .skip(original.segments.len()); - let mut new_path = new.clone(); - new_path.segments.extend(kept_segments); + let new_path = Path { + segments: new.segments.clone().into_iter().chain(kept_segments).collect(), + ..new.clone() + }; + GenericItem::$ty($ty { - path_aggregate: it.clone().path_aggregate(), - ..it.clone() + path_aggregate: it.path_aggregate().clone().push(new_path), + ..it }) } )+ GenericItem::Module(m) => { - let kept_segments = m - .clone() - .path() + let kept_segments = ::path(&m) .segments + .clone() .into_iter() .skip(original.segments.len()); - let mut new_path = new.clone(); - new_path.segments.extend(kept_segments); - let new_items: FfiItems = m + let new_path = Path { + segments: new.segments.clone().into_iter().chain(kept_segments).collect(), + ..new.clone() + }; + + let new_items = m .items .items() - .map(|it| handle_item(it.clone(), original.clone(), new.clone())) + .map(|it| handle_item(it, &original, &new)) .collect(); + GenericItem::Module(Module { - path_aggregate: m.clone().path_aggregate().push(new_path.clone()), - items: new_items.clone(), - ..m.clone() + path_aggregate: m.path_aggregate().clone().push(new_path), + items: new_items, + ..m }) } } }; } + map! { Type, Struct, @@ -141,22 +147,17 @@ impl PathUtils for Module { Static, } } - let new_aggregate = self.path_aggregate.push(path.clone()); - let diff = self.path_aggregate.diff(&new_aggregate); - let new_module = diff.into_iter().fold(self.clone(), |m, p| { - let new_items: FfiItems = self - .items - .items() - .map(|it| handle_item(it.clone(), m.path.clone(), p.clone())) - .collect(); - Module { - items: new_items.clone(), - ..m.clone() - } - }); + + let new_items = self + .items + .items() + .map(|it| handle_item(it, ::path(&self), &path)) + .collect(); + Module { - path_aggregate: new_aggregate.clone(), - ..new_module.clone() + path_aggregate: self.path_aggregate.push(path), + items: new_items, + ..self } } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index cc485ab55ebd9..f030a8d73b025 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -545,7 +545,7 @@ fn normalize_path(path: syn::UseTree) -> Vec { /// /// Points (2) and (3) require having access to the entire crate's module tree. /// Some ancestor item is looked up by the parent path to the item being -/// searched in respect of. Point (1) is satisfied by the `subtree` field. +/// searched with respect to. Point (1) is satisfied by the `subtree` field. #[derive(Debug, Clone)] struct PassPair { subtree: Module, @@ -785,8 +785,8 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { if is_self(&ident) { single_item!(modules: change_vis_in_mod(state.subtree)); } else if is_super(&ident) { - let parent_path = get_parent_path(state.subtree); - let Some(parent) = get_module(parent_path, state.whole_crate) else { + let parent_path = get_parent_path(&state.subtree); + let Some(parent) = get_module(&parent_path, &state.whole_crate) else { unreachable!( "The module path is guaranteed to be found in the \ whole crate module tree. It is the path of the \ @@ -888,14 +888,14 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { if is_self(&ident) { single_item!(modules: change_vis_in_mod(renamer(state.subtree))); } else if is_super(&ident) { - let parent_path = get_parent_path(state.subtree); - let Some(parent) = get_module(parent_path, state.whole_crate) else { + let parent_path = get_parent_path(&state.subtree); + let Some(parent) = get_module(&parent_path, &state.whole_crate) else { unreachable!( "See the note on super in the name basecase. The same \ applies here." ); }; - single_item!(modules: change_vis_in_mod(renamer(parent))); + single_item!(modules: change_vis_in_mod(renamer(parent.clone()))); } else if is_crate(&ident) { single_item!(modules: change_vis_in_mod(renamer(state.whole_crate))); } @@ -926,16 +926,15 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { whole_crate: state.whole_crate, } } else if is_super(&ident) { - let parent_path = get_parent_path(state.subtree.clone()); - let Some(parent) = get_module(parent_path.clone(), state.whole_crate.clone()) - else { + let parent_path = get_parent_path(&state.subtree); + let Some(parent) = get_module(&parent_path, &state.whole_crate) else { unreachable!( "See the note on super in the name basecase. The same \ applies here." ); }; PassPair { - subtree: parent, + subtree: parent.clone(), whole_crate: state.whole_crate, } } else if is_crate(&ident) { @@ -965,14 +964,14 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { /// Attempts to find a given module, provided its path (`target`,) within a /// module subtree (`root`.) -fn get_module(target: Path, root: Module) -> Option { - if root.path == target { +fn get_module<'m>(target: &Path, root: &'m Module) -> Option<&'m Module> { + if root.path == *target { return root.into(); } root.items .modules - .into_iter() - .find_map(|m| get_module(target.clone(), m)) + .iter() + .find_map(|m| get_module(target, m)) } /// Fetches the parent path to a given module. @@ -980,7 +979,7 @@ fn get_module(target: Path, root: Module) -> Option { /// If the module is a top-level item (i.e. it appears at the crate root,) the /// returned path will be empty. This follows from the parsing strategy for /// paths in `FfiItems`. The absolute paths are all `crate`-_unprepended_. -fn get_parent_path(root: Module) -> Path { +fn get_parent_path(root: &Module) -> Path { // [NOTE]: this performs a saturating_sub because there is one and only one // item in the crate's module tree that has an empty path; The crate root. // That is only wrapped in a Module of its own only after parsing. This @@ -1000,8 +999,9 @@ fn get_parent_path(root: Module) -> Path { segments: root .path .segments - .into_iter() + .iter() .take(segments_before_ident) + .cloned() .collect(), } } @@ -1109,6 +1109,9 @@ fn update_tree( /// existing items on `dst`. It is call-site's responsibility to deduplicate /// those through [`FfiItems::dedup`]. fn merge_module(state: PassPair, src: Vec) -> PassPair { + // [NOTE]: we collect all items here because we want the actions in the + // mapping to take place in whole before we start folding over the items to + // merge. let processed_items: Vec<_> = src .into_iter() .filter_map(|r| { @@ -1143,9 +1146,9 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { $field.first().map(|it| { it.path .segments - .clone() - .into_iter() + .iter() .take(it.path.segments.len() - 1) + .cloned() .collect::>() }) }}; @@ -1157,9 +1160,9 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { // items down in the fold that finishes up the merging function. // We don't invoke unreachable! in the diverging branch because // a resolution item could pack an empty item container if a use - // declaration consists a glob import of all items in a module - // with no items. We can be sure that all items about to be - // merged share the same parent because use declarations are + // declaration consists of a glob import of all items in a + // module with no items. We can be sure that all items about to + // be merged share the same parent because use declarations are // cleansed of groups at parse-time. let Some(source_module_path) = seek_parent!( aliases, @@ -1175,7 +1178,9 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { // [NOTE]: the dummy module is only in charge of wrapping all // items we want to rename with a path that makes up some // initial portion of the path segments of all descendant items' - // path. + // path. This is so that the below call to function + // RenameUtils::rename can use it as reference for the subtree + // items' path. let dummy_module = Module { path: Path { leading_colon: None, @@ -1195,12 +1200,10 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { }; let new_dummy_module = dummy_module.rename(state.subtree.path.clone()); fn remove_uses(m: Module) -> Module { - let new_modules: Vec<_> = - m.items.modules.into_iter().map(remove_uses).collect(); Module { items: FfiItems { uses: vec![], - modules: new_modules, + modules: m.items.modules.into_iter().map(remove_uses).collect(), ..m.items }, ..m @@ -1224,7 +1227,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { ) .collect(); processed_items.into_iter().fold( - state.clone(), + state, |PassPair { subtree, whole_crate, @@ -1232,8 +1235,8 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { it| { // [TODO]: this is still wrong but it also seems to be headed in the // right direction. - let new_whole_crate = sync_driver(it.clone(), whole_crate); - let Some(new_subtree) = get_module(subtree.path, new_whole_crate.clone()) else { + let new_whole_crate = sync_driver(&it, whole_crate); + let Some(new_subtree) = get_module(&subtree.path, &new_whole_crate) else { unreachable!( "The subtree is bound to be found in the whole-crate \ module tree because it was sourced from it, and syncing \ @@ -1244,7 +1247,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { PassPair { subtree: Module { items: new_subtree.items.items().chain(it.items()).collect(), - ..new_subtree + ..new_subtree.clone() }, whole_crate: new_whole_crate, } @@ -1253,10 +1256,10 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { } /// Provided an item container with the most up-to-date occurrences of some -/// item(s,) this function will propagate the state of the path aggregate of -/// each of those items throughout the entirety of the subtree rooted at the -/// `whole_crate` module tree. -fn sync_driver(items: FfiItems, tree: Module) -> Module { +/// item(s) (`items`,) this function will propagate the state of the path +/// aggregate of each of those items throughout the entirety of the subtree +/// rooted at the `tree` module tree. +fn sync_driver(items: &FfiItems, tree: Module) -> Module { items.items().fold(tree, |m, it| { macro_rules! exe { ($($ty:tt),+ $(,)?) => { From 77f2eeb1c3c6cd8f5f3e011ed91ad7eecef5da14 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:00:22 +0100 Subject: [PATCH 093/100] chore: wip --- ctest/src/ast/mod.rs | 14 ++ ctest/src/ast/module.rs | 46 ++--- ctest/src/ast/static_variable.rs | 10 +- ctest/src/ast/structure.rs | 18 +- ctest/src/ast/type_alias.rs | 10 +- ctest/src/ast/union.rs | 10 +- ctest/src/ffi_items.rs | 303 +++++++++++++++---------------- 7 files changed, 208 insertions(+), 203 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 46dfbf4f21331..fa498a94d4d65 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -129,6 +129,20 @@ impl Default for PathAggregateStore { } } +impl From> for PathAggregateStore { + fn from(value: Vec) -> Self { + Self { value } + } +} + +impl FromIterator for PathAggregateStore { + fn from_iter>(iter: I) -> Self { + Self { + value: iter.into_iter().collect(), + } + } +} + /// Utility interface for keeping a single renaming function for all parsed /// items. /// diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 172929ffdf44d..481f3d3520315 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -170,14 +170,15 @@ impl PathUtils for Module { // means all segments will be skipped and nothing will be appended to the // new path. In all other cases of items within its subtree, they will have // the previous identifier of the ancestor in which this rename request was - // originally called trimmed, and whatever segments are left will be + // originally called, trimmed, and whatever segments are left will be // appended to the new path of that (potentially grand-) ancestor. fn rename(self, new_path: Path) -> Self { let old_path_len = self.path.segments.len(); self.rename_with(|p| { p.segments - .into_iter() + .iter() .skip(old_path_len) + .cloned() .fold(new_path.clone(), |new_path, s| Path { segments: new_path.segments.clone().into_iter().chain([s]).collect(), ..new_path @@ -185,14 +186,16 @@ impl PathUtils for Module { }) } - fn rename_with(self, f: impl ops::Fn(Path) -> Path + Clone) -> Self { + fn rename_with(self, f: impl ops::Fn(&Path) -> Path) -> Self { macro_rules! map_non_modules { ($($field:ident: $ty:tt),+ $(,)?) => {$( let $field = self.items .$field - .clone() .into_iter() - .map(|it| it.clone().rename(f(it.path.clone()))) + .map(|it| { + let path = f(&it.path); + it.rename(path) + }) .collect::>(); )+}; } @@ -204,31 +207,30 @@ impl PathUtils for Module { foreign_functions: Fn, foreign_statics: Static, } - let modules: Vec<_> = self + let modules = self .items .modules - .clone() .into_iter() - .map(|m| m.clone().rename_with(f.clone())) + .map(|m| m.rename_with(&f)) .collect(); let new_items = FfiItems { - aliases: aliases.clone(), - structs: structs.clone(), - unions: unions.clone(), - constants: constants.clone(), - foreign_functions: foreign_functions.clone(), - foreign_statics: foreign_statics.clone(), - modules: modules.clone(), - ..Default::default() + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + ..self.items }; - let new_path = f(self.path.clone()); + let new_path = f(&self.path); Module { - cached_path: ast::path_to_string(new_path.clone()), - cached_ident: ast::ident_to_string(new_path.clone()), + cached_path: ast::path_to_string(&new_path), + cached_ident: ast::ident_to_string(&new_path), path: new_path.clone(), - path_aggregate: self.path_aggregate.clone().push(new_path.clone()), - items: new_items.clone(), - ..self.clone() + path_aggregate: self.path_aggregate.clone().push(new_path), + items: new_items, + ..self } } } diff --git a/ctest/src/ast/static_variable.rs b/ctest/src/ast/static_variable.rs index a19ab2dabc6c1..42b31d51cd883 100644 --- a/ctest/src/ast/static_variable.rs +++ b/ctest/src/ast/static_variable.rs @@ -51,12 +51,12 @@ impl Static { } impl PathUtils for Static { - fn path_aggregate(self) -> PathAggregateStore { - self.path_aggregate.clone() + fn path_aggregate(&self) -> &PathAggregateStore { + &self.path_aggregate } - fn path(self) -> Path { - self.path.clone() + fn path(&self) -> &Path { + &self.path } fn mk( @@ -71,7 +71,7 @@ impl PathUtils for Static { cached_ident, path, path_aggregate: addressable_paths, - ..self.clone() + ..self } } } diff --git a/ctest/src/ast/structure.rs b/ctest/src/ast/structure.rs index 8e387c8b50d6a..f9ed404225ea1 100644 --- a/ctest/src/ast/structure.rs +++ b/ctest/src/ast/structure.rs @@ -39,12 +39,12 @@ impl Struct { } impl PathUtils for Struct { - fn path_aggregate(self) -> PathAggregateStore { - self.path_aggregate.clone() + fn path_aggregate(&self) -> &PathAggregateStore { + &self.path_aggregate } - fn path(self) -> Path { - self.path.clone() + fn path(&self) -> &Path { + &self.path } fn mk( @@ -59,7 +59,7 @@ impl PathUtils for Struct { cached_ident, path, path_aggregate: addressable_paths, - ..self.clone() + ..self } } } @@ -71,11 +71,11 @@ impl Default for Struct { segments: Punctuated::new(), }; Self { - public: bool::default(), - cached_path: String::new().into_boxed_str(), - cached_ident: String::new().into_boxed_str(), + public: Default::default(), + cached_path: Default::default(), + cached_ident: Default::default(), path: dpath.clone(), - path_aggregate: PathAggregateStore::new().push(dpath.clone()), + path_aggregate: PathAggregateStore::from_iter([dpath]), fields: vec![], } } diff --git a/ctest/src/ast/type_alias.rs b/ctest/src/ast/type_alias.rs index ce76b6ff7f414..c0ae9a3d62e70 100644 --- a/ctest/src/ast/type_alias.rs +++ b/ctest/src/ast/type_alias.rs @@ -36,12 +36,12 @@ impl Type { } impl PathUtils for Type { - fn path_aggregate(self) -> PathAggregateStore { - self.path_aggregate.clone() + fn path_aggregate(&self) -> &PathAggregateStore { + &self.path_aggregate } - fn path(self) -> Path { - self.path.clone() + fn path(&self) -> &Path { + &self.path } fn mk( @@ -56,7 +56,7 @@ impl PathUtils for Type { cached_ident, path, path_aggregate: addressable_paths, - ..self.clone() + ..self } } } diff --git a/ctest/src/ast/union.rs b/ctest/src/ast/union.rs index e6e2936e820c5..3ebe92bde8af4 100644 --- a/ctest/src/ast/union.rs +++ b/ctest/src/ast/union.rs @@ -38,12 +38,12 @@ impl Union { } impl PathUtils for Union { - fn path_aggregate(self) -> PathAggregateStore { - self.path_aggregate.clone() + fn path_aggregate(&self) -> &PathAggregateStore { + &self.path_aggregate } - fn path(self) -> Path { - self.path.clone() + fn path(&self) -> &Path { + &self.path } fn mk( @@ -58,7 +58,7 @@ impl PathUtils for Union { cached_ident, path, path_aggregate: addressable_paths, - ..self.clone() + ..self } } } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index f030a8d73b025..299f34d85286b 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -150,11 +150,14 @@ impl FfiItems { content: Some((Brace::default(), mod_items.clone())), semi: None, }; + self.visit_item_mod(&file_mod); + let root_module = Module { items: self.clone(), ..Default::default() }; + *self = resolve_all(root_module).items; } @@ -183,6 +186,7 @@ impl FfiItems { .map(GenericItem::Static), ) .chain(self.modules.clone().into_iter().map(GenericItem::Module)); + Box::new(base) } @@ -218,6 +222,7 @@ impl FfiItems { /// possibly some more items. pub(super) fn custom_cmp(&self, other: &Self) -> Ordering { let (ours, theirs) = (self.modules.len(), other.modules.len()); + // [NOTE]: the goal here is to have modules with "larger" subtrees // appear first in the item container, so declaring them to be "smaller" // is required for slice::sort_unstable_by to put them first in the @@ -250,10 +255,11 @@ impl FfiItems { macro_rules! cmp { ($($var:tt),+ $(,)?) => { match it {$( - GenericItem::$var(it) => it.ident() == ident.to_string(), + GenericItem::$var(it) => ident == it.ident(), )+} }; } + cmp! { Type, Struct, @@ -272,26 +278,32 @@ impl FfiItems { /// to have the same path will not get deduplicated. A consequence of /// calling this method is that the items are also sorted by their paths. fn dedup(self) -> FfiItems { - macro_rules! key_fn { - () => { - |it| it.cached_path.clone() - }; - } macro_rules! dedup { ($($it:tt),+ $(,)?) => {{ + macro_rules! key_fn { + () => { + |it| it.cached_path.clone() + }; + } + let items = self.clone(); + $( let mut $it = self.$it; + $it.sort_unstable_by_key(key_fn!()); $it.dedup_by_key(key_fn!()); + let items = FfiItems { $it: $it, ..items }; )+ + items }}; } + let new_items = dedup!( aliases, structs, @@ -300,9 +312,11 @@ impl FfiItems { foreign_functions, foreign_statics, ); + let mut new_modules = self.modules; new_modules.sort_unstable_by(Module::custom_cmp); new_modules.dedup_by(|Module { path: lhs, .. }, Module { path: rhs, .. }| *lhs == *rhs); + FfiItems { modules: new_modules, ..new_items @@ -332,8 +346,10 @@ impl Default for FfiItems { impl FromIterator for FfiItems { fn from_iter>(iter: I) -> FfiItems { let mut partitioned_items: Vec<_> = iter.into_iter().collect(); + // [NOTE]: this is expected to sort all items by their kind, such that // we end up with the following ordering: + // // 1. Type aliases. // 2. Stucts. // 3. Unions. @@ -341,6 +357,7 @@ impl FromIterator for FfiItems { // 5. Foreign functions. // 6. Foreign statics. // 7. Modules. + // // This is then used to partition over the resulting collection and get // "buckets" of GenericItem that can be mapped to a concrete type // through partial mapping. @@ -395,6 +412,7 @@ impl FromIterator for FfiItems { | (GenericItem::Module(_), GenericItem::Fn(_)) | (GenericItem::Module(_), GenericItem::Static(_)) => Ordering::Greater, }); + macro_rules! get_partitions { ($([$field:ident, $ty:tt]),+ $(,)?) => {$( let start = @@ -427,6 +445,7 @@ impl FromIterator for FfiItems { }; )+}; } + get_partitions! { [aliases, Type], [structs, Struct], @@ -436,6 +455,7 @@ impl FromIterator for FfiItems { [foreign_statics, Static], [modules, Module], } + FfiItems { aliases, structs, @@ -570,6 +590,7 @@ fn resolve_all(root: Module) -> Module { subtree: root.clone(), whole_crate: root.clone(), }); + match new_root == root { true => ControlFlow::Break(new_root), false => ControlFlow::Continue(new_root), @@ -614,11 +635,14 @@ fn resolve_pass(src: PassPair) -> PassPair { subtree: m, whole_crate: whole_crate, }); + let mut new_children = children; new_children.push(pair.subtree.clone()); + (pair, new_children) }, ); + let root_with_resolved_children = Module { items: FfiItems { modules: resolved_children, @@ -626,10 +650,12 @@ fn resolve_pass(src: PassPair) -> PassPair { }, ..src.subtree }; + let resolved_uses = resolve_one(PassPair { subtree: root_with_resolved_children.clone(), whole_crate: whole_crate_with_resolved_children.clone(), }); + let PassPair { subtree: new_root_with_duplicates, whole_crate: new_whole_crate_with_resolved_children, @@ -640,6 +666,7 @@ fn resolve_pass(src: PassPair) -> PassPair { }, resolved_uses, ); + let new_root = Module { items: new_root_with_duplicates.items.dedup(), ..new_root_with_duplicates @@ -649,6 +676,7 @@ fn resolve_pass(src: PassPair) -> PassPair { |_| GenericItem::Module(new_root.clone()), new_whole_crate_with_resolved_children, ); + println!( "finished module pass on module {}", if &*new_root.cached_path == "" { @@ -657,6 +685,7 @@ fn resolve_pass(src: PassPair) -> PassPair { &*new_root.cached_path }, ); + PassPair { subtree: new_root, whole_crate: new_whole_crate, @@ -680,6 +709,7 @@ fn resolve_one(state: PassPair) -> Vec { &*state.subtree.cached_path }, ); + state .subtree .items @@ -719,59 +749,19 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! single_item { ($field:ident: $it:expr) => { return Resolution::Resolved(FfiItems { - $field: Vec::from([$it]), + $field: vec![$it], ..Default::default() }) }; } - // [NOTE]: this ensures any returned item has the visibility of the use - // declaration, and doesn't preserve its own visibility. - macro_rules! change_vis { - ($it:ident : $ty:tt) => { - $ty { - public: r#use.is_public, - ..$it - } - }; - } - let change_vis_in_mod = |m: Module| { - macro_rules! c { - ($($field:ident: $ty:tt),+ $(,)?) => {$( - let $field: Vec<_> = m.items.$field - .into_iter() - .map(|it| change_vis!(it: $ty)) - .collect(); - )+}; - } - c! { - aliases: Type, - constants: Const, - structs: Struct, - unions: Union, - foreign_functions: Fn, - foreign_statics: Static, - modules: Module, - } - Module { - items: FfiItems { - aliases, - constants, - structs, - unions, - foreign_functions, - foreign_statics, - modules, - ..m.items - }, - ..m - } - }; - match r#use.tree.clone() { + + match r#use.tree { RefinedUseTree::Name(UseName { ident }) => { println!( "entered name resolution case with {} in module {}", - ident, state.subtree.cached_path + ident, state.subtree.cached_path, ); + // [NOTE]: self may not appear as the name basecase of a well-formed // path. And yet it is still considered here. That is because of // path normalization. self may appear as part of a tail group in a @@ -780,10 +770,10 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { // normalize_path above. That happens at parse-time. That is why // this function must consider the case of self as a name basecase. // [NOTE]: crate does not appear as the tail end of a path in a use - // declaration under no circumstances, so that case is not - // considered. + // declaration (that is not a rename) under no circumstances, so + // that case is not considered here. if is_self(&ident) { - single_item!(modules: change_vis_in_mod(state.subtree)); + single_item!(modules: state.subtree); } else if is_super(&ident) { let parent_path = get_parent_path(&state.subtree); let Some(parent) = get_module(&parent_path, &state.whole_crate) else { @@ -799,18 +789,18 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { considered malformed input." ); }; - single_item!(modules: change_vis_in_mod(parent.clone())); + + single_item!(modules: parent.clone()); } + match state.subtree.items.search(&ident) { - Some(GenericItem::Type(t)) => single_item!(aliases: change_vis!(t: Type)), - Some(GenericItem::Struct(s)) => single_item!(structs: change_vis!(s: Struct)), - Some(GenericItem::Union(u)) => single_item!(unions: change_vis!(u: Union)), - Some(GenericItem::Const(c)) => single_item!(constants: change_vis!(c: Const)), - Some(GenericItem::Fn(f)) => single_item!(foreign_functions: change_vis!(f: Fn)), - Some(GenericItem::Static(s)) => { - single_item!(foreign_statics: change_vis!(s: Static)) - } - Some(GenericItem::Module(m)) => single_item!(modules: change_vis!(m: Module)), + Some(GenericItem::Type(t)) => single_item!(aliases: t), + Some(GenericItem::Struct(s)) => single_item!(structs: s), + Some(GenericItem::Union(u)) => single_item!(unions: u), + Some(GenericItem::Const(c)) => single_item!(constants: c), + Some(GenericItem::Fn(f)) => single_item!(foreign_functions: f), + Some(GenericItem::Static(s)) => single_item!(foreign_statics: s), + Some(GenericItem::Module(m)) => single_item!(modules: m), None => Resolution::Unresolved, } } @@ -823,7 +813,8 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { &*state.subtree.cached_path }, ); - Resolution::Resolved(change_vis_in_mod(state.subtree).items) + + Resolution::Resolved(state.subtree.items) } RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { println!( @@ -833,84 +824,68 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { as_token: syn::parse_quote! {as}, rename: rename.clone(), }, - state.subtree.cached_path + state.subtree.cached_path, ); + macro_rules! new_path { ($it:expr) => {{ + let new_ident = $it.path.segments.iter().rev().take(1).map(|_| PathSegment { + ident: rename.clone(), + arguments: PathArguments::None, + }); let new_segments = $it .path .segments .clone() .into_iter() - .skip($it.path.segments.len().saturating_sub(1)) - .map(|_| PathSegment { - ident: rename.clone(), - arguments: PathArguments::None, - }) + .chain(new_ident) .collect(); + Path { segments: new_segments, ..$it.path } }}; } + + // [NOTE]: it may seem odd that borrowck doesn't mention the fact + // the linear resource in the item at $it hasn't already been used + // by the time the linear PathUtils::rename trait method is used. + // Notice how new_path! does not make use of any linear resources in + // $it; The remaining fields filled in by $it in the syn::Path it + // yields have an unrestricted multiplicity (i.e. they're Copy.) macro_rules! rename_single_item { - ($field:ident , $ty:tt : $it:expr) => {{ + ($field:ident , $it:expr , $ty:tt) => {{ let new_path = new_path!($it); - let new_item = $it.rename(new_path); - single_item!($field: new_item); + + single_item!($field: $it.rename(new_path)); }}; } - let renamer = |m: Module| { - let new_path = new_path!(m); - // [NOTE]: it may seem better to use the segment length of the - // old path of the module, much like we do in function - // RenameUtils::rename, but that is not necessary here. We know - // the segment length will be the same before and after the - // rename because use declarations can only rename the module's - // identifier. The module identifier only accounts for one - // segment of the item's path. - let new_path_len = new_path.segments.len(); - m.clone().rename_with(|p| { - let segments_we_keep = p.segments.clone().into_iter().skip(new_path_len); - let new_segments = new_path - .segments - .clone() - .into_iter() - .chain(segments_we_keep) - .collect(); - Path { - leading_colon: p.leading_colon, - segments: new_segments, - } - }) - }; + if is_self(&ident) { - single_item!(modules: change_vis_in_mod(renamer(state.subtree))); + rename_single_item!(modules, state.subtree, Module); } else if is_super(&ident) { let parent_path = get_parent_path(&state.subtree); - let Some(parent) = get_module(&parent_path, &state.whole_crate) else { + let Some(parent) = get_module(&parent_path, &state.whole_crate).cloned() else { unreachable!( "See the note on super in the name basecase. The same \ applies here." ); }; - single_item!(modules: change_vis_in_mod(renamer(parent.clone()))); + + rename_single_item!(modules, parent, Module); } else if is_crate(&ident) { - single_item!(modules: change_vis_in_mod(renamer(state.whole_crate))); + rename_single_item!(modules, state.whole_crate, Module); } + match state.subtree.items.search(&ident) { - Some(GenericItem::Module(m)) => { - single_item!(modules: change_vis_in_mod(renamer(m))) - } - Some(GenericItem::Type(t)) => rename_single_item!(aliases, Type: t), - Some(GenericItem::Struct(s)) => rename_single_item!(structs, Struct: s), - Some(GenericItem::Union(u)) => rename_single_item!(unions, Union: u), - Some(GenericItem::Const(c)) => rename_single_item!(constants, Const: c), - Some(GenericItem::Fn(f)) => rename_single_item!(foreign_functions, Fn: f), - Some(GenericItem::Static(s)) => { - rename_single_item!(foreign_statics, Static: s) - } + Some(GenericItem::Type(t)) => rename_single_item!(aliases, t, Type), + Some(GenericItem::Struct(s)) => rename_single_item!(structs, s, Struct), + Some(GenericItem::Union(u)) => rename_single_item!(unions, u, Union), + Some(GenericItem::Const(c)) => rename_single_item!(constants, c, Const), + Some(GenericItem::Fn(f)) => rename_single_item!(foreign_functions, f, Fn), + Some(GenericItem::Static(s)) => rename_single_item!(foreign_statics, s, Static), + Some(GenericItem::Module(m)) => rename_single_item!(modules, m, Module), None => Resolution::Unresolved, } } @@ -918,8 +893,9 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { println!( "entered path resolution case with {} in module {}", - ident, state.subtree.cached_path + ident, state.subtree.cached_path, ); + let new_state = if is_self(&ident) { PassPair { subtree: state.subtree, @@ -933,6 +909,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { applies here." ); }; + PassPair { subtree: parent.clone(), whole_crate: state.whole_crate, @@ -951,6 +928,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { Some(_) | None => return Resolution::Unresolved, } }; + resolve_use( RefinedUse { tree: *tree, @@ -968,6 +946,7 @@ fn get_module<'m>(target: &Path, root: &'m Module) -> Option<&'m Module> { if root.path == *target { return root.into(); } + root.items .modules .iter() @@ -994,6 +973,7 @@ fn get_parent_path(root: &Module) -> Path { // subtraction. It is best if this function is protected against what would // currently be misuse. let segments_before_ident = root.path.segments.len().saturating_sub(1); + Path { leading_colon: None, segments: root @@ -1024,11 +1004,14 @@ fn is_super(ident: &Ident) -> bool { /// Attempts to find some item in module tree `root` with path `target`, then /// runs the passed closure on the item, and replaces it with the result. /// -/// It is key that the generic wrapper around the item returned is the same as -/// the generic item that was passed in. +/// # Panics +/// +/// The function will panic if the specific injection of the returned +/// `GenericItem` does not match that of the input `GenericItem` to closure +/// `action`. fn update_tree( target: &Path, - action: impl ops::Fn(GenericItem) -> GenericItem + Clone, + action: impl ops::Fn(GenericItem) -> GenericItem, root: Module, ) -> Module { macro_rules! ignored { @@ -1039,24 +1022,24 @@ fn update_tree( ); }; } + if *target == root.path { let GenericItem::Module(new_root) = action(GenericItem::Module(root)) else { ignored!(); }; + return new_root; } + macro_rules! exe { - ($($field:tt: $ty:tt),+ $(,)?) => { - $( + ($($field:tt: $ty:tt),+ $(,)?) => {$( let $field: Vec<_> = root .items .$field - .clone() .into_iter() .map(|it| { if it.path == *target { - let new = action(GenericItem::$ty(it)); - let GenericItem::$ty(it) = new else { + let GenericItem::$ty(it) = action(GenericItem::$ty(it)) else { ignored!(); }; it @@ -1065,9 +1048,9 @@ fn update_tree( } }) .collect(); - )+ - }; + )+}; } + exe! { aliases: Type, structs: Struct, @@ -1077,13 +1060,12 @@ fn update_tree( foreign_statics: Static, modules: Module, } - // [NOTE]: this additionally processes modules if no matching item has been - // found in the current module, by recursing with the same function on each - // of the child modules. - let modules: Vec<_> = modules + + let modules = modules .into_iter() - .map(|m| update_tree(target, action.clone(), m)) + .map(|m| update_tree(target, &action, m)) .collect(); + Module { items: FfiItems { aliases, @@ -1093,7 +1075,7 @@ fn update_tree( foreign_functions, foreign_statics, modules, - ..root.items.clone() + ..root.items }, ..root } @@ -1282,22 +1264,29 @@ fn sync_driver(items: &FfiItems, tree: Module) -> Module { /// Provided a reference path aggregate corresponding with that of an item, this /// function traverses module `m` in search of all occurrences of such an item -/// and "updates" such ocurrences to integrate the reference path aggregate -/// store. +/// and "updates" such ocurrences to integrate the paths from `reference`'s path +/// aggregate store that are not part of any one of those item occurrences. +/// +/// Note this affects only one concrete item's occurrences throughout the crate. fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { let akin = |agg: &PathAggregateStore| agg.akin(reference); + let new_m = if akin(&m.path_aggregate) { // [NOTE]: this we diff instead of merging because merging will get us a // set of path aggregates that contain both the ones already in m, as // well as the ones that were part of reference but not part of m. // Diffing gets us only the paths in reference that are not part of m's - // path aggregate. - let new_agg = m.path_aggregate.diff(reference); - new_agg.into_iter().fold(m, Module::add_path) + // path aggregate. We don't just merge the path aggregates and create + // a new m with the result because adding a path aggregate to a module + // has an effect on the subtree of items rooted at the module in + // question (see the Module::add_path trait method override.) + let diff = m.path_aggregate.diff(reference); + diff.into_iter().fold(m, Module::add_path) } else { m }; - let new_items: FfiItems = new_m + + let new_items = new_m .items .items() .map(|it| { @@ -1332,6 +1321,7 @@ fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { } }) .collect(); + Module { items: new_items, ..new_m @@ -1406,7 +1396,7 @@ fn visit_foreign_item_fn(table: &mut FfiItems, i: &syn::ForeignItemFn, abi: &Abi let public = is_visible(&i.vis); let abi = abi.clone(); let path = append_path(table.current_module.clone(), i.sig.ident.clone()); - let cached_path = ast::path_to_string(path.clone()); + let cached_path = ast::path_to_string(&path); let cached_ident = i.sig.ident.to_string().into_boxed_str(); let parameters = i .sig @@ -1456,7 +1446,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a let public = is_visible(&i.vis); let abi = abi.clone(); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = ast::path_to_string(path.clone()); + let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); let link_name = extract_single_link_name(&i.attrs); @@ -1476,7 +1466,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = ast::path_to_string(path.clone()); + let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); @@ -1493,7 +1483,7 @@ fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = ast::path_to_string(path.clone()); + let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let fields = match &i.fields { syn::Fields::Named(fields) => collect_fields(&fields.named), @@ -1514,7 +1504,7 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = ast::path_to_string(path.clone()); + let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let fields = collect_fields(&i.fields.named); @@ -1531,7 +1521,7 @@ fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { let public = is_visible(&i.vis); let path = append_path(table.current_module.clone(), i.ident.clone()); - let cached_path = ast::path_to_string(path.clone()); + let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); @@ -1574,7 +1564,7 @@ impl<'ast> Visit<'ast> for FfiItems { ident, content: Some((_, mod_items)), .. - } = i.clone() + } = i else { unreachable!( "This runs post cargo-expand, which inlines all \ @@ -1592,9 +1582,8 @@ impl<'ast> Visit<'ast> for FfiItems { } }; - let uses: Vec<_> = mod_items + let uses = mod_items .iter() - .cloned() .filter_map(|it| match it { Item::Type(t) => { visit_item_type(&mut running_items, &t); @@ -1616,7 +1605,7 @@ impl<'ast> Visit<'ast> for FfiItems { visit_item_foreign_mod(&mut running_items, &m); None } - Item::Use(ItemUse { vis, tree, .. }) => normalize_path(tree) + Item::Use(ItemUse { vis, tree, .. }) => normalize_path(tree.clone()) .into_iter() .map(move |tree| RefinedUse { is_public: matches!(vis, Visibility::Public(_)), @@ -1629,19 +1618,19 @@ impl<'ast> Visit<'ast> for FfiItems { .collect(); visit::visit_item_mod(&mut running_items, i); - running_items.uses = uses.clone(); + running_items.uses = uses; if is_crate_root { - *self = running_items.clone(); + *self = running_items; } else { let path = running_items.current_module.clone(); self.modules.push(Module { public: matches!(vis, Visibility::Public(_)), - cached_path: ast::path_to_string(running_items.current_module.clone()), + cached_path: ast::path_to_string(&running_items.current_module), cached_ident: ident.to_string().into_boxed_str(), path: path.clone(), - path_aggregate: PathAggregateStore::new().push(path.clone()), - items: running_items.clone(), + path_aggregate: PathAggregateStore::from_iter([path]), + items: running_items, }); } } @@ -1650,18 +1639,18 @@ impl<'ast> Visit<'ast> for FfiItems { #[test] fn tmp() { /* - * time should exist with the following paths: - * [time, test1::time, test1::test2::time] - * Foo should exist with the following paths: + * time should end up with the following paths: + * [time, test1::time, test1::test2::time, test2::time] + * Foo should end up with the following paths: * [ * Foo, - * test2::test3::Foo, - * test2::Foo, - * test3::Foo, * test1::Foo, * test1::test3::Foo, * test1::test2::Foo, * test1::test2::test3::Foo, + * test2::Foo, + * test2::test3::Foo, + * test3::Foo, * ] */ let source = r#" From 52ec4b1324a2b54cdc5db7d4ad9e9f39dca00068 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:07:24 +0100 Subject: [PATCH 094/100] chore: wip --- ctest/src/ast/mod.rs | 86 ++++++------ ctest/src/ast/module.rs | 124 +++++++++-------- ctest/src/ffi_items.rs | 290 +++++++++++++++++++--------------------- 3 files changed, 244 insertions(+), 256 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index fa498a94d4d65..5c7cafdd223c2 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -32,12 +32,10 @@ use crate::{ MapInput, }; -/// Container for all paths by which a given item may be addressed throughout -/// the crate. +/// Container for all paths by which a given item may be addressed throughout the crate. /// -/// This is used to store all paths by which an item may be referred to, after -/// it has potentially been imported to modules other than the one it was -/// declared in. +/// This is used to store all paths by which an item may be referred to, after it has potentially +/// been imported to modules other than the one it was declared in. #[derive(Clone, Debug, PartialEq)] pub(crate) struct PathAggregateStore { value: Vec, @@ -55,24 +53,23 @@ impl PathAggregateStore { Self { value } } - /// Checks if any two path aggregates are known to correspond with those of - /// the same item. + /// Checks if any two path aggregates are known to correspond with those of the same item. /// - /// In other words, it checks if the item owning the receiver path aggregate - /// and the item owning the `other` path aggregate are different occurrences - /// of the same item across the crate's module tree. + /// In other words, it checks if the item owning the receiver path aggregate and the item owning + /// the `other` path aggregate are different occurrences of the same item across the crate's + /// module tree. pub(crate) fn akin(&self, other: &Self) -> bool { let ours = &self.value; let theirs = &other.value; ours.iter().any(|p1| theirs.iter().any(|p2| p1 == p2)) } - /// Diffs any two given path aggregates and returns the set of paths that - /// were present in `other` but missing in the receiver. + /// Diffs any two given path aggregates and returns the set of paths that were present in + /// `other` but missing in the receiver. /// - /// Note that if the receiver contains more paths than `other`, the returned - /// collection will be empty. In other words, the operation is not - /// reflexive (`self.diff(other) != other.diff(self)`.) + /// Note that if the receiver contains more paths than `other`, the returned collection will be + /// empty. In other words, the operation is not reflexive + /// (`self.diff(other) != other.diff(self)`.) pub(crate) fn diff(&self, other: &Self) -> Vec { let ours = &self.value; let theirs = &other.value; @@ -83,11 +80,10 @@ impl PathAggregateStore { .collect() } - /// Yields a new path aggregate that integrates the paths of the receiver - /// with those of `other`. + /// Yields a new path aggregate that integrates the paths of the receiver with those of `other`. /// - /// Unlike diffing, the resulting path aggregate store unifies the paths - /// from both parties; The receiver and `other`. + /// Unlike diffing, the resulting path aggregate store unifies the paths from both parties; The + /// receiver and `other`. #[expect(unused)] pub(crate) fn merge(self, other: &Self) -> Self { self.diff(other) @@ -143,12 +139,11 @@ impl FromIterator for PathAggregateStore { } } -/// Utility interface for keeping a single renaming function for all parsed -/// items. +/// Utility interface for keeping a single renaming function for all parsed items. /// -/// They all share the same set of fields relative to their identifier and path, -/// all of which need updating whenever the path is renamed. We ensure that the -/// only place where this happens is in this function for convenience's sake. +/// They all share the same set of fields relative to their identifier and path, all of which need +/// updating whenever the path is renamed. We ensure that the only place where this happens is in +/// this function for convenience's sake. pub(crate) trait PathUtils: Clone { fn path(&self) -> &Path; fn path_aggregate(&self) -> &PathAggregateStore; @@ -160,12 +155,10 @@ pub(crate) trait PathUtils: Clone { addressable_paths: PathAggregateStore, ) -> Self; - /// Provided a path, it ensures the path aggregate of a given item - /// integrates the new path. + /// Provided a path, it ensures the path aggregate of a given item integrates the new path. /// - /// Unlike methods `rename` and `rename_with`, this does not change the path - /// of the item itself, but rather produces an updated receiver with only - /// the path aggregate store expanded. + /// Unlike methods `rename` and `rename_with`, this does not change the path of the item itself, + /// but rather produces an updated receiver with only the path aggregate store expanded. fn add_path(self, path: Path) -> Self { let old_path = self.path().clone(); let old_aggregate = self.path_aggregate().clone(); @@ -177,21 +170,18 @@ pub(crate) trait PathUtils: Clone { ) } - /// Provided some parsed type, this method will run the given closure on - /// each variation of a term of that type (`self`.) + /// Provided some parsed type, this method will run the given closure on each variation of a + /// term of that type (`self`.) /// - /// The variations of a given term are given by their addressable paths. - /// This is an append-only aggregate all implementors of this interface have - /// as one of their projections. The variations are themselves the possible - /// paths by which an item may be referred to as a consequence of having - /// been imported over to other modules of the crate. + /// The variations of a given term are given by their addressable paths. This is an append-only + /// aggregate all implementors of this interface have as one of their projections. The + /// variations are themselves the possible paths by which an item may be referred to as a + /// consequence of having been imported over to other modules of the crate. /// - /// This interface gets access to it through the - /// [`RenameUtils::addressable_paths`] projection. + /// This interface gets access to it through the [`RenameUtils::addressable_paths`] projection. /// - /// This is a utility to avoid having to go through the variations, as well - /// as updating the identifier and paths caches whenever some operation - /// needs to be performed on all variations. + /// This is a utility to avoid having to go through the variations, as well as updating the + /// identifier and paths caches whenever some operation needs to be performed on all variations. fn with_variations(&self, f: impl ops::Fn(&Self) -> T) -> Vec { self.path_aggregate() .into_iter() @@ -200,13 +190,15 @@ pub(crate) trait PathUtils: Clone { .collect() } - fn rename_with(self, f: impl ops::Fn(&Path) -> Path) -> Self { + fn rename_with(self, f: impl ops::Fn(&Path) -> Path + Clone) -> Self { let path = self.path().clone(); + self.rename(f(&path)) } fn rename(self, new_path: Path) -> Self { let old_aggregate = self.path_aggregate().clone(); + self.mk( path_to_string(&new_path), ident_to_string(&new_path), @@ -216,8 +208,7 @@ pub(crate) trait PathUtils: Clone { } } -/// Transforms an item's absolute path to use `_` as path separator instead of -/// `::`. +/// 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(), @@ -232,8 +223,8 @@ pub(crate) fn escape_item_path<'a>(item: impl Into>) -> String { path.replace("::", "_") } -/// Returns a stringified `syn::Path` that is meant to match one-to-one the path -/// to the item from the crate root. +/// Returns a stringified `syn::Path` that is meant to match one-to-one the path to the item from +/// the crate root. pub(crate) fn path_to_string(path: &Path) -> BoxStr { path.into_token_stream() .to_string() @@ -241,8 +232,7 @@ pub(crate) fn path_to_string(path: &Path) -> BoxStr { .into_boxed_str() } -/// Returns a stringified representation of the last segment in the passed -/// identifier. +/// Returns a stringified representation of the last segment in the passed identifier. pub(crate) fn ident_to_string(path: &Path) -> BoxStr { path.segments .last() diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 481f3d3520315..9965cb6d2207e 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,5 +1,8 @@ use std::cmp::Ordering; -use std::ops; +use std::ops::{ + self, + Not, +}; use syn::Path; use syn::punctuated::Punctuated; @@ -23,8 +26,8 @@ use crate::{ ast, }; -/// Represents a Rust module. `ctest` only considers items for which there is a -/// corresponding type in this crate. +/// 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, @@ -38,10 +41,9 @@ pub struct Module { impl Module { /// Returns the full path to the module item. /// - /// If inside a nested module, this will return a top-level-relative path, - /// but not a crate-relative path. For some item `foo` in module - /// `crate::bar`, the returned string will be `bar::foo`, and not - /// `crate::bar::foo`. + /// 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.cached_path } @@ -53,8 +55,8 @@ impl Module { /// Comparison function for use when sorting modules in [`FfiItems::dedup`]. /// - /// This is not implemented as part of [`Ord`] because the implementation is - /// inconsistent with the [`PartialEq`] implementation. + /// This is not implemented as part of [`Ord`] because the implementation is inconsistent with + /// the [`PartialEq`] implementation. pub(crate) fn custom_cmp(&self, other: &Self) -> Ordering { match self.cached_path.cmp(&other.cached_path) { Ordering::Equal => self.items.custom_cmp(&other.items), @@ -101,7 +103,12 @@ impl PathUtils for Module { .into_iter() .skip(original.segments.len()); let new_path = Path { - segments: new.segments.clone().into_iter().chain(kept_segments).collect(), + segments: new + .segments + .clone() + .into_iter() + .chain(kept_segments) + .collect(), ..new.clone() }; @@ -118,7 +125,12 @@ impl PathUtils for Module { .into_iter() .skip(original.segments.len()); let new_path = Path { - segments: new.segments.clone().into_iter().chain(kept_segments).collect(), + segments: new + .segments + .clone() + .into_iter() + .chain(kept_segments) + .collect(), ..new.clone() }; @@ -174,6 +186,7 @@ impl PathUtils for Module { // appended to the new path of that (potentially grand-) ancestor. fn rename(self, new_path: Path) -> Self { let old_path_len = self.path.segments.len(); + self.rename_with(|p| { p.segments .iter() @@ -186,49 +199,45 @@ impl PathUtils for Module { }) } - fn rename_with(self, f: impl ops::Fn(&Path) -> Path) -> Self { - macro_rules! map_non_modules { - ($($field:ident: $ty:tt),+ $(,)?) => {$( - let $field = self.items - .$field - .into_iter() - .map(|it| { - let path = f(&it.path); - it.rename(path) - }) - .collect::>(); - )+}; - } - map_non_modules! { - aliases: Type, - structs: Struct, - unions: Union, - constants: Const, - foreign_functions: Fn, - foreign_statics: Static, - } - let modules = self + fn rename_with(self, f: impl ops::Fn(&Path) -> Path + Clone) -> Self { + let new_items = self .items - .modules - .into_iter() - .map(|m| m.rename_with(&f)) + .items() + .filter(|it| matches!(it, GenericItem::Module(_)).not()) + .map(|it| { + macro_rules! exe { + ($($ty:tt),+ $(,)?) => { + match it { + $( + GenericItem::$ty(it) => { + let new_path = f(&it.path); + + GenericItem::$ty(it.rename(new_path)) + } + )+ + GenericItem::Module(it) => GenericItem::Module(it.rename_with(f.clone())), + } + }; + } + + exe! { + Type, + Struct, + Union, + Const, + Fn, + Static, + } + }) .collect(); - let new_items = FfiItems { - aliases, - structs, - unions, - constants, - foreign_functions, - foreign_statics, - modules, - ..self.items - }; + let new_path = f(&self.path); + Module { cached_path: ast::path_to_string(&new_path), cached_ident: ast::ident_to_string(&new_path), path: new_path.clone(), - path_aggregate: self.path_aggregate.clone().push(new_path), + path_aggregate: self.path_aggregate.push(new_path), items: new_items, ..self } @@ -241,13 +250,14 @@ impl Default for Module { leading_colon: None, segments: Punctuated::new(), }; + Self { - public: bool::default(), - cached_path: String::new().into_boxed_str(), - cached_ident: String::new().into_boxed_str(), + public: Default::default(), + cached_path: Default::default(), + cached_ident: Default::default(), path: dpath.clone(), - path_aggregate: PathAggregateStore::new().push(dpath.clone()), - items: FfiItems::default(), + path_aggregate: PathAggregateStore::from_iter([dpath]), + items: Default::default(), } } } @@ -258,9 +268,17 @@ impl Default for Module { // would likely be unintuitive to have its PartialEq not take into account. impl PartialEq for Module { fn eq(&self, other: &Self) -> bool { + if self.path == other.path { + assert_eq!( + self.path_aggregate, other.path_aggregate, + "path aggregates of module {} and {} are not synchronized", + self.cached_path, other.cached_path, + ); + } + self.public == other.public - && self.cached_path == other.cached_path && self.path == other.path + && self.path_aggregate == other.path_aggregate && self.items.custom_eq(&other.items) } } diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 299f34d85286b..d0cf3e263e636 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -57,12 +57,11 @@ use crate::{ ast, }; -/// 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. Modules are collected as recursive `FfiItems`, and currently used -/// to narrow down tested items to those in a specific module. +/// 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, @@ -74,9 +73,8 @@ pub(crate) struct FfiItems { pub(crate) uses: 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. + /// 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. // [FIXME]: get rid of this and refactor parsing into using a helper that // stores this ancilliary state. pub(crate) current_module: syn::Path, @@ -132,9 +130,8 @@ impl FfiItems { /// Entry point to parse a [`syn::File`]. /// - /// This will also resolve `use`-trees such that the resulting `FfiItems` - /// instance is always left with no reexports that are not sourced from - /// third-party crates. + /// This will also resolve `use`-trees such that the resulting `FfiItems` instance is always + /// left with no reexports that are not sourced from third-party crates. pub(crate) fn visit_file(&mut self, file: &syn::File) { let File { attrs, @@ -190,15 +187,11 @@ impl FfiItems { Box::new(base) } - /// Performs a comparison for equality between FfiItems without taking field - /// current_module into consideration. + /// Performs a comparison for equality between `FfiItems`, without taking fields + /// `current_module` nor `uses` into consideration. /// - /// This is not an implementation of PartialEq for FfiItems because it would - /// be unintuitive to have it ignore one field. This is only ever needed - /// during use declaration resolution. - /// - /// The visibility restriction allows the Module type in ast/module.rs to - /// have access to it. + /// This is not an implementation of `PartialEq` for `FfiItems` because, at the time of writing, + /// the logic for use declaration resolution is not yet finished. pub(super) fn custom_eq(&self, other: &Self) -> bool { self.aliases == other.aliases && self.structs == other.structs @@ -206,20 +199,16 @@ impl FfiItems { && self.constants == other.constants && self.foreign_functions == other.foreign_functions && self.foreign_statics == other.foreign_statics - && self.uses == other.uses && self.modules == other.modules } - /// Comparison function for use when sorting and deduplicating modules in - /// [`FfiItems::dedup`]. + /// Comparison function for use when sorting and deduplicating modules in [`FfiItems::dedup`]. /// - /// This is used in [`Module::custom_cmp`]. It trusts that the modules to - /// compare have the same overall shape, and one will be a "natural" - /// extension of the other. + /// This is used in [`Module::custom_cmp`]. It trusts that the modules to compare have the same + /// overall shape, and one will be a "natural" extension of the other. /// - /// We consider "extension" here to mean that one module subtree is based on - /// the other, and thus only contains both the items from the other and - /// possibly some more items. + /// We consider "extension" here to mean that one module subtree is based on the other, and thus + /// only contains both the items from the other and possibly some more items. pub(super) fn custom_cmp(&self, other: &Self) -> Ordering { let (ours, theirs) = (self.modules.len(), other.modules.len()); @@ -245,11 +234,9 @@ impl FfiItems { } } - /// Searches for an item in the container, and returns an owned instance of - /// that item. + /// Searches for an item in the container, and returns an owned instance of that item. /// - /// The generic item may be pattern-matched to find out specifically which - /// item was found. + /// The generic item may be pattern-matched to find out specifically which item was found. fn search(&self, ident: &Ident) -> Option { self.items().find(|it| { macro_rules! cmp { @@ -274,9 +261,9 @@ impl FfiItems { /// Deduplicates the items in an item container by their path. /// - /// Items of differing kind, like functions and constants, that may happen - /// to have the same path will not get deduplicated. A consequence of - /// calling this method is that the items are also sorted by their paths. + /// Items of differing kind, like functions and constants, that may happen to have the same path + /// will not get deduplicated. A consequence of calling this method is that the items are also + /// sorted by their paths. fn dedup(self) -> FfiItems { macro_rules! dedup { ($($it:tt),+ $(,)?) => {{ @@ -359,9 +346,9 @@ impl FromIterator for FfiItems { // 7. Modules. // // This is then used to partition over the resulting collection and get - // "buckets" of GenericItem that can be mapped to a concrete type + // "buckets" of GenericItems that can be mapped to a concrete type // through partial mapping. - partitioned_items.sort_by(|it1, it2| match (it1.clone(), it2.clone()) { + partitioned_items.sort_by(|it1, it2| match (it1, it2) { (GenericItem::Type(_), GenericItem::Type(_)) | (GenericItem::Struct(_), GenericItem::Struct(_)) | (GenericItem::Union(_), GenericItem::Union(_)) @@ -414,7 +401,7 @@ impl FromIterator for FfiItems { }); macro_rules! get_partitions { - ($([$field:ident, $ty:tt]),+ $(,)?) => {$( + ($($field:ident: $ty:tt),+ $(,)?) => {$( let start = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_)).not()); let end = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_))); @@ -447,13 +434,13 @@ impl FromIterator for FfiItems { } get_partitions! { - [aliases, Type], - [structs, Struct], - [unions, Union], - [constants, Const], - [foreign_functions, Fn], - [foreign_statics, Static], - [modules, Module], + aliases: Type, + structs: Struct, + unions: Union, + constants: Const, + foreign_functions: Fn, + foreign_statics: Static, + modules: Module, } FfiItems { @@ -470,7 +457,7 @@ impl FromIterator for FfiItems { } /// Sum over any item held within an `FfiItems` term. -#[derive(Clone)] +#[derive(Debug, Clone)] pub(crate) enum GenericItem { Type(Type), Struct(Struct), @@ -486,9 +473,8 @@ type ItemIterator = Box>; /// Refined type for [`syn::UsePath`] that does not consider group paths. /// -/// This is a lot like [`RefinedUse`] in that it only exists as replacement for -/// [`syn::UsePath`] but uses [`RefinedUseTree`] instead of [`syn::UseTree`]. -/// See [`RefinedUseTree`] for details. +/// This is a lot like [`RefinedUse`] in that it only exists as replacement for [`syn::UsePath`] but +/// uses [`RefinedUseTree`] instead of [`syn::UseTree`]. See [`RefinedUseTree`] for details. #[derive(Clone, Debug, PartialEq)] pub(crate) struct RefinedUsePath { ident: Ident, @@ -506,12 +492,12 @@ impl Default for RefinedUsePath { /// Refined type for [`syn::UseTree`] that does not consider group paths. /// -/// This is the byproduct of normalizing a `use` declaration's path through -/// [`normalize_path`]. That function ensures some such path containing a group -/// as its tail end is converted into a set of paths without groups. +/// This is the byproduct of normalizing a `use` declaration's path through [`normalize_path`]. That +/// function ensures some such path containing a group as its tail end is converted into a set of +/// paths without groups. /// -/// This enumeration is thus equivalent to `syn::UseTree` but does not consider -/// the case of `syn::UseTree::Group`. +/// This enumeration is thus equivalent to `syn::UseTree` but does not consider the case of +/// [`syn::UseTree::Group`]. #[derive(Clone, Debug, PartialEq, Default)] pub(crate) enum RefinedUseTree { Path(RefinedUsePath), @@ -523,17 +509,17 @@ pub(crate) enum RefinedUseTree { /// Refined type for `use` declarations that do not consider group paths. /// -/// This exists only for the purpose of refining [`syn::UseTree`], which is used -/// within [`syn::ItemUse`]. See type [`RefinedUseTree`] for the actual -/// refinement and details on the purpose of this type. +/// This exists only for the purpose of refining [`syn::UseTree`], which is used within +/// [`syn::ItemUse`]. See type [`RefinedUseTree`] for the actual refinement and details on the +/// purpose of this type. #[derive(Clone, Debug, PartialEq, Default)] pub(crate) struct RefinedUse { is_public: bool, tree: RefinedUseTree, } -/// Gets rid of group reexports in a `use` declaration by flattenning them into -/// a set of individual reexports. +/// Gets rid of group reexports in a `use` declaration by flattenning them into a set of individual +/// reexports. fn normalize_path(path: syn::UseTree) -> Vec { match path { UseTree::Name(n) => vec![RefinedUseTree::Name(n)], @@ -559,25 +545,23 @@ fn normalize_path(path: syn::UseTree) -> Vec { /// Stateful pair carried through resolution passes. /// -/// Each `use` declaration requires access to either one of (1) the module where -/// it was sourced from, or (2) the crate root's subtree, or (3) any one -/// ascendant or descendant module of the module where it was sourced from. +/// Each `use` declaration requires access to either one of (1) the module where it was sourced +/// from, or (2) the crate root's subtree, or (3) any one ascendant or descendant module of the +/// module where it was sourced from. /// -/// Points (2) and (3) require having access to the entire crate's module tree. -/// Some ancestor item is looked up by the parent path to the item being -/// searched with respect to. Point (1) is satisfied by the `subtree` field. +/// Points (2) and (3) require having access to the entire crate's module tree. Some ancestor item +/// is looked up by the parent path to the item being searched with respect to. Point (1) is +/// satisfied by the `subtree` field. #[derive(Debug, Clone)] struct PassPair { subtree: Module, whole_crate: Module, } -/// This is the resolution driver. It is the entrypoint for whole crate -/// resolution. +/// This is the resolution driver. It is the entrypoint for whole crate resolution. /// -/// It performs passes on a given module. It diffs the results of a whole-crate -/// pass with the pre-pass state. Equality in that comparison determines -/// termination. +/// It performs passes on a given module. It diffs the results of a whole-crate pass with the +/// pre-pass state. Equality in that comparison determines termination. fn resolve_all(root: Module) -> Module { let (ControlFlow::Break(m) | ControlFlow::Continue(m)) = iter::repeat(()).try_fold(root, |root, _| { @@ -601,26 +585,24 @@ fn resolve_all(root: Module) -> Module { /// Represents a resolved or unresolved `use` declaration. /// -/// The state of resolution may change across resolution passes. It is used in -/// [`merge_module`] to merge whichever `use` declarations resolved to a set of -/// items back into the module they were sourced from. +/// The state of resolution may change across resolution passes. It is used in [`merge_module`] to +/// merge whichever `use` declarations resolved to a set of items back into the module they were +/// sourced from. #[derive(Debug, Clone)] enum Resolution { Resolved(FfiItems), Unresolved, } -/// Performs one pass through the provided pair of a subtree and the whole-crate -/// subtree. +/// Performs one pass through the provided pair of a subtree and the whole-crate subtree. /// -/// The input pair's fields correspond with the subtree in which to perform the -/// resolution pass on, and the tree at the crate root. The latter is required -/// to resolve absolute imports that refer to some item in an ancestor module -/// that is more than one level up the tree than the subtree being resolved. +/// The input pair's fields correspond with the subtree in which to perform the resolution pass on, +/// and the tree at the crate root. The latter is required to resolve absolute imports that refer to +/// some item in an ancestor module that is more than one level up the tree than the subtree being +/// resolved. /// -/// The returned pair contains updated snapshots of the input pair. The subtree -/// may have had new items added to it. The whole-crate module tree is -/// guaranteed to have integrated these changes. +/// The returned pair contains updated snapshots of the input pair. The subtree may have had new +/// items added to it. The whole-crate module tree is guaranteed to have integrated these changes. fn resolve_pass(src: PassPair) -> PassPair { let ( PassPair { @@ -694,9 +676,9 @@ fn resolve_pass(src: PassPair) -> PassPair { /// Attempts to resolve the `use` declarations in a given module. /// -/// The input pair consists of the module from which to source the `use` -/// declarations, and of the whole-crate module tree. The latter is used to -/// resolve certain types of paths in `use` declarations. +/// The input pair consists of the module from which to source the `use` declarations, and of the +/// whole-crate module tree. The latter is used to resolve certain types of paths in `use` +/// declarations. /// /// See [`resolve_pass`] for details. fn resolve_one(state: PassPair) -> Vec { @@ -724,27 +706,22 @@ fn resolve_one(state: PassPair) -> Vec { /// /// This can go one of two ways. /// -/// 1. Either the `use` declaration matches a specific module in the stateful -/// pair, or -/// -/// 2. the `use` declaration is assumed to be unresolved in this resolution -/// pass. +/// 1. Either the `use` declaration matches a specific module in the stateful pair, or +/// 2. the `use` declaration is assumed to be unresolved in this resolution pass. /// -/// The stateful pair initially consists of the module in which the `use` -/// declaration appears at. If the `use` declaration refers to an item by a path -/// with more than one segment, this function recursively munches through it. +/// The stateful pair initially consists of the module in which the `use` declaration appears at. If +/// the `use` declaration refers to an item by a path with more than one segment, this function +/// recursively munches through it. /// -/// These recursive calls will only be made if the extracted path segment -/// matches against some item in the stateful subtree. Currently, the only types -/// of items that can match on a path that still has another unmunched segment -/// are modules. +/// These recursive calls will only be made if the extracted path segment matches against some item +/// in the stateful subtree. Currently, the only types of items that can match on a path that still +/// has another unmunched segment are modules. /// -/// Upon finding the module, the new stateful subtree of the pair becomes this -/// module. This is then used in the recursive call with the path tail. +/// Upon finding the module, the new stateful subtree of the pair becomes this module. This is then +/// used in the recursive call with the path tail. /// -/// The other element of the pair is the whole-crate module tree. That is used -/// to resolve ancestor paths (when `super` is found) and absolute paths (when -/// `crate` is found.) +/// The other element of the pair is the whole-crate module tree. That is used to resolve ancestor +/// paths (when `super` is found) and absolute paths (when `crate` is found.) fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { macro_rules! single_item { ($field:ident: $it:expr) => { @@ -897,13 +874,10 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { ); let new_state = if is_self(&ident) { - PassPair { - subtree: state.subtree, - whole_crate: state.whole_crate, - } + state } else if is_super(&ident) { let parent_path = get_parent_path(&state.subtree); - let Some(parent) = get_module(&parent_path, &state.whole_crate) else { + let Some(parent) = get_module(&parent_path, &state.whole_crate).cloned() else { unreachable!( "See the note on super in the name basecase. The same \ applies here." @@ -911,7 +885,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { }; PassPair { - subtree: parent.clone(), + subtree: parent, whole_crate: state.whole_crate, } } else if is_crate(&ident) { @@ -919,14 +893,13 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { subtree: state.whole_crate.clone(), whole_crate: state.whole_crate, } - } else { - match state.subtree.items.search(&ident) { - Some(GenericItem::Module(m)) => PassPair { - subtree: m, - whole_crate: state.whole_crate, - }, - Some(_) | None => return Resolution::Unresolved, + } else if let Some(GenericItem::Module(m)) = state.subtree.items.search(&ident) { + PassPair { + subtree: m, + whole_crate: state.whole_crate, } + } else { + return Resolution::Unresolved; }; resolve_use( @@ -940,8 +913,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { } } -/// Attempts to find a given module, provided its path (`target`,) within a -/// module subtree (`root`.) +/// Attempts to find a given module, provided its path (`target`,) within a module subtree (`root`.) fn get_module<'m>(target: &Path, root: &'m Module) -> Option<&'m Module> { if root.path == *target { return root.into(); @@ -955,9 +927,9 @@ fn get_module<'m>(target: &Path, root: &'m Module) -> Option<&'m Module> { /// Fetches the parent path to a given module. /// -/// If the module is a top-level item (i.e. it appears at the crate root,) the -/// returned path will be empty. This follows from the parsing strategy for -/// paths in `FfiItems`. The absolute paths are all `crate`-_unprepended_. +/// If the module is a top-level item (i.e. it appears at the crate root,) the returned path will be +/// empty. This follows from the parsing strategy for paths in `FfiItems`. The absolute paths are +/// all `crate`-_unprepended_. fn get_parent_path(root: &Module) -> Path { // [NOTE]: this performs a saturating_sub because there is one and only one // item in the crate's module tree that has an empty path; The crate root. @@ -1001,17 +973,16 @@ fn is_super(ident: &Ident) -> bool { ident.to_string() == "super" } -/// Attempts to find some item in module tree `root` with path `target`, then -/// runs the passed closure on the item, and replaces it with the result. +/// Attempts to find some item in module tree `root` with path `target`, then runs the passed +/// closure on the item, and replaces it with the result. /// /// # Panics /// -/// The function will panic if the specific injection of the returned -/// `GenericItem` does not match that of the input `GenericItem` to closure -/// `action`. +/// The function will panic if the specific injection of the returned `GenericItem` does not match +/// that of the input `GenericItem` to closure `action`. fn update_tree( target: &Path, - action: impl ops::Fn(GenericItem) -> GenericItem, + action: impl ops::Fn(GenericItem) -> GenericItem + Clone, root: Module, ) -> Module { macro_rules! ignored { @@ -1042,6 +1013,7 @@ fn update_tree( let GenericItem::$ty(it) = action(GenericItem::$ty(it)) else { ignored!(); }; + it } else { it @@ -1063,7 +1035,7 @@ fn update_tree( let modules = modules .into_iter() - .map(|m| update_tree(target, &action, m)) + .map(|m| update_tree(target, action.clone(), m)) .collect(); Module { @@ -1083,13 +1055,12 @@ fn update_tree( /// Integrates the resolution items of `src` into `dst`. /// -/// Each resolution item is either one of an `Unresolved` or a `Resolved` -/// variant. The former are essentially ignored. The latter pack an `FfiItems` -/// that is integrated into `dst`'s own items. That is the merging part. +/// Each resolution item is either one of an `Unresolved` or a `Resolved` variant. The former are +/// essentially ignored. The latter pack an `FfiItems` that is integrated into `dst`'s own items. +/// That is the merging part. /// -/// Note that, during merging, the items to be merged may end up duplicating -/// existing items on `dst`. It is call-site's responsibility to deduplicate -/// those through [`FfiItems::dedup`]. +/// Note that, during merging, the items to be merged may end up duplicating existing items on +/// `dst`. It is call-site's responsibility to deduplicate those through [`FfiItems::dedup`]. fn merge_module(state: PassPair, src: Vec) -> PassPair { // [NOTE]: we collect all items here because we want the actions in the // mapping to take place in whole before we start folding over the items to @@ -1135,6 +1106,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { }) }}; } + // [NOTE]: this ensures we find the immediate parent to any one // of the items about to be merged. If there really are no items // about to be merged, then we can simply return an empty @@ -1157,6 +1129,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { ) else { return FfiItems::default(); }; + // [NOTE]: the dummy module is only in charge of wrapping all // items we want to rename with a path that makes up some // initial portion of the path segments of all descendant items' @@ -1180,7 +1153,10 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { }, ..Default::default() }; - let new_dummy_module = dummy_module.rename(state.subtree.path.clone()); + let Module { + items: new_items, .. + } = dummy_module.rename(state.subtree.path.clone()); + fn remove_uses(m: Module) -> Module { Module { items: FfiItems { @@ -1191,23 +1167,22 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { ..m } } + // [NOTE]: this removes all use declarations in all descendant // modules of the subtrees to merge because otherwise merged // modules can increase the number of use declarations in the // crate's module tree, which is one cause of non-termination. - let new_modules_without_uses: Vec<_> = new_dummy_module - .items - .modules - .into_iter() - .map(remove_uses) - .collect(); + let new_modules_without_uses: Vec<_> = + new_items.modules.into_iter().map(remove_uses).collect(); + FfiItems { modules: new_modules_without_uses, - ..new_dummy_module.items + ..new_items } }, ) .collect(); + processed_items.into_iter().fold( state, |PassPair { @@ -1218,7 +1193,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { // [TODO]: this is still wrong but it also seems to be headed in the // right direction. let new_whole_crate = sync_driver(&it, whole_crate); - let Some(new_subtree) = get_module(&subtree.path, &new_whole_crate) else { + let Some(new_subtree) = get_module(&subtree.path, &new_whole_crate).cloned() else { unreachable!( "The subtree is bound to be found in the whole-crate \ module tree because it was sourced from it, and syncing \ @@ -1226,10 +1201,11 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { modules." ); }; + PassPair { subtree: Module { items: new_subtree.items.items().chain(it.items()).collect(), - ..new_subtree.clone() + ..new_subtree }, whole_crate: new_whole_crate, } @@ -1237,10 +1213,9 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { ) } -/// Provided an item container with the most up-to-date occurrences of some -/// item(s) (`items`,) this function will propagate the state of the path -/// aggregate of each of those items throughout the entirety of the subtree -/// rooted at the `tree` module tree. +/// Provided an item container with the most up-to-date occurrences of some item(s) (`items`,) this +/// function will propagate the state of the path aggregate of each of those items throughout the +/// entirety of the subtree rooted at the `tree` module tree. fn sync_driver(items: &FfiItems, tree: Module) -> Module { items.items().fold(tree, |m, it| { macro_rules! exe { @@ -1262,10 +1237,10 @@ fn sync_driver(items: &FfiItems, tree: Module) -> Module { }) } -/// Provided a reference path aggregate corresponding with that of an item, this -/// function traverses module `m` in search of all occurrences of such an item -/// and "updates" such ocurrences to integrate the paths from `reference`'s path -/// aggregate store that are not part of any one of those item occurrences. +/// Provided a reference path aggregate corresponding with that of an item, this function traverses +/// module `m` in search of all occurrences of such an item and "updates" such ocurrences to +/// integrate the paths from `reference`'s path aggregate store that are not part of any one of +/// those item occurrences. /// /// Note this affects only one concrete item's occurrences throughout the crate. fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { @@ -1281,6 +1256,7 @@ fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { // has an effect on the subtree of items rooted at the module in // question (see the Module::add_path trait method override.) let diff = m.path_aggregate.diff(reference); + diff.into_iter().fold(m, Module::add_path) } else { m @@ -1328,8 +1304,7 @@ fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { } } -/// Appends a new module-local item to an absolute path that does _not_ start -/// with `crate`. +/// 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: Path, new: Ident) -> Path { @@ -1342,8 +1317,7 @@ fn append_path(base: Path, new: Ident) -> Path { /// Determine whether an item is visible to other crates. /// -/// This function assumes that if the visibility is restricted then it is not -/// meant to be accessed. +/// This function assumes that if the visibility is restricted then it is not meant to be accessed. fn is_visible(vis: &syn::Visibility) -> bool { match vis { syn::Visibility::Public(_) => true, @@ -1617,6 +1591,12 @@ impl<'ast> Visit<'ast> for FfiItems { .flatten() .collect(); + /* + * [NOTE]: this doesn't itself parse any items within the module because + * the only hook in syn::visit::Visit we have overridden is that of + * modules. In other words, this recurses only to attempt to find any + * child modules within the module currently being parsed. + */ visit::visit_item_mod(&mut running_items, i); running_items.uses = uses; From 4d6ee9028b35a69676f3699003cc8ac41be676c9 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:47:43 +0100 Subject: [PATCH 095/100] chore: wip --- ctest/src/ffi_items.rs | 200 ++++++++++++++++++++++++++++++++++------- 1 file changed, 167 insertions(+), 33 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index d0cf3e263e636..98b4a20a2106a 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -1,6 +1,11 @@ //! Conversion of Rust code to a simplified abstract syntax tree. use std::cmp::Ordering; +use std::fmt::{ + self, + Debug, + Formatter, +}; use std::ops::{ self, ControlFlow, @@ -13,6 +18,7 @@ use std::{ }; use proc_macro2::Span; +use quote::ToTokens; use syn::punctuated::Punctuated; use syn::token::{ Brace, @@ -57,6 +63,98 @@ use crate::{ ast, }; +// [NOTE]: for use only in printf-debugging. It doesn't automatically derive +// Debug to selectively print out the fields I'm interested in while debugging. +#[derive(Clone)] +struct DebugFfiItems { + aliases: Vec, + structs: Vec, + unions: Vec, + constants: Vec, + foreign_functions: Vec, + foreign_statics: Vec, + modules: Vec, +} + +impl Debug for DebugFfiItems { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut dbg_struct = f.debug_struct("DebugFfiItems"); + + #[derive(Clone)] + struct PrettyPath<'a>(&'a Path); + + impl Debug for PrettyPath<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.pad(&self.0.to_token_stream().to_string()) + } + } + + macro_rules! pretty_print { + ($($field:tt: $ty:tt),+ $(,)?) => {$( + dbg_struct.field_with(stringify!($field), |f| { + f.pad("[ ") + .and_then(|_| { + self.$field + .iter() + .map(|it| { + f.pad(&format!( + concat!( + stringify!($ty), + " {{ path: {}, path_aggregate: {:?} }}", + ), + it.path.to_token_stream().to_string(), + (&it.path_aggregate) + .into_iter() + .map(PrettyPath) + .collect::>(), + )) + }) + .collect::() + }) + .and_then(|_| f.pad(" ]")) + }); + )+}; + } + + pretty_print! { + aliases: Type, + structs: Struct, + unions: Union, + constants: Const, + foreign_functions: Fn, + foreign_statics: Static, + modules: Module, + } + + dbg_struct.finish() + } +} + +impl From for DebugFfiItems { + fn from( + FfiItems { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + .. + }: FfiItems, + ) -> Self { + Self { + aliases, + structs, + unions, + constants, + foreign_functions, + foreign_statics, + modules, + } + } +} + /// 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. Modules are @@ -405,6 +503,7 @@ impl FromIterator for FfiItems { let start = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_)).not()); let end = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_))); + let $field = if start >= end { Vec::new() } else { @@ -798,7 +897,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { "entered rename resolution case with {:?} in module {}", UseRename { ident: ident.clone(), - as_token: syn::parse_quote! {as}, + as_token: syn::parse_quote! { as }, rename: rename.clone(), }, state.subtree.cached_path, @@ -1062,6 +1161,19 @@ fn update_tree( /// Note that, during merging, the items to be merged may end up duplicating existing items on /// `dst`. It is call-site's responsibility to deduplicate those through [`FfiItems::dedup`]. fn merge_module(state: PassPair, src: Vec) -> PassPair { + println!( + "preprocessed_items: {:#?}", + src.iter() + .map(|r| { + if let Resolution::Resolved(it) = r { + format!("resolved: {:?}", DebugFfiItems::from(it.clone())).into() + } else { + format!("unresolved") + } + }) + .collect::>(), + ); + // [NOTE]: we collect all items here because we want the actions in the // mapping to take place in whole before we start folding over the items to // merge. @@ -1086,15 +1198,13 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { .. }| { macro_rules! seek_parent { - ($($it:tt),+ $(,)?) => { - seek_parent!(@@ $($it)+) - }; + ($($it:tt),+ $(,)?) => { seek_parent!(@@ $($it)+) }; + (@@ $head:tt $($tail:tt)*) => { seek_parent!(@ $head).or_else(|| seek_parent!(@@ $($tail)*)) }; - (@@) => { - None - }; + (@@) => { None }; + (@ $field:tt) => {{ $field.first().map(|it| { it.path @@ -1172,7 +1282,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { // modules of the subtrees to merge because otherwise merged // modules can increase the number of use declarations in the // crate's module tree, which is one cause of non-termination. - let new_modules_without_uses: Vec<_> = + let new_modules_without_uses = new_items.modules.into_iter().map(remove_uses).collect(); FfiItems { @@ -1183,6 +1293,15 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { ) .collect(); + println!( + "processed_items: {:#?}", + processed_items + .iter() + .cloned() + .map(|it| format!("{:?}", DebugFfiItems::from(it))) + .collect::>(), + ); + processed_items.into_iter().fold( state, |PassPair { @@ -1225,6 +1344,7 @@ fn sync_driver(items: &FfiItems, tree: Module) -> Module { )+} }; } + exe! { Type, Struct, @@ -1242,7 +1362,20 @@ fn sync_driver(items: &FfiItems, tree: Module) -> Module { /// integrate the paths from `reference`'s path aggregate store that are not part of any one of /// those item occurrences. /// -/// Note this affects only one concrete item's occurrences throughout the crate. +/// # Implementation notes +/// +/// The strategy follows that for any given item `a`, its path aggregate is bound to be unique with +/// respect to that of another item `b` so long as `a` is not an occurrence of `b` in another module +/// of the crate's module tree. We thus define the relation `a ~ b` as one where `b` contains some +/// path in its path aggregate that is also part of `a`'s path aggregate. +/// +/// If we fix `a` and it holds that `a ~ b`, then it follows that there exists some non-empty set +/// `B` of all occurrences `b` of `a` in the crate's module tree. Under the assumption that some two +/// items `a` and `b` may only share path aggregates it they are of the same kind, the below +/// strategy attempts to find akin path aggregates where `a ~ b` holds. This requires not for a +/// single kind of item to be checked against the fixed point (here `reference`) because of the +/// above assumption; Akin path aggregates will only be found for occurrences of the same item, and +/// thus the same kind of item, in the crate's module tree. fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { let akin = |agg: &PathAggregateStore| agg.akin(reference); @@ -1287,6 +1420,7 @@ fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { } }; } + exe! { Type, Struct, @@ -1307,7 +1441,7 @@ fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { /// 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: Path, new: Ident) -> Path { +fn append_path(base: Path, new: &Ident) -> Path { if base.segments.is_empty() { syn::parse_quote! { #new } } else { @@ -1369,7 +1503,7 @@ 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 path = append_path(table.current_module.clone(), i.sig.ident.clone()); + let path = append_path(table.current_module.clone(), &i.sig.ident); let cached_path = ast::path_to_string(&path); let cached_ident = i.sig.ident.to_string().into_boxed_str(); let parameters = i @@ -1419,7 +1553,7 @@ 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 path = append_path(table.current_module.clone(), i.ident.clone()); + let path = append_path(table.current_module.clone(), &i.ident); let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); @@ -1439,7 +1573,7 @@ fn visit_foreign_item_static(table: &mut FfiItems, i: &syn::ForeignItemStatic, a fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { let public = is_visible(&i.vis); - let path = append_path(table.current_module.clone(), i.ident.clone()); + let path = append_path(table.current_module.clone(), &i.ident); let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); @@ -1456,7 +1590,7 @@ fn visit_item_type(table: &mut FfiItems, i: &syn::ItemType) { fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { let public = is_visible(&i.vis); - let path = append_path(table.current_module.clone(), i.ident.clone()); + let path = append_path(table.current_module.clone(), &i.ident); let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let fields = match &i.fields { @@ -1477,7 +1611,7 @@ fn visit_item_struct(table: &mut FfiItems, i: &syn::ItemStruct) { fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { let public = is_visible(&i.vis); - let path = append_path(table.current_module.clone(), i.ident.clone()); + let path = append_path(table.current_module.clone(), &i.ident); let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let fields = collect_fields(&i.fields.named); @@ -1494,7 +1628,7 @@ fn visit_item_union(table: &mut FfiItems, i: &syn::ItemUnion) { fn visit_item_const(table: &mut FfiItems, i: &syn::ItemConst) { let public = is_visible(&i.vis); - let path = append_path(table.current_module.clone(), i.ident.clone()); + let path = append_path(table.current_module.clone(), &i.ident); let cached_path = ast::path_to_string(&path); let cached_ident = i.ident.to_string().into_boxed_str(); let ty = i.ty.deref().clone(); @@ -1551,7 +1685,7 @@ impl<'ast> Visit<'ast> for FfiItems { self.clone() } else { FfiItems { - current_module: append_path(self.current_module.clone(), ident.clone()), + current_module: append_path(self.current_module.clone(), &ident), ..Default::default() } }; @@ -1591,12 +1725,10 @@ impl<'ast> Visit<'ast> for FfiItems { .flatten() .collect(); - /* - * [NOTE]: this doesn't itself parse any items within the module because - * the only hook in syn::visit::Visit we have overridden is that of - * modules. In other words, this recurses only to attempt to find any - * child modules within the module currently being parsed. - */ + // [NOTE]: this doesn't itself parse any items within the module because + // the only hook in syn::visit::Visit we have overridden is that of + // modules. In other words, this recurses only to attempt to find any + // child modules within the module currently being parsed. visit::visit_item_mod(&mut running_items, i); running_items.uses = uses; @@ -1604,6 +1736,7 @@ impl<'ast> Visit<'ast> for FfiItems { *self = running_items; } else { let path = running_items.current_module.clone(); + self.modules.push(Module { public: matches!(vis, Visibility::Public(_)), cached_path: ast::path_to_string(&running_items.current_module), @@ -1662,28 +1795,29 @@ mod test1 { } } "#; + let file = syn::parse_file(source).unwrap(); + fn remove_uses(it: FfiItems) -> FfiItems { let modules: Vec<_> = it .modules - .clone() .into_iter() - .map(|m| { - let items = remove_uses(m.items.clone()); - Module { items, ..m.clone() } + .map(|m| Module { + items: remove_uses(m.items), + ..m }) .collect(); + FfiItems { modules, uses: vec![], - ..it.clone() + ..it } } - let file = syn::parse_file(source).unwrap(); - let items = FfiItems::default(); - let mut items = items.clone(); + + let mut items = FfiItems::default(); items.visit_file(&file); - let items = remove_uses(items.clone()); - println!("{:#?}", items); + + println!("{:#?}", remove_uses(items)); } #[test] From 2db4255509492099cd9c5826bea09c7bf0636be4 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 4 Nov 2026 15:58:05 +0100 Subject: [PATCH 096/100] chore: wip --- ctest/src/ast/mod.rs | 7 +- ctest/src/ast/module.rs | 8 -- ctest/src/ffi_items.rs | 199 +++++++++++++++++++++++++++++++++------- 3 files changed, 172 insertions(+), 42 deletions(-) diff --git a/ctest/src/ast/mod.rs b/ctest/src/ast/mod.rs index 5c7cafdd223c2..865bc9129806a 100644 --- a/ctest/src/ast/mod.rs +++ b/ctest/src/ast/mod.rs @@ -50,6 +50,7 @@ impl PathAggregateStore { pub(crate) fn push(self, p: Path) -> Self { let PathAggregateStore { mut value } = self; value.push(p); + Self { value } } @@ -59,9 +60,9 @@ impl PathAggregateStore { /// the `other` path aggregate are different occurrences of the same item across the crate's /// module tree. pub(crate) fn akin(&self, other: &Self) -> bool { - let ours = &self.value; - let theirs = &other.value; - ours.iter().any(|p1| theirs.iter().any(|p2| p1 == p2)) + self.value + .iter() + .any(|p1| other.value.iter().any(|p2| p1 == p2)) } /// Diffs any two given path aggregates and returns the set of paths that were present in diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 9965cb6d2207e..472efdad45653 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -268,14 +268,6 @@ impl Default for Module { // would likely be unintuitive to have its PartialEq not take into account. impl PartialEq for Module { fn eq(&self, other: &Self) -> bool { - if self.path == other.path { - assert_eq!( - self.path_aggregate, other.path_aggregate, - "path aggregates of module {} and {} are not synchronized", - self.cached_path, other.cached_path, - ); - } - self.public == other.public && self.path == other.path && self.path_aggregate == other.path_aggregate diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 98b4a20a2106a..c65581d1e3611 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -18,7 +18,6 @@ use std::{ }; use proc_macro2::Span; -use quote::ToTokens; use syn::punctuated::Punctuated; use syn::token::{ Brace, @@ -85,7 +84,7 @@ impl Debug for DebugFfiItems { impl Debug for PrettyPath<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.pad(&self.0.to_token_stream().to_string()) + f.pad(&ast::path_to_string(self.0)) } } @@ -100,9 +99,9 @@ impl Debug for DebugFfiItems { f.pad(&format!( concat!( stringify!($ty), - " {{ path: {}, path_aggregate: {:?} }}", + " {{ path: {:?}, path_aggregate: {:?} }}", ), - it.path.to_token_stream().to_string(), + PrettyPath(&it.path), (&it.path_aggregate) .into_iter() .map(PrettyPath) @@ -123,9 +122,33 @@ impl Debug for DebugFfiItems { constants: Const, foreign_functions: Fn, foreign_statics: Static, - modules: Module, } + dbg_struct.field_with("modules", |f| { + f.pad("[ ") + .and_then(|_| { + self.modules + .iter() + .map(|m| { + f.pad("[ ") + .and_then(|_| { + f.pad(&format!( + "path: {:?}, path_aggregate: {:?}, items: {{ {:?} }}", + PrettyPath(&m.path), + (&m.path_aggregate) + .into_iter() + .map(PrettyPath) + .collect::>(), + DebugFfiItems::from(m.items.clone()), + )) + }) + .and_then(|_| f.pad(" ]")) + }) + .collect::() + }) + .and_then(|_| f.pad(" ]")) + }); + dbg_struct.finish() } } @@ -432,6 +455,32 @@ impl FromIterator for FfiItems { fn from_iter>(iter: I) -> FfiItems { let mut partitioned_items: Vec<_> = iter.into_iter().collect(); + println!( + "[FfiItems::from_iter] partitioned_items: {:?}", + partitioned_items + .iter() + .map(|it| { + macro_rules! exe { + ($($ty:tt),+ $(,)?) => { + match it {$( + GenericItem::$ty(it) => it.cached_path.clone(), + )+} + }; + } + + exe! { + Type, + Struct, + Union, + Const, + Fn, + Static, + Module, + } + }) + .collect::>() + ); + // [NOTE]: this is expected to sort all items by their kind, such that // we end up with the following ordering: // @@ -472,9 +521,9 @@ impl FromIterator for FfiItems { | (GenericItem::Const(_), GenericItem::Fn(_)) | (GenericItem::Const(_), GenericItem::Static(_)) | (GenericItem::Const(_), GenericItem::Module(_)) - | (GenericItem::Static(_), GenericItem::Module(_)) | (GenericItem::Fn(_), GenericItem::Static(_)) - | (GenericItem::Fn(_), GenericItem::Module(_)) => Ordering::Less, + | (GenericItem::Fn(_), GenericItem::Module(_)) + | (GenericItem::Static(_), GenericItem::Module(_)) => Ordering::Less, (GenericItem::Struct(_), GenericItem::Type(_)) | (GenericItem::Union(_), GenericItem::Type(_)) | (GenericItem::Union(_), GenericItem::Struct(_)) @@ -500,14 +549,27 @@ impl FromIterator for FfiItems { macro_rules! get_partitions { ($($field:ident: $ty:tt),+ $(,)?) => {$( + macro_rules! search_idx { + ($start:expr, $pred:expr) => {{ + partitioned_items + .iter() + .enumerate() + .skip($start) + .skip_while(|(_, it)| $pred(it)) + .next() + .map(|(idx, _)| idx) + .unwrap_or(partitioned_items.len()) + }}; + } + let start = - partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_)).not()); - let end = partitioned_items.partition_point(|it| matches!(it, GenericItem::$ty(_))); + search_idx!(0, |it: &&GenericItem| matches!(it, GenericItem::$ty(_)).not()); + let end = search_idx!(start, |it: &&GenericItem| matches!(it, GenericItem::$ty(_))); let $field = if start >= end { Vec::new() } else { - partitioned_items + let partition = partitioned_items .get(start..end) .map(|it| { it.into_iter() @@ -521,13 +583,27 @@ impl FromIterator for FfiItems { ".", )), }) - .collect() + .collect::>() }) .expect( "The slice must be valid because the range is \ logical, and the components of the range are \ indices into the collection.", - ) + ); + + println!( + concat!( + "[FfiItems::from_iter] ", + stringify!($field), + "_partition: {:?}" + ), + partition + .iter() + .map(|it| &it.cached_path) + .collect::>(), + ); + + partition }; )+}; } @@ -759,7 +835,7 @@ fn resolve_pass(src: PassPair) -> PassPair { ); println!( - "finished module pass on module {}", + "[resolve_pass] finished module pass on module {}", if &*new_root.cached_path == "" { "root".into() } else { @@ -782,13 +858,45 @@ fn resolve_pass(src: PassPair) -> PassPair { /// See [`resolve_pass`] for details. fn resolve_one(state: PassPair) -> Vec { println!( - "found {} use declarations in module {}", + "[resolve_one] found {} use declarations in module {}: {:?}", state.subtree.items.uses.len(), if &*state.subtree.cached_path == "" { "root" } else { &*state.subtree.cached_path }, + state + .subtree + .items + .uses + .iter() + .map(|it| { + // [NOTE]: we use a dedicated type so as to avoid having the + // overarching call to the Debug implementation of the container + // cause the inner strings to be quoted. + #[derive(Clone)] + struct PrettyUseTree<'a>(&'a RefinedUseTree); + + impl Debug for PrettyUseTree<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.pad(&refined_tree_to_string(&self.0)) + } + } + + fn refined_tree_to_string(t: &RefinedUseTree) -> String { + match t { + RefinedUseTree::Path(p) => { + format!("{}::{}", p.ident, refined_tree_to_string(&p.tree)) + } + RefinedUseTree::Name(n) => format!("{}", n.ident), + RefinedUseTree::Rename(r) => format!("{} as {}", r.ident, r.rename), + RefinedUseTree::Glob => format!("*"), + } + } + + PrettyUseTree(&it.tree) + }) + .collect::>(), ); state @@ -834,7 +942,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { match r#use.tree { RefinedUseTree::Name(UseName { ident }) => { println!( - "entered name resolution case with {} in module {}", + "[resolve_use] entered name resolution case with {} in module {}", ident, state.subtree.cached_path, ); @@ -882,7 +990,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { } RefinedUseTree::Glob => { println!( - "entered glob resolution case in module {}", + "[resolve_use] entered glob resolution case in module {}", if &*state.subtree.cached_path == "" { "root" } else { @@ -894,7 +1002,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { } RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { println!( - "entered rename resolution case with {:?} in module {}", + "[resolve_use] entered rename resolution case with {:?} in module {}", UseRename { ident: ident.clone(), as_token: syn::parse_quote! { as }, @@ -968,7 +1076,7 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { println!( - "entered path resolution case with {} in module {}", + "[resolve_use] entered path resolution case with {} in module {}", ident, state.subtree.cached_path, ); @@ -1162,7 +1270,7 @@ fn update_tree( /// `dst`. It is call-site's responsibility to deduplicate those through [`FfiItems::dedup`]. fn merge_module(state: PassPair, src: Vec) -> PassPair { println!( - "preprocessed_items: {:#?}", + "[merge_module] preprocessed_items: {:#?}", src.iter() .map(|r| { if let Resolution::Resolved(it) = r { @@ -1294,7 +1402,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { .collect(); println!( - "processed_items: {:#?}", + "[merge_module] processed_items: {:#?}", processed_items .iter() .cloned() @@ -1311,7 +1419,8 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { it| { // [TODO]: this is still wrong but it also seems to be headed in the // right direction. - let new_whole_crate = sync_driver(&it, whole_crate); + // [FIXME]: get rid of the below clone once debugging is done. + let new_whole_crate = sync_driver(&it, whole_crate.clone()); let Some(new_subtree) = get_module(&subtree.path, &new_whole_crate).cloned() else { unreachable!( "The subtree is bound to be found in the whole-crate \ @@ -1321,6 +1430,17 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { ); }; + assert_ne!( + whole_crate, new_whole_crate, + "old whole-crate module tree matches new whole-crate module \ + tree even though there were items to merge", + ); + + println!( + "[merge_module] new_whole_crate: {:?}", + DebugFfiItems::from(new_whole_crate.items.clone()) + ); + PassPair { subtree: Module { items: new_subtree.items.items().chain(it.items()).collect(), @@ -1379,6 +1499,16 @@ fn sync_driver(items: &FfiItems, tree: Module) -> Module { fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { let akin = |agg: &PathAggregateStore| agg.akin(reference); + println!( + "[sync_aggregates] preprocessed_module {}: {:?}", + if m.cached_path.is_empty() { + "root" + } else { + &m.cached_path + }, + DebugFfiItems::from(m.items.clone()), + ); + let new_m = if akin(&m.path_aggregate) { // [NOTE]: this we diff instead of merging because merging will get us a // set of path aggregates that contain both the ones already in m, as @@ -1395,7 +1525,7 @@ fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { m }; - let new_items = new_m + let new_items: FfiItems = new_m .items .items() .map(|it| { @@ -1403,20 +1533,17 @@ fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { ($($ty:tt),+ $(,)?) => { match it { $( - GenericItem::$ty(it) => { - if akin(&it.path_aggregate) { - GenericItem::$ty($ty { - path_aggregate: reference.clone(), - ..it - }) - } else { - GenericItem::$ty(it) - } + GenericItem::$ty(it) if akin(&it.path_aggregate) => { + GenericItem::$ty($ty { + path_aggregate: reference.clone(), + ..it + }) } )+ GenericItem::Module(it) => { GenericItem::Module(sync_aggregates(reference, it)) } + non_matching => non_matching, } }; } @@ -1432,6 +1559,16 @@ fn sync_aggregates(reference: &PathAggregateStore, m: Module) -> Module { }) .collect(); + println!( + "[sync_aggregates] processed_module {}: {:?}", + if new_m.cached_path.is_empty() { + "root" + } else { + &new_m.cached_path + }, + DebugFfiItems::from(new_items.clone()), + ); + Module { items: new_items, ..new_m From 845db1631f90c9e8ab76de1999454b9ee9b53d46 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:00:31 +0100 Subject: [PATCH 097/100] chore: wip --- ctest/src/ast/module.rs | 6 +- ctest/src/ffi_items.rs | 118 ++++++++++++++++++++++++++-------------- ctest/src/template.rs | 9 ++- 3 files changed, 82 insertions(+), 51 deletions(-) diff --git a/ctest/src/ast/module.rs b/ctest/src/ast/module.rs index 472efdad45653..96ab8b29649c5 100644 --- a/ctest/src/ast/module.rs +++ b/ctest/src/ast/module.rs @@ -1,8 +1,5 @@ use std::cmp::Ordering; -use std::ops::{ - self, - Not, -}; +use std::ops; use syn::Path; use syn::punctuated::Punctuated; @@ -203,7 +200,6 @@ impl PathUtils for Module { let new_items = self .items .items() - .filter(|it| matches!(it, GenericItem::Module(_)).not()) .map(|it| { macro_rules! exe { ($($ty:tt),+ $(,)?) => { diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index c65581d1e3611..c044bcdb94bf5 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -153,6 +153,28 @@ impl Debug for DebugFfiItems { } } +// [NOTE]: for debugging purposes only. Pretty prints a refined type for +// syn::UseTree. +#[derive(Clone)] +struct PrettyUseTree<'a>(&'a RefinedUseTree); + +impl Debug for PrettyUseTree<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.pad(&refined_tree_to_string(&self.0)) + } +} + +fn refined_tree_to_string(t: &RefinedUseTree) -> String { + match t { + RefinedUseTree::Path(p) => { + format!("{}::{}", p.ident, refined_tree_to_string(&p.tree)) + } + RefinedUseTree::Name(n) => format!("{}", n.ident), + RefinedUseTree::Rename(r) => format!("{} as {}", r.ident, r.rename), + RefinedUseTree::Glob => format!("*"), + } +} + impl From for DebugFfiItems { fn from( FfiItems { @@ -269,6 +291,23 @@ impl FfiItems { semi: None, }; + /* + [TODO]: the idea here is to change this into using a state monad that + lets us worry not about mutable state (even though syn's visitor + interface is mutable in nature, it would hide those details from us.) + This would carry both the `current_module` state that is now part of + FfiItems, as well as the unique ID that is assigned to each parsed + item. Then, we can get rid of the path_aggregate field of each of the + parsed items and the whole machinery built around it. Once we're + actually generating the test templates, we'll have to carry around + both the whole-crate FfiItems as well as the FfiItems specific to the + module for which the current test is being generated. That may not + need any specific type to carry the state because the whole-crate + FfiItems would only be needed up to filtering. That's when, for each + given item, we search its unique ID in a whole-crate traversal, such + that we run the user-configured skip with each of the occurrences of + the item in question. + */ self.visit_item_mod(&file_mod); let root_module = Module { @@ -870,32 +909,8 @@ fn resolve_one(state: PassPair) -> Vec { .items .uses .iter() - .map(|it| { - // [NOTE]: we use a dedicated type so as to avoid having the - // overarching call to the Debug implementation of the container - // cause the inner strings to be quoted. - #[derive(Clone)] - struct PrettyUseTree<'a>(&'a RefinedUseTree); - - impl Debug for PrettyUseTree<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.pad(&refined_tree_to_string(&self.0)) - } - } - - fn refined_tree_to_string(t: &RefinedUseTree) -> String { - match t { - RefinedUseTree::Path(p) => { - format!("{}::{}", p.ident, refined_tree_to_string(&p.tree)) - } - RefinedUseTree::Name(n) => format!("{}", n.ident), - RefinedUseTree::Rename(r) => format!("{} as {}", r.ident, r.rename), - RefinedUseTree::Glob => format!("*"), - } - } - - PrettyUseTree(&it.tree) - }) + .map(|RefinedUse { tree, .. }| tree) + .map(PrettyUseTree) .collect::>(), ); @@ -930,6 +945,16 @@ fn resolve_one(state: PassPair) -> Vec { /// The other element of the pair is the whole-crate module tree. That is used to resolve ancestor /// paths (when `super` is found) and absolute paths (when `crate` is found.) fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { + macro_rules! log { + ($msg:literal $(, $complement:expr)* $(,)?) => { + println!( + "[resolve_use {:?}] {}", + PrettyUseTree(&(r#use.tree)), + format_args!($msg $(, $complement)*), + ); + }; + } + macro_rules! single_item { ($field:ident: $it:expr) => { return Resolution::Resolved(FfiItems { @@ -940,10 +965,11 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { } match r#use.tree { - RefinedUseTree::Name(UseName { ident }) => { - println!( - "[resolve_use] entered name resolution case with {} in module {}", - ident, state.subtree.cached_path, + RefinedUseTree::Name(UseName { ref ident }) => { + log!( + "entered name resolution case with {} in module {}", + ident, + state.subtree.cached_path, ); // [NOTE]: self may not appear as the name basecase of a well-formed @@ -989,8 +1015,8 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { } } RefinedUseTree::Glob => { - println!( - "[resolve_use] entered glob resolution case in module {}", + log!( + "entered glob resolution case in module {}", if &*state.subtree.cached_path == "" { "root" } else { @@ -1000,9 +1026,13 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { Resolution::Resolved(state.subtree.items) } - RefinedUseTree::Rename(UseRename { ident, rename, .. }) => { - println!( - "[resolve_use] entered rename resolution case with {:?} in module {}", + RefinedUseTree::Rename(UseRename { + ref ident, + ref rename, + .. + }) => { + log!( + "entered rename resolution case with {:?} in module {}", UseRename { ident: ident.clone(), as_token: syn::parse_quote! { as }, @@ -1074,10 +1104,14 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { } } - RefinedUseTree::Path(RefinedUsePath { ident, tree }) => { - println!( - "[resolve_use] entered path resolution case with {} in module {}", - ident, state.subtree.cached_path, + RefinedUseTree::Path(RefinedUsePath { + ref ident, + ref tree, + }) => { + log!( + "entered path resolution case with {} in module {}", + ident, + state.subtree.cached_path, ); let new_state = if is_self(&ident) { @@ -1109,9 +1143,11 @@ fn resolve_use(r#use: RefinedUse, state: PassPair) -> Resolution { return Resolution::Unresolved; }; + // [FIXME]: remove the below clone once debugging is done and the + // logging macro invocation above is removed. resolve_use( RefinedUse { - tree: *tree, + tree: *tree.clone(), ..r#use }, new_state, @@ -1274,7 +1310,7 @@ fn merge_module(state: PassPair, src: Vec) -> PassPair { src.iter() .map(|r| { if let Resolution::Resolved(it) = r { - format!("resolved: {:?}", DebugFfiItems::from(it.clone())).into() + format!("resolved: {:?}", DebugFfiItems::from(it.clone())) } else { format!("unresolved") } diff --git a/ctest/src/template.rs b/ctest/src/template.rs index de8c86108eb81..66aa7b1768bcc 100644 --- a/ctest/src/template.rs +++ b/ctest/src/template.rs @@ -696,11 +696,10 @@ 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) { fn skipper(items: &mut FfiItems, generator: &TestGenerator) { let skipped = items.aliases.extract_if(.., |alias| { @@ -711,7 +710,7 @@ impl<'a> TranslateHelper<'a> { // crate.) generator.skips.iter().any(|f| { alias - .with_variations(|it| f(&MapInput::CEnumType(Type::path(&it)))) + .with_variations(|it| f(&MapInput::CEnumType(&it.cached_path))) .into_iter() .any(convert::identity) }) From 44c78193f0b9bacffc88e041e661e60a359936e3 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 5 Nov 2026 12:13:13 +0100 Subject: [PATCH 098/100] chore: wip --- ctest/src/ffi_items.rs | 87 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index c044bcdb94bf5..3c52cbaddf535 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -14,6 +14,8 @@ use std::ops::{ }; use std::{ iter, + mem, + ptr, vec, }; @@ -200,6 +202,70 @@ impl From for DebugFfiItems { } } +/// Tuple-based state monad used for keeping track of mutable state while parsing items for +/// FfiItems. +/// +/// The monad is currently used for tracking the current module of the item being parsed, as well as +/// the unique ID of all parsed items. The monad in those cases corresponds with the concrete type +/// `StateMonad`. The implementation of `syn::visit::Visit` doesn't avoid +/// the impure mutable state of that interface but scopes it to polymorphic parameter `T` in +/// a separate copy of the state monad. After the recursive visitor traversal is done in the copy, +/// the original and mutated state monads are reconciled. +struct StateMonad { + seq: Box (S, T)>, +} + +impl StateMonad { + fn pure(value: T) -> Self + where + for<'a> T: 'a, + { + Self { + seq: Box::new(|s| (s, value)), + } + } + + fn bind(self, f: F) -> StateMonad + where + for<'a> S: 'a, + for<'a> T: 'a, + for<'a> F: ops::FnOnce(T) -> StateMonad + 'a, + { + StateMonad { + seq: Box::new(|s| { + let (state, value) = (self.seq)(s); + + (f(value).seq)(state) + }), + } + } + + fn run(self, init: S) -> (S, T) { + (self.seq)(init) + } +} + +macro_rules! set_state { + ($f:expr) => { + StateMonad { seq: Box::new($f) } + }; +} + +#[test] +fn tmpmonad() { + let (state, value) = StateMonad::pure(2) + .bind(|x| set_state!(move |s| (s * x, x))) + .run(2); + + println!("state: {state}, value: {value}"); +} + +#[derive(Debug, Clone)] +struct ParsingState { + uid: usize, + current_module: Path, +} + /// 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. Modules are @@ -295,7 +361,7 @@ impl FfiItems { [TODO]: the idea here is to change this into using a state monad that lets us worry not about mutable state (even though syn's visitor interface is mutable in nature, it would hide those details from us.) - This would carry both the `current_module` state that is now part of + This would carry both the current_module state that is now part of FfiItems, as well as the unique ID that is assigned to each parsed item. Then, we can get rid of the path_aggregate field of each of the parsed items and the whole machinery built around it. Once we're @@ -1838,6 +1904,25 @@ fn visit_item_foreign_mod(table: &mut FfiItems, i: &syn::ItemForeignMod) { } } +impl<'ast> Visit<'ast> for StateMonad { + fn visit_item_mod(&mut self, i: &'ast ItemMod) { + let ItemMod { + vis, + ident, + content: Some((_, items)), + .. + } = i + else { + unreachable!("Ibid.") + }; + + let is_crate_root = ident == "__ctest_root_mod"; + let current_monad = mem::replace(self, set_state!(|s| (s, FfiItems::default()))); + + todo!("Port over the rest of FfiItems to this impl.") + } +} + impl<'ast> Visit<'ast> for FfiItems { fn visit_item_mod(&mut self, i: &'ast ItemMod) { let ItemMod { From ac54788b387c0cab175807b00e91da64f611e2c5 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:13:53 +0100 Subject: [PATCH 099/100] chore: wip --- ctest/src/ffi_items.rs | 148 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 136 insertions(+), 12 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 3c52cbaddf535..7cf1a27377dd6 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -15,7 +15,6 @@ use std::ops::{ use std::{ iter, mem, - ptr, vec, }; @@ -208,9 +207,11 @@ impl From for DebugFfiItems { /// The monad is currently used for tracking the current module of the item being parsed, as well as /// the unique ID of all parsed items. The monad in those cases corresponds with the concrete type /// `StateMonad`. The implementation of `syn::visit::Visit` doesn't avoid -/// the impure mutable state of that interface but scopes it to polymorphic parameter `T` in -/// a separate copy of the state monad. After the recursive visitor traversal is done in the copy, -/// the original and mutated state monads are reconciled. +/// the impure mutable state of that interface but scopes it to polymorphic parameter `T` in a +/// separate copy of the state monad. After the recursive visitor traversal is done in the copy, the +/// original and mutated state monads are reconciled by binding the former to the latter. +// [FIXME]: it may be a better idea to change this into a cps state monad just +// to see whether they work out better with linear terms. struct StateMonad { seq: Box (S, T)>, } @@ -245,7 +246,7 @@ impl StateMonad { } } -macro_rules! set_state { +macro_rules! set { ($f:expr) => { StateMonad { seq: Box::new($f) } }; @@ -254,10 +255,10 @@ macro_rules! set_state { #[test] fn tmpmonad() { let (state, value) = StateMonad::pure(2) - .bind(|x| set_state!(move |s| (s * x, x))) - .run(2); + .bind(|x| set!(move |s| (s * x, x))) + .run(3); - println!("state: {state}, value: {value}"); + assert_eq!((state, value), (6, 2)); } #[derive(Debug, Clone)] @@ -794,7 +795,7 @@ pub(crate) enum RefinedUseTree { /// purpose of this type. #[derive(Clone, Debug, PartialEq, Default)] pub(crate) struct RefinedUse { - is_public: bool, + public: bool, tree: RefinedUseTree, } @@ -1917,9 +1918,132 @@ impl<'ast> Visit<'ast> for StateMonad { }; let is_crate_root = ident == "__ctest_root_mod"; - let current_monad = mem::replace(self, set_state!(|s| (s, FfiItems::default()))); + let base_monad = mem::replace(self, set!(|s| (s, FfiItems::default()))); - todo!("Port over the rest of FfiItems to this impl.") + let current_monad = if is_crate_root { + base_monad + } else { + let ident = ident.clone(); + + base_monad.bind(move |items| { + set!(move |ParsingState { + uid, + current_module, + }| ( + ParsingState { + uid, + current_module: append_path(current_module, &ident), + }, + items + )) + }) + }; + + // [NOTE]: we don't parse modules here because they will get their state + // updated while folding through the items, which would mean we would + // lose the current_module piece of state that we keep in the state + // monad. + let new_monad = items + .iter() + .filter(|it| match it { + Item::Type(_) + | Item::Struct(_) + | Item::Union(_) + | Item::Const(_) + | Item::ForeignMod(_) + | Item::Mod(_) + | Item::Use(_) => true, + _ => false, + }) + .cloned() + .fold(current_monad, |monad, it| { + macro_rules! exe { + ($($f:tt: $ty:tt),+ $(,)?) => { + match it { + $( + // [NOTE]: this is the one part where we have to + // fall into sin and allow in-place mutation of the + // items within the monad. On a more pragmatic note, + // this is fair because items is known to be a + // linear resource and this occurrence is + // independent of any other one in the universe. + Item::$ty(it) => monad.bind(move |mut items| { + $f(&mut items, &it); + + StateMonad::pure(items) + }), + )+ + Item::Use(ItemUse { vis, tree, .. }) => { + let base = normalize_path(tree).into_iter().map(move |tree| { + RefinedUse { + public: matches!(vis, Visibility::Public(_)), + tree, + } + }); + + monad.bind(|items| { + StateMonad::pure(FfiItems { + uses: items.uses.into_iter().chain(base).collect(), + ..items + }) + }) + } + _ => monad, + } + }; + } + + exe! { + visit_item_type: Type, + visit_item_struct: Struct, + visit_item_union: Union, + visit_item_const: Const, + visit_item_foreign_mod: ForeignMod, + } + }); + + // [TODO]: this will likely need me wrapping the rest of the function in + // one huge bind call with which I can get access to the current monad's + // state while using the below "child" monad for recursing into other + // modules. Then, the monad returned in the outer bind ought integrate + // its previous items and the new module parsed off of the child monad. + // Though the details of it are a bit fuzzy. + let mut child_monad = set!(|s| ( + ParsingState { + current_module: todo!(), + uid: todo!(), + }, + FfiItems::default() + )); + visit::visit_item_mod(&mut child_monad, i); + + if is_crate_root { + *self = new_monad; + } else { + let vis = vis.clone(); + + *self = new_monad.bind(move |items| { + set!(move |s| { + let path = s.current_module.clone(); + let items = FfiItems { + modules: items + .modules + .into_iter() + .chain([Module { + public: matches!(vis, Visibility::Public(_)), + cached_path: ast::path_to_string(&path), + cached_ident: ast::ident_to_string(&path), + path: path.clone(), + items: items.clone(), + ..Default::default() + }]) + .collect(), + ..items + }; + (s, items) + }) + }); + } } } @@ -1974,7 +2098,7 @@ impl<'ast> Visit<'ast> for FfiItems { Item::Use(ItemUse { vis, tree, .. }) => normalize_path(tree.clone()) .into_iter() .map(move |tree| RefinedUse { - is_public: matches!(vis, Visibility::Public(_)), + public: matches!(vis, Visibility::Public(_)), tree, }) .into(), From 986475a78f3671de556f7e13c586e5c78205dba8 Mon Sep 17 00:00:00 2001 From: Adam Martinez <149513579+dybucc@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:03:02 +0100 Subject: [PATCH 100/100] chore: wip --- ctest/src/ffi_items.rs | 164 +++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 57 deletions(-) diff --git a/ctest/src/ffi_items.rs b/ctest/src/ffi_items.rs index 7cf1a27377dd6..7dbae031f437f 100644 --- a/ctest/src/ffi_items.rs +++ b/ctest/src/ffi_items.rs @@ -241,21 +241,22 @@ impl StateMonad { } } + fn set(f: F) -> StateMonad + where + for<'a> F: ops::FnOnce(S) -> (S, T) + 'a, + { + StateMonad { seq: Box::new(f) } + } + fn run(self, init: S) -> (S, T) { (self.seq)(init) } } -macro_rules! set { - ($f:expr) => { - StateMonad { seq: Box::new($f) } - }; -} - #[test] fn tmpmonad() { let (state, value) = StateMonad::pure(2) - .bind(|x| set!(move |s| (s * x, x))) + .bind(|x| StateMonad::set(move |s| (s * x, x))) .run(3); assert_eq!((state, value), (6, 2)); @@ -265,6 +266,7 @@ fn tmpmonad() { struct ParsingState { uid: usize, current_module: Path, + module_is_public: bool, } /// Represents a collected set of top-level Rust items relevant to FFI generation or analysis. @@ -1905,6 +1907,16 @@ fn visit_item_foreign_mod(table: &mut FfiItems, i: &syn::ItemForeignMod) { } } +/* + * [TODO]: this we can either change into using as value in the state monad a + * Module, or otherwise add further state to the parsing state that would carry + * the information of a given module. The latter option seems like the one that + * would require the least amount of changes. + */ +/* + * [TODO]: implement the unique ID stuff; Take care of which state is carried + * through state monads passed in (eventually) recursive call-sites. + */ impl<'ast> Visit<'ast> for StateMonad { fn visit_item_mod(&mut self, i: &'ast ItemMod) { let ItemMod { @@ -1914,28 +1926,39 @@ impl<'ast> Visit<'ast> for StateMonad { .. } = i else { + /* + * [FIXME]: copy over the comment from the Visit implementation for + * FfiItems. + */ unreachable!("Ibid.") }; let is_crate_root = ident == "__ctest_root_mod"; - let base_monad = mem::replace(self, set!(|s| (s, FfiItems::default()))); + let base_monad = mem::replace(self, StateMonad::set(|s| (s, FfiItems::default()))); let current_monad = if is_crate_root { base_monad } else { let ident = ident.clone(); + let vis = vis.clone(); base_monad.bind(move |items| { - set!(move |ParsingState { - uid, - current_module, - }| ( - ParsingState { - uid, - current_module: append_path(current_module, &ident), + StateMonad::set( + move |ParsingState { + uid, + current_module, + .. + }| { + ( + ParsingState { + uid, + current_module: append_path(current_module, &ident), + module_is_public: matches!(vis, Visibility::Public(_)), + }, + items, + ) }, - items - )) + ) }) }; @@ -1957,6 +1980,13 @@ impl<'ast> Visit<'ast> for StateMonad { }) .cloned() .fold(current_monad, |monad, it| { + /* + [FIXME]: tweak the functions we currently use for parsing + items such that they use the state monad's state rather than + the state currently within FfiItems (i.e. field + current_module.) + */ + macro_rules! exe { ($($f:tt: $ty:tt),+ $(,)?) => { match it { @@ -1973,6 +2003,43 @@ impl<'ast> Visit<'ast> for StateMonad { StateMonad::pure(items) }), )+ + Item::Mod(it) => { + let mut child_monad = StateMonad::pure(FfiItems::default()); + child_monad.visit_item_mod(&it); + + monad.bind(|items| { + StateMonad::set(|state: ParsingState| { + let (ParsingState { + uid, + module_is_public, + current_module, + .. + }, child_items) = child_monad.run(state.clone()); + + // [TODO]: this is still fuzzy. + let new_state = ParsingState { uid, ..state }; + let new_items = FfiItems { + modules: items + .modules + .into_iter() + .chain([Module { + public: module_is_public, + cached_ident: + ast::ident_to_string(¤t_module), + cached_path: + ast::path_to_string(¤t_module), + path: current_module, + items: child_items, + ..Default::default() + }]) + .collect(), + ..items + }; + + (new_state, new_items) + }) + }) + }, Item::Use(ItemUse { vis, tree, .. }) => { let base = normalize_path(tree).into_iter().map(move |tree| { RefinedUse { @@ -2002,48 +2069,31 @@ impl<'ast> Visit<'ast> for StateMonad { } }); - // [TODO]: this will likely need me wrapping the rest of the function in - // one huge bind call with which I can get access to the current monad's - // state while using the below "child" monad for recursing into other - // modules. Then, the monad returned in the outer bind ought integrate - // its previous items and the new module parsed off of the child monad. - // Though the details of it are a bit fuzzy. - let mut child_monad = set!(|s| ( - ParsingState { - current_module: todo!(), - uid: todo!(), - }, - FfiItems::default() - )); - visit::visit_item_mod(&mut child_monad, i); + *self = new_monad.bind(move |it| { + StateMonad::set(move |state: ParsingState| { + let mut child_monad = StateMonad::pure(FfiItems::default()); - if is_crate_root { - *self = new_monad; - } else { - let vis = vis.clone(); + let ( + ParsingState { + uid, + module_is_public, + .. + }, + child_items, + ) = child_monad.run(state.clone()); + let new_state = ParsingState { + uid, + current_module: state.current_module, + module_is_public: state.module_is_public, + }; + let new_items = FfiItems { + modules: it.modules.into_iter().chain(child_items.modules).collect(), + ..it + }; - *self = new_monad.bind(move |items| { - set!(move |s| { - let path = s.current_module.clone(); - let items = FfiItems { - modules: items - .modules - .into_iter() - .chain([Module { - public: matches!(vis, Visibility::Public(_)), - cached_path: ast::path_to_string(&path), - cached_ident: ast::ident_to_string(&path), - path: path.clone(), - items: items.clone(), - ..Default::default() - }]) - .collect(), - ..items - }; - (s, items) - }) - }); - } + (new_state, new_items) + }) + }); } }