From 304b5dc207128f8e3bf7995df8b7941933ffb91a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:07:19 -0800 Subject: [PATCH 1/9] chore(quest): claim quest/m1/ffi-export/refusals/generics.md Quest-Claim: 119062c50befb018 From 02a732ac632e99d31929b8dacb872160df5bba8a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:29:56 -0800 Subject: [PATCH 2/9] feat(ffi): export generic parameters through blanket impls and per-choice bounds, and leave out traits C cannot implement Co-Authored-By: Claude Opus 5.5 --- ffi/README.md | 10 +- ffi/fixtures/api/examples/transcript.rs | 25 ++++ ffi/fixtures/api/ffi.toml | 10 +- ffi/fixtures/api/src/lib.rs | 46 +++++++ ffi/fixtures/reject/expected.txt | 33 ++--- ffi/fixtures/reject/src/lib.rs | 7 +- ffi/src/census.rs | 16 ++- ffi/src/collect.rs | 155 +++++++++++++++++------- ffi/src/ir.rs | 2 + ffi/tests/api.c | 13 ++ ffi/tests/api.cpp | 9 ++ ffi/tests/api.dart | 7 ++ ffi/tests/api.go | 7 ++ ffi/tests/api.kt | 8 ++ ffi/tests/api.py | 8 ++ ffi/tests/api.swift | 9 ++ 16 files changed, 292 insertions(+), 73 deletions(-) diff --git a/ffi/README.md b/ffi/README.md index 8125eb80d..7a2d910fb 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -62,7 +62,7 @@ its types map by role; a type no path reaches is refused. | a type whose only generic parameters are lifetimes, such as `Path<'a>` | an object holding its `'static` form | | a generic type, at each instance it infers | that instance as any other type, with each method whose impl and own bounds it meets; any other instance is refused | | a `pub` field of an object | a getter returning a copy, and a setter assigning in place, as `x.field = value` does | -| a `pub` trait | a callback interface: a table of C functions and their data | +| a `pub` trait | a callback interface: a table of C functions and their data; a trait C cannot implement, such as one with a blanket impl, an associated type, or a supertrait but a marker, is left out, since C never passes one, and a signature taking one from C is refused | | a type parameter or `impl` bounded by `Fn`, `FnMut`, or `FnOnce`, which no lifetime bound or return lets outlive the call | a C function and its `void *` data, which Rust calls only during the call; any other closure is refused | | `fn` and inherent `pub` methods | functions named through their type | | `Result` | `bool f(..., T *out, E *err)` | @@ -86,8 +86,9 @@ its types map by role; a type no path reaches is refused. | a `pub const` | a `#define` if a `bool` or integer, else a function returning it | | `impl Into`, `impl AsRef`, or a bound implying one, as a supertrait does | `X`, or `&X` and then `String` or `Vec` where `X` is `str` or `[T]` | | `impl Into`, where `X` does not map | what a `From` implementation into `X` takes: a source parameter by its bounds as above, so `impl> From for Timed` takes `Bytes`, and by each `Into` whose `Y` does not map, what converts into `Y` in turn, each `Y` once and refusing a chain longer than the crate's recursion limit; any other source as it is, so `impl<'a, V> From<&'a V> for Timed<&'a V>` takes `&V`. Two sources C passes as one buffer, such as `String` and `Vec`, are refused, unless they are forms of one value, as `&str` and `String` are | -| `impl IntoIterator`, where `X` is no type parameter | `Vec` | -| another type parameter | `serde_json::Value` for a serde `Serialize` or `Deserialize` bound, else the first of `String`, `Bytes`, and `Vec` meeting its bounds, or a foreign implementation of its one exported trait; else an instance of the function at each type the exports implement a trait bounding it for, as `&X` where that meets its bounds too and no bound, its own or one a supertrait implies, makes it outlive the call, so C keeps its handle, and, as an argument of a generic type, at each argument of an instance that meets its bounds and makes each generic type the signature names an instance | +| `impl IntoIterator`, where `X` is no type parameter | `Vec`, or `&[Y]` where `X` is `&Y` | +| `impl io::Read` | `&[u8]` | +| another type parameter | `serde_json::Value` for a serde `Serialize` or `Deserialize` bound, else the first of `String`, `Bytes`, and `Vec` meeting its bounds, or a foreign implementation of its one exported trait; else an instance of the function at each type the exports implement a trait bounding it for, through a blanket impl at each type implementing what the impl requires, as `&X` where that meets its bounds too and no bound, its own or one a supertrait implies, makes it outlive the call, so C keeps its handle, and, as an argument of a generic type, at each argument of an instance that meets its bounds and makes each generic type the signature names an instance | A generic type infers the instances it exports at, with no config: the one at its defaults, if each type parameter has one; the one each inherent impl is of, such as @@ -614,7 +615,8 @@ runtime package and each crate's include [`runtime/rt.go`](runtime/rt.go) and `--census dir` exports nothing. It writes `.md` for the facade, which counts what exports and ranks every refusal by construct and by the type it names, lists each refused item at its span, and ends with the `exclude` list that exports the -rest. It ignores the config's `exclude`. It still fails on an `instance not inferred` +rest, and with what is left out, such as the traits C cannot implement and why. It ignores +the config's `exclude`. It still fails on an `instance not inferred` refusal, which is a bug in the exporter's inference rather than in the crate. ## Tests diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index b58b4ae3c..d82c0a6b4 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -374,6 +374,13 @@ fn main() { println!("cross {} outer {}", api::cross(left, right), api::Outer::new(4).get()); println!("kept {} boxed {}", api::kept(Point { x: 1, y: -2 }), api::BOXED.value); println!("keep {}", api::keep(Point { x: 1, y: -2 })); + let five = Point { x: 5, y: 2 }; + println!( + "kept or {} {} keeper {}", + api::kept_or(five), + api::kept_or::(None), + api::keeper(five) + ); let sides = Pair { left: 2u32, right: 5 }; println!("apply {}", api::apply(sides, |side| side * 3)); println!("report {}", api::report(sides, Print)); @@ -465,6 +472,24 @@ fn main() { api::longest(["a".to_string(), "abc".to_string()]), api::longest([]) ); + let limits = api::byte_limits(Bytes::from_static(b"a"), Bytes::from_static(b"bc")); + let waits = api::Limits { + low: std::time::Duration::from_millis(5), + high: std::time::Duration::from_secs(2), + }; + println!( + "limits {} {} breadth {}", + String::from_utf8_lossy(&limits.low), + String::from_utf8_lossy(&limits.high), + api::breadth(waits).as_millis() + ); + let parts = [Bytes::from_static(b"ab"), Bytes::new(), Bytes::from_static(b"c")]; + println!( + "joined {} {} read {}", + String::from_utf8_lossy(&api::joined(&parts)), + api::joined(&[]).len(), + api::read_len(&b"hello"[..]) + ); let doc: serde_json::Value = r#"{"b": [1, 2.5, null], "a": "\u00e9"}"#.parse().expect("a JSON document"); println!("json {}", api::encode(&doc)); println!("timed text {}", api::timed_text(&doc)); diff --git a/ffi/fixtures/api/ffi.toml b/ffi/fixtures/api/ffi.toml index 187ab2526..6a277fe3d 100644 --- a/ffi/fixtures/api/ffi.toml +++ b/ffi/fixtures/api/ffi.toml @@ -1,23 +1,19 @@ # The list's tail is not `Clone`, so its getter has no copy to return, one instance leaves -# out a method the others keep, `Seed` is generic, so no callback implements it, `Payload` -# and `Kept` consume themselves, which no callback can, `Timed`, `Sample`, `Queued`, -# `Tick`, `Tock`, `Scoped`, `Rooted`, `Copied`, `Stored`, and `Layer` only convert, so -# their parameters take what converts into them, and +# out a method the others keep, `Timed`, `Sample`, `Queued`, `Tick`, `Tock`, `Scoped`, +# `Rooted`, `Copied`, `Stored`, and `Layer` only convert, so their parameters take what +# converts into them, and # `field@` leaves out a field alone where a method takes its path, as `Retry`'s and # `Slot`'s do. `Slot` has no such method, so its path alone names its field. A # function instance is left out by its path, as `depth_past` at `polar::Point` is. exclude = [ "Copied", - "Kept", "Layer", "Node::next", "Pair::swap", - "Payload", "Queued", "Rooted", "Sample", "Scoped", - "Seed", "Slot::value", "field@Retry::delay", "field@Slot::value", diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 6c7843334..7993fd220 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -1339,6 +1339,24 @@ pub fn keep(kept: impl Kept) -> i32 { kept.kept() } +/// `kept`'s, or 0 without one: `K` exports at each type implementing `Kept`, and the +/// parameter naming it at the option of each. +pub fn kept_or(kept: impl Into>) -> i32 { + kept.into().map_or(0, Kept::kept) +} + +/// Implemented for what `Kept` is, as moq's `Boxable` is for each `Session`: a blanket impl, +/// which C cannot implement apart, so a parameter it bounds exports at each type implementing +/// what the impl requires. +pub trait Keeper: Kept + Copy {} + +impl Keeper for K {} + +/// As `keep`, through the blanket impl. +pub fn keeper(keeper: impl Keeper) -> i32 { + keeper.kept() +} + /// A generic record only a constant names, which infers its instance. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Boxed { @@ -1644,6 +1662,34 @@ pub fn summed(points: impl IntoIterator) -> Point { points.into_iter().fold(Point::default(), |sum, p| sum.offset(p.x, p.y)) } +/// Bounds on a measure, as moq's `health::Limits` is: a generic record whose +/// instances are of types that map by role. +#[derive(Clone, Debug, PartialEq)] +pub struct Limits { + pub low: T, + pub high: T, +} + +/// How far apart `limits` are: an instance at `Duration`, passed in. +pub fn breadth(limits: Limits) -> Duration { + limits.high.saturating_sub(limits.low) +} + +/// `low` and `high` as limits: an instance at `Bytes`, returned. +pub fn byte_limits(low: Bytes, high: Bytes) -> Limits { + Limits { low, high } +} + +/// The parts joined, from anything that lends them for the call: a slice C lends. +pub fn joined<'a, I: IntoIterator>(parts: I) -> Bytes { + parts.into_iter().flat_map(|part| part.iter().copied()).collect() +} + +/// How many bytes `reader` holds: bytes C lends for the call. +pub fn read_len(mut reader: impl std::io::Read) -> u64 { + std::io::copy(&mut reader, &mut std::io::sink()).expect("a slice reads") +} + /// The length of the longest word, naming its iterator. pub fn longest(words: I) -> usize where diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index 1dfe156be..e57381ba6 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -58,23 +58,24 @@ fixtures/reject/src/lib.rs:492:1: rust2-ffi: cannot export fn `opaques`: borrowe fixtures/reject/src/lib.rs:497:1: rust2-ffi: cannot export fn `tried`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:49:1: rust2-ffi: cannot export macro `noop`: macro; exclude it in the config to leave it out fixtures/reject/src/lib.rs:502:1: rust2-ffi: cannot export fn `eventually`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:507:1: rust2-ffi: cannot export trait `Source`: callback signature `items`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:512:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:517:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:534:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:551:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:574:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:579:1: rust2-ffi: cannot export struct `Token`: object no export returns; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:512:1: rust2-ffi: cannot export fn `drain`: trait object `dyn Source`; exclude it in the config to leave it out + fixtures/reject/src/lib.rs:507:1: note: C cannot implement `Source`: callback signature `items` +fixtures/reject/src/lib.rs:517:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:522:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:539:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:556:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:579:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:57:1: rust2-ffi: cannot export fn `excluded`: excluded type `Excluded`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:588:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:593:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:600:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:605:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:610:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:630:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:637:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:661:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:679:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:584:1: rust2-ffi: cannot export struct `Token`: object no export returns; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:593:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:598:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:605:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:610:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:615:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:635:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:642:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:666:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:684:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:68:2: rust2-ffi: fn `reject::audio::encoder_finish` and fn `reject::audio_encoder::finish` are both `reject_audio_encoder_finish` in C fixtures/reject/src/lib.rs:63:2: note: fn `reject::audio_encoder::finish` is here fixtures/reject/src/lib.rs:6:1: rust2-ffi: cannot export struct `Wrap`: generic type; exclude it in the config to leave it out diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index 5b4c72a44..fda353eb6 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -503,11 +503,16 @@ pub async fn eventually() -> std::vec::IntoIter { Vec::new().into_iter() } -/// A callback returning an iterator, which C would have to make. +/// A callback returning an iterator, which C would have to make, so C cannot implement it. pub trait Source: Send + Sync { fn items(&self) -> std::vec::IntoIter; } +/// How many items `source` has: a trait object C cannot make. +pub fn drain(source: Box) -> usize { + source.items().count() +} + /// One byte forever. pub fn ones() -> std::iter::Repeat { std::iter::repeat(1) diff --git a/ffi/src/census.rs b/ffi/src/census.rs index cffbf27d9..d954b9784 100644 --- a/ffi/src/census.rs +++ b/ffi/src/census.rs @@ -51,16 +51,17 @@ pub fn render(api: &Api, collected: &Report, clashes: &[Clash], at: &dyn Fn(Span methods, refused(&["method", "field"]), ); - row("Traits", callbacks, refused(&["trait"])); + row("Traits", callbacks, 0); row("Constants", constants, refused(&["const"])); row("Other items", 0, refused(&["use", "macro", "static", "item"])); let _ = writeln!( out, - "\nExported types: {} records, {} enums, {} objects. A refused type's methods are not counted, nor are the {} `poll_*` methods left out for their async twins.", + "\nExported types: {} records, {} enums, {} objects. A refused type's methods are not counted, nor are the {} `poll_*` methods left out for their async twins, or the {} traits left out since C cannot implement them.", records.len(), enums.len(), objects.len(), - collected.twins.len() + collected.twins.len(), + collected.traits.len() ); // Each item counts once per construct, however many times it hits it. @@ -159,6 +160,15 @@ pub fn render(api: &Api, collected: &Report, clashes: &[Clash], at: &dyn Fn(Span let _ = writeln!(out, "- `{path}`: {} `{}`", why.construct, why.detail); } } + if !collected.traits.is_empty() { + let _ = writeln!(out, "\n## Traits left out, since C cannot implement them\n"); + for (path, why) in &collected.traits { + let _ = match why.detail.is_empty() { + true => writeln!(out, "- `{path}`: {}", why.construct), + false => writeln!(out, "- `{path}`: {} `{}`", why.construct, why.detail), + }; + } + } if !collected.twins.is_empty() { let _ = writeln!( out, diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index f420b69df..1804b6196 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -4,7 +4,8 @@ //! crates too, is exported by fixed rules, or refused with its span. A type it does not //! reach is refused. Nothing is opted in by attribute and nothing is skipped silently: the //! config's `exclude` is the only way to leave an item out, and `#[doc(hidden)]` items, -//! which are no part of the documented API, are listed in the census. A generic type +//! which are no part of the documented API, and traits C cannot implement, which C never +//! passes, are listed in the census. A generic type //! exports at the instances it infers, before any type is mapped: its defaults, each //! instance an inherent impl is of, its exports' implementations of the traits bounding it, //! and each instance a signature names at any choice its bounds allow; and only there. @@ -90,6 +91,8 @@ pub struct Report { pub refusals: Vec, /// Records and enums exported as objects, since a field does not map: the first. pub opaque: Vec<(String, Why)>, + /// Traits no foreign implementation can implement, left out: the first reason. + pub traits: Vec<(String, Why)>, /// `poll_x` methods that do not map, left out since `x` exports as a future. pub twins: Vec, /// `#[doc(hidden)]` items, by path. @@ -254,6 +257,7 @@ pub fn collect<'tcx>(tcx: TyCtxt<'tcx>, facade: &Facade<'_>) -> Collected { root: facade.root.clone(), refusals: Vec::new(), opaque: Vec::new(), + traits: Vec::new(), twins: Vec::new(), hidden: Vec::new(), excluded: facade.exclude.iter().map(|path| (path.clone(), 0)).collect(), @@ -706,7 +710,8 @@ impl<'tcx> Collector<'tcx> { } /// The first type argument an instance is named by that cannot map as [`Self::arg`] - /// requires, which is `()`, a primitive, `String`, a foreign object, or an export's type. + /// requires, which is `()`, a primitive, `String`, `Bytes`, `Duration`, a foreign object, + /// or an export's type. fn unarguable(&self, site: &Site<'tcx>) -> Option> { let tcx = self.tcx; site.args.types().take(site.explicit).find(|&ty| match ty.kind() { @@ -715,6 +720,8 @@ impl<'tcx> Collector<'tcx> { ty::Int(_) | ty::Uint(_) => int(ty).is_none(), ty::Adt(adt, _) => { tcx.lang_items().string() != Some(adt.did()) + && self.bytes != Some(adt.did()) + && !tcx.is_diagnostic_item(rustc_span::Symbol::intern("Duration"), adt.did()) && !self.exported(adt.did().krate) && !self.foreign.iter().any(|&(ty, _)| ty == adt.did()) } @@ -777,19 +784,33 @@ impl<'tcx> Collector<'tcx> { /// The types the exports implement a trait bounding `param` of `def_id` for, one of /// the exports': each impl's in an export, whose type takes no type parameters and is - /// `()`, a primitive, `String`, or an exported type. + /// `()`, a primitive, `String`, or an exported type. A blanket impl, such as + /// `impl Boxable for S`, stands for the types implementing what it bounds + /// its type by, which the caller checks the bounds of. fn implementors(&self, def_id: DefId, param: Ty<'tcx>) -> Vec> { let tcx = self.tcx; + let bounding = |def_id: DefId, param: Ty<'tcx>| { + (tcx.clauses_of(def_id).instantiate_identity(tcx).clauses.iter()) + .filter_map(|clause| clause.skip_norm_wip().as_trait_clause()) + .filter(move |bound| bound.self_ty().skip_binder() == param && self.exported(bound.def_id().krate)) + .map(|bound| bound.def_id()) + .collect::>() + }; + let mut traits = bounding(def_id, param); let mut found = Vec::new(); - for clause in tcx.clauses_of(def_id).instantiate_identity(tcx).clauses { - let Some(bound) = clause.skip_norm_wip().as_trait_clause() else { - continue; - }; - if bound.self_ty().skip_binder() != param || !self.exported(bound.def_id().krate) { - continue; - } - for imp in tcx.all_impls(bound.def_id()) { + let mut i = 0; + while let Some(&tr) = traits.get(i) { + i += 1; + for imp in tcx.all_impls(tr) { let ty = tcx.type_of(imp).instantiate_identity().skip_norm_wip(); + if let ty::Param(_) = ty.kind() { + for tr in bounding(imp, ty) { + if !traits.contains(&tr) { + traits.push(tr); + } + } + continue; + } let inner = ty.peel_refs(); let named = inner.is_primitive() || inner.is_unit() @@ -1258,6 +1279,8 @@ impl<'tcx> Collector<'tcx> { | Type::F64 | Type::Char | Type::String + | Type::Bytes + | Type::Duration | Type::Record(_) | Type::Enum(_) | Type::Object(_) => Ok(arg), @@ -1475,10 +1498,16 @@ impl<'tcx> Collector<'tcx> { } for site in self.sites(def_id, &path) { let decl = self.decls.get(&site.key).cloned(); + // A trait exports only as what C implements: one it cannot implement is no + // part of C's API, while a signature that takes one from C is refused. + if let (Some(Decl::Refused(why)), DefKind::Trait) = (&decl, kind) { + let (report, rest) = self.report(&site.shown); + report.traits.push((rest, why.clone())); + continue; + } if let Some(Decl::Refused(why)) = decl { let kind = match kind { _ if site.key.args.is_some() => "instance", - DefKind::Trait => "trait", DefKind::Enum => "enum", DefKind::Union => "union", _ => "struct", @@ -2115,26 +2144,47 @@ impl<'tcx> Collector<'tcx> { } let base = args.clone(); let mut kept = Vec::new(); - let mut several = Vec::new(); + // Each choice of the parameters so far, with the types that name it. + let mut choices = vec![(args, Vec::new())]; for (i, arg) in identity.iter().enumerate() { - match arg.kind() { - _ if i < count => {} - GenericArgKind::Lifetime(_) => {} + let param = match arg.kind() { + _ if i < count => continue, + GenericArgKind::Lifetime(_) => continue, // The first refusal ends it, before any later parameter's candidates are mapped. GenericArgKind::Const(_) => return Err(vec![why("const generic", "")]), - GenericArgKind::Type(param) if self.lent(def_id, param) => kept.push(param), - // Its bounds see each earlier parameter chosen, as `impl Into>` names `T`. - GenericArgKind::Type(param) => match self.choose(def_id, param, &args) { + GenericArgKind::Type(param) if self.lent(def_id, param) => { + kept.push(param); + continue; + } + GenericArgKind::Type(param) => param, + }; + let mut next = Vec::new(); + let mut refused = None; + for (args, named) in choices { + // Its bounds see each earlier parameter as this choice has it, as + // `impl Into>` names `T`, and `impl Into>` the `R` chosen. + let each = match self.choose(def_id, param, &args) { // Typing takes a later type only where an earlier one's export is refused. Ok(ranked) if self.inferring => { let last = ranked.iter().position(|&ty| !self.names_export(ty)); - several.push((i, ranked[..last.map_or(ranked.len(), |last| last + 1)].to_vec())); + ranked[..last.map_or(ranked.len(), |last| last + 1)].to_vec() + } + Ok(ranked) => { + let mut args = args; + args[i] = ranked[0].into(); + next.push((args, named)); + continue; } - Ok(ranked) => args[i] = ranked[0].into(), Err(Why { construct: "callback bound", .. - }) if self.taken(def_id, param).is_some() => kept.push(param), + }) if self.taken(def_id, param).is_some() => { + if !kept.contains(¶m) { + kept.push(param); + } + next.push((args, named)); + continue; + } // A conversion refused for its chains takes no other type in its place. Err( why @ Why { @@ -2149,24 +2199,23 @@ impl<'tcx> Collector<'tcx> { each.push(param); } if each.is_empty() { - return Err(vec![why]); + refused.get_or_insert(why); } - several.push((i, each)); + each } - }, + }; + for ty in each { + let mut args = args.clone(); + args[i] = ty.into(); + next.push((args, [&named[..], &[ty]].concat())); + } } - } - let mut choices = vec![(args, Vec::new())]; - for (i, each) in several { - choices = (choices.into_iter()) - .flat_map(|(args, named): (Vec>, Vec>)| { - each.iter().map(move |&ty| { - let mut args = args.clone(); - args[i] = ty.into(); - (args, [&named[..], &[ty]].concat()) - }) - }) - .collect(); + // A choice no type of this parameter meets is dropped; with none left, the first + // refusal ends it. + if let (true, Some(why)) = (next.is_empty(), refused) { + return Err(vec![why]); + } + choices = next; } let choices: Vec<_> = (choices.into_iter()) .map(|(args, named)| (tcx.mk_args(&args), named)) @@ -2218,7 +2267,8 @@ impl<'tcx> Collector<'tcx> { /// The types a type parameter may be exported as, best first, which typing takes the /// first of: `X` for each `Into` or `AsRef` its bounds imply, or `&X` and then /// `String` or `Vec` where `X` is `str` or `[T]`, `Vec` for - /// `impl IntoIterator`, `serde_json::Value` for a serde bound, then `String`, + /// `impl IntoIterator`, or `&[Y]` where `X` is `&Y`, `&[u8]` for + /// `impl io::Read`, `serde_json::Value` for a serde bound, then `String`, /// `Bytes`, and `Vec`, each that meets its bounds and maps. fn choose(&self, def_id: DefId, param: Ty<'tcx>, base: &[ty::GenericArg<'tcx>]) -> Result>, Why> { let tcx = self.tcx; @@ -2270,8 +2320,18 @@ impl<'tcx> Collector<'tcx> { } else if Some(tr) == self.range_bounds { candidates.extend(self.bounds(target()).map(|ty| (ty, None))); } else if tcx.is_diagnostic_item(sym::IntoIterator, tr) { - let items = self.items(&identity.clauses, param).and_then(|item| self.vec_of(item)); + // Items borrowed for the call iterate a slice C lends. + let items = self.items(&identity.clauses, param).and_then(|item| match item.kind() { + ty::Ref(_, elem, ty::Mutability::Not) if !outlives => { + Some(Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, Ty::new_slice(tcx, *elem))) + } + _ => self.vec_of(item), + }); candidates.extend(items.map(|ty| (ty, None))); + } else if tcx.is_diagnostic_item(rustc_span::Symbol::intern("IoRead"), tr) && !outlives { + // What it reads, which C lends for the call. + let bytes = Ty::new_slice(tcx, tcx.types.u8); + candidates.push((Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, bytes), None)); } else if serde(tcx, tr) { candidates.extend(self.json().map(|ty| (ty, None))); } @@ -3496,10 +3556,21 @@ impl<'tcx> Collector<'tcx> { /// A trait object of one of the crate's traits, as a foreign implementation. /// A foreign implementation crosses only into Rust, as a parameter. fn dynamic(&self, principal: Option, form: Dyn, ty: Ty<'tcx>, pos: Pos) -> Result { - match principal.and_then(|tr| self.trait_decl(tr)) { - Some(Decl::Callback(i)) if pos == Pos::Param => Ok(Type::Callback(*i, form)), - _ => Err(why("trait object", ty.to_string())), + let mut refusal = why("trait object", ty.to_string()); + match principal.map(|tr| (tr, self.trait_decl(tr))) { + Some((_, Some(Decl::Callback(i)))) if pos == Pos::Param => return Ok(Type::Callback(*i, form)), + // Left out of the export, so the reason it is shows where a signature needs it. + Some((tr, Some(Decl::Refused(left)))) => { + let reason = match left.detail.is_empty() { + true => left.construct.to_string(), + false => format!("{} `{}`", left.construct, left.detail), + }; + let note = format!("C cannot implement `{}`: {reason}", self.tcx.def_path_str(tr)); + refusal.note = Some((self.tcx.def_span(tr), note)); + } + _ => {} } + Err(refusal) } } diff --git a/ffi/src/ir.rs b/ffi/src/ir.rs index d85445324..5623cf802 100644 --- a/ffi/src/ir.rs +++ b/ffi/src/ir.rs @@ -42,6 +42,8 @@ impl Api { Type::F64 => "f64".to_string(), Type::Char => "char".to_string(), Type::String => "String".to_string(), + Type::Bytes => "bytes::Bytes".to_string(), + Type::Duration => "std::time::Duration".to_string(), Type::Record(i) => self.rust(&self.records[*i].path, &self.records[*i].args), Type::Enum(i) => self.rust(&self.enums[*i].path, &self.enums[*i].args), Type::Object(i) => self.rust(&self.objects[*i].path, &self.objects[*i].args), diff --git a/ffi/tests/api.c b/ffi/tests/api.c index aab6e5855..86cf0f4b9 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -636,6 +636,9 @@ static int script(void) { /* A bound that outlives the call takes a value, and an instance only a constant names. */ printf("kept %llu boxed %u\n", (unsigned long long)api_kept_Point((api_Point){.x = 1, .y = -2}), api_BOXED().value); printf("keep %d\n", api_keep_Point((api_Point){.x = 1, .y = -2})); + api_Point five = {.x = 5, .y = 2}; + printf("kept or %d %d keeper %d\n", api_kept_or_Point((api_option_Point){.some = true, .value = five}), + api_kept_or_Point((api_option_Point){0}), api_keeper_Point(five)); /* Inferred instances taking a lent closure and a foreign implementation, and one only * its impl of a trait names. */ api_Pair_u32 sides = {.left = 2, .right = 5}; @@ -819,6 +822,16 @@ static int script(void) { size_t most = api_longest((api_vec_string){.ptr = longest_words, .len = 2}); size_t no_longest = api_longest((api_vec_string){.ptr = longest_words, .len = 0}); printf("longest %zu %zu\n", most, no_longest); + api_Limits_bytes limits = api_byte_limits(str("a"), str("bc")); + api_duration breadth = api_breadth((api_Limits_duration){.low = {.secs = 0, .nanos = 5000000}, .high = {.secs = 2, .nanos = 0}}); + printf("limits %.*s %.*s breadth %llu\n", (int)limits.low.len, (const char *)limits.low.ptr, (int)limits.high.len, + (const char *)limits.high.ptr, (unsigned long long)(breadth.secs * 1000 + breadth.nanos / 1000000)); + api_Limits_bytes_free(&limits); + api_buf parts[] = {str("ab"), str(""), str("c")}; + print("joined ", api_joined((api_vec_bytes){.ptr = parts, .len = 3}), " "); + api_buf no_parts = api_joined((api_vec_bytes){.ptr = parts, .len = 0}); + printf("%zu read %llu\n", no_parts.len, (unsigned long long)api_read_len(str("hello"))); + api_buf_free(no_parts); /* JSON crosses as a document, made from text and read back as text. */ api_Json *doc; diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index 4871c1137..7998badb8 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -534,6 +534,8 @@ int script() { // A bound that outlives the call takes a value, and an instance only a constant names. std::printf("kept %llu boxed %u\n", ull(api::kept_Point({1, -2})), api::BOXED().value); std::printf("keep %d\n", api::keep_Point({1, -2})); + std::printf("kept or %d %d keeper %d\n", api::kept_or_Point(api::Point{5, 2}), api::kept_or_Point(std::nullopt), + api::keeper_Point({5, 2})); // Inferred instances taking a lent closure and a foreign implementation, and one only // its impl of a trait names. api::Pair_u32 sides{2, 5}; @@ -667,6 +669,13 @@ int script() { auto none_summed = api::summed({}); std::printf("summed %d %d %d %d\n", summed.x, summed.y, none_summed.x, none_summed.y); std::printf("longest %zu %zu\n", api::longest({"a", "abc"}), api::longest({})); + auto limits = api::byte_limits("a", "bc"); + print("limits ", limits.low, " "); + print("", limits.high, " "); + auto breadth = api::breadth({{0, 5000000}, {2, 0}}); + std::printf("breadth %llu\n", static_cast(breadth.secs * 1000 + breadth.nanos / 1000000)); + print("joined ", api::joined({"ab", "", "c"}), " "); + std::printf("%zu read %llu\n", api::joined({}).size(), static_cast(api::read_len("hello"))); api::Json doc = api::Json::parse(R"({"b": [1, 2.5, null], "a": "\u00e9"})").value(); print("json ", api::encode(doc), "\n"); diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index 114294f25..b48fa5d1c 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -383,6 +383,8 @@ Future script() async { // A bound that outlives the call takes a value, and an instance only a constant names. print('kept ${api.keptPoint(const api.Point(x: 1, y: -2))} boxed ${api.BOXED.value}'); print('keep ${api.keepPoint(const api.Point(x: 1, y: -2))}'); + const five = api.Point(x: 5, y: 2); + print('kept or ${api.keptOrPoint(five)} ${api.keptOrPoint(null)} keeper ${api.keeperPoint(five)}'); // Inferred instances taking a lent closure and a foreign implementation, and one only // its impl of a trait names. const sides = api.PairU32(left: 2, right: 5); @@ -456,6 +458,11 @@ Future script() async { final noneSummed = api.summed([]); print('summed ${summed.x} ${summed.y} ${noneSummed.x} ${noneSummed.y}'); print('longest ${api.longest(['a', 'abc'])} ${api.longest([])}'); + final limits = api.byteLimits(utf8.encode('a'), utf8.encode('bc')); + final waits = api.breadth(const api.LimitsDuration(low: Duration(milliseconds: 5), high: Duration(seconds: 2))); + print('limits ${utf8.decode(limits.low)} ${utf8.decode(limits.high)} breadth ${waits.inMilliseconds}'); + final parts = utf8.decode(api.joined([utf8.encode('ab'), Uint8List(0), utf8.encode('c')])); + print('joined $parts ${api.joined([]).length} read ${api.readLen(utf8.encode('hello'))}'); final doc = api.Json.parse('{"b": [1, 2.5, null], "a": "é"}'); print('json ${api.encode(doc)}'); print('timed text ${api.timedText(doc)}'); diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 6f4a54b48..640f51716 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -443,6 +443,8 @@ func script() { // A bound that outlives the call takes a value, and an instance only a constant names. fmt.Printf("kept %d boxed %d\n", api.KeptPoint(api.Point{X: 1, Y: -2}), api.BOXED().Value) fmt.Printf("keep %d\n", api.KeepPoint(api.Point{X: 1, Y: -2})) + five := api.Point{X: 5, Y: 2} + fmt.Printf("kept or %d %d keeper %d\n", api.KeptOrPoint(&five), api.KeptOrPoint(nil), api.KeeperPoint(five)) // Inferred instances taking a lent closure and a foreign implementation, and one only // its impl of a trait names. sides := api.PairU32{Left: 2, Right: 5} @@ -535,6 +537,11 @@ func script() { none := api.Summed(nil) fmt.Printf("summed %d %d %d %d\n", sum.X, sum.Y, none.X, none.Y) fmt.Printf("longest %d %d\n", api.Longest([]string{"a", "abc"}), api.Longest(nil)) + limits := api.ByteLimits([]byte("a"), []byte("bc")) + breadth := api.Breadth(api.LimitsDuration{Low: 5 * time.Millisecond, High: 2 * time.Second}) + fmt.Printf("limits %s %s breadth %d\n", limits.Low.Copy(), limits.High.Copy(), breadth.Milliseconds()) + parts := []rt.Bytes{rt.NewBytes([]byte("ab")), rt.NewBytes(nil), rt.NewBytes([]byte("c"))} + fmt.Printf("joined %s %d read %d\n", api.Joined(parts).Copy(), len(api.Joined(nil).Copy()), api.ReadLen([]byte("hello"))) doc := must(serdejson.JsonParse(`{"b": [1, 2.5, null], "a": "é"}`)) fmt.Printf("json %s\n", must(api.Encode(doc))) fmt.Printf("timed text %s\n", must(api.TimedText(doc))) diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index 196ef96d8..5ee4be102 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -8,6 +8,7 @@ import api.* import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Executor import kotlin.concurrent.thread +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -307,6 +308,8 @@ fun script() { // A bound that outlives the call takes a value, and an instance only a constant names. println("kept ${keptPoint(Point(1, -2))} boxed ${BOXED.value}") println("keep ${keepPoint(Point(1, -2))}") + val five = Point(5, 2) + println("kept or ${keptOrPoint(five)} ${keptOrPoint(null)} keeper ${keeperPoint(five)}") // Inferred instances taking a lent closure and a foreign implementation, and one only // its impl of a trait names. val sides = PairU32(2u, 5u) @@ -368,6 +371,11 @@ fun script() { val noneSummed = summed(emptyList()) println("summed ${summed.x} ${summed.y} ${noneSummed.x} ${noneSummed.y}") println("longest ${longest(listOf("a", "abc"))} ${longest(emptyList())}") + val limits = byteLimits("a".encodeToByteArray(), "bc".encodeToByteArray()) + val waits = breadth(LimitsDuration(5.milliseconds, 2.seconds)) + println("limits ${limits.low.toByteArray().decodeToString()} ${limits.high.toByteArray().decodeToString()} breadth ${waits.inWholeMilliseconds}") + val parts = joined(listOf("ab".encodeToByteArray(), ByteArray(0), "c".encodeToByteArray())).toByteArray().decodeToString() + println("joined $parts ${joined(emptyList()).toByteArray().size} read ${readLen("hello".encodeToByteArray())}") val doc = Json.parse("""{"b": [1, 2.5, null], "a": "\u00e9"}""") println("json ${encode(doc)}") println("timed text ${timedText(doc)}") diff --git a/ffi/tests/api.py b/ffi/tests/api.py index adbd5e5c6..ea5844434 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -395,6 +395,8 @@ async def block(): # A bound that outlives the call takes a value, and an instance only a constant names. print(f"kept {api.kept_point(api.Point(1, -2))} boxed {api.BOXED.value}") print(f"keep {api.keep_point(api.Point(1, -2))}") + five = api.Point(5, 2) + print(f"kept or {api.kept_or_point(five)} {api.kept_or_point(None)} keeper {api.keeper_point(five)}") # Inferred instances taking a lent closure and a foreign implementation, and one only # its impl of a trait names. sides = api.PairU32(2, 5) @@ -461,6 +463,12 @@ async def block(): none_summed = api.summed([]) print(f"summed {summed.x} {summed.y} {none_summed.x} {none_summed.y}") print(f"longest {api.longest(['a', 'abc'])} {api.longest([])}") + limits = api.byte_limits(b'a', b'bc') + waits = api.LimitsDuration(datetime.timedelta(milliseconds=5), datetime.timedelta(seconds=2)) + breadth = api.breadth(waits) // datetime.timedelta(milliseconds=1) + print(f"limits {bytes(limits.low).decode()} {bytes(limits.high).decode()} breadth {breadth}") + joined = bytes(api.joined([b'ab', b'', b'c'])).decode() + print(f"joined {joined} {len(api.joined([]))} read {api.read_len(b'hello')}") doc = api.Json.parse('{"b": [1, 2.5, null], "a": "é"}') print(f"json {api.encode(doc)}") print(f"timed text {api.timed_text(doc)}") diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index 396368d0d..02cb515c8 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -432,6 +432,8 @@ func script() { // A bound that outlives the call takes a value, and an instance only a constant names. print("kept \(keptPoint(Point(x: 1, y: -2))) boxed \(BOXED.value)") print("keep \(keepPoint(Point(x: 1, y: -2)))") + let five = Point(x: 5, y: 2) + print("kept or \(keptOrPoint(five)) \(keptOrPoint(nil)) keeper \(keeperPoint(five))") // Inferred instances taking a lent closure and a foreign implementation, and one only // its impl of a trait names. let sides = PairU32(left: 2, right: 5) @@ -496,6 +498,13 @@ func script() { let noneSum = summed([]) print("summed \(sum.x) \(sum.y) \(noneSum.x) \(noneSum.y)") print("longest \(longest(["a", "abc"])) \(longest([]))") + let limits = byteLimits(Data("a".utf8), Data("bc".utf8)) + let waits = breadth(LimitsDuration(low: .milliseconds(5), high: .seconds(2))) + let low = String(decoding: limits.low, as: UTF8.self) + let high = String(decoding: limits.high, as: UTF8.self) + print("limits \(low) \(high) breadth \(waits.components.seconds * 1000 + waits.components.attoseconds / 1_000_000_000_000_000)") + let parts = String(decoding: Api.joined([Data("ab".utf8), Data(), Data("c".utf8)]), as: UTF8.self) + print("joined \(parts) \(Api.joined([]).count) read \(readLen(Data("hello".utf8)))") let doc = try! Json.parse(#"{"b": [1, 2.5, null], "a": "é"}"#) print("json \(encode(doc))") print("timed text \(timedText(doc))") From 13721aa6a70ac6f879b746b815bf03597cf1898a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:32:18 -0800 Subject: [PATCH 3/9] quest: rewrite generics to the futures and kept closures that remain Co-Authored-By: Claude Opus 5.5 --- quest/m1/ffi-export/refusals/README.md | 2 +- quest/m1/ffi-export/refusals/generics.md | 47 +++++++++++++++++------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/quest/m1/ffi-export/refusals/README.md b/quest/m1/ffi-export/refusals/README.md index 20faf940f..8431d7936 100644 --- a/quest/m1/ffi-export/refusals/README.md +++ b/quest/m1/ffi-export/refusals/README.md @@ -56,7 +56,7 @@ Public API: the exporter's command line and the C API of every item that exports - [The stand-in exports only moq-tokio's session side](/quest/m1/ffi-export/refusals/narrow-standin.md) - `just ffi standin` names the session items moq-ffi ships in place of `pub use ::moq_tokio::*;` - [Whole-crate re-exports are refused with one span](/quest/m1/ffi-export/refusals/reexports.md) - the exporter stops at a `pub use` of a crate root, turning about 790 refusals into one per re-export -- [Generic parameters and traits export](/quest/m1/ffi-export/refusals/generics.md) - `Client::connect`'s `Boxable`, futures and readers as parameters, trait objects, associated types, and callbacks +- [Futures and kept closures as parameters export](/quest/m1/ffi-export/refusals/generics.md) - `clock::timeout`'s future, `clock::drive`'s kept closure, and `mp4::repair`'s seekable reader cross from C - [Standard and common types export by role](/quest/m1/ffi-export/refusals/std-types.md) - maps, sets, errors, `NonZero`, addresses, instants, sockets, levels, JSON maps, and iterator adapters - [Poll and Context export](/quest/m1/ffi-export/refusals/poll.md) - a `poll_*` with no async twin takes a waker, as futures' polls do - [Borrow shapes export](/quest/m1/ffi-export/refusals/borrows.md) - `Option<&T>`, `&mut` buffers, `&Bytes`, `Arc`, `'static` borrows, and borrowed returns that are not `Clone` diff --git a/quest/m1/ffi-export/refusals/generics.md b/quest/m1/ffi-export/refusals/generics.md index b6c282fd5..b8e751ff9 100644 --- a/quest/m1/ffi-export/refusals/generics.md +++ b/quest/m1/ffi-export/refusals/generics.md @@ -1,21 +1,40 @@ -# [L] Generic parameters and traits export +# [L] Futures and kept closures as parameters export ## Goal -The census at the moq pin refuses no item of moq's own for a generic parameter, type -argument, trait object, supertrait, associated type, unmet bound, callback or closure -signature, or blanket impl: 51 items at moq `ffcb7cd9`. +The census at the moq pin refuses no item of moq's own for a future or a kept closure +taken as a parameter, or a reader it seeks: 11 items at moq `ffcb7cd9`, once +[narrowing](/quest/m1/ffi-export/refusals/narrow-standin.md) drops the moq-tokio items +outside its session side. + +- A future parameter: kio's `Waiter::poll_future`, `clock::timeout`, and + `clock::timeout_at`, and moq-tokio's `panic::isolate` and `server::Request::within`. +- A closure kept past the call: kio's `clock::drive` and `wait`, and `Waiter::batch`, whose + lent closure returns a type parameter. +- moq-mux's `container::mp4::repair`, which seeks a `&mut impl Read + Seek`. +- moq-tokio's `panic::message`, which takes a `&(dyn Any + Send)` panic payload, and + `tls::Serve::resolver`, which takes rustls's `Arc`. ## Plan -Ranked first by moq-ffi's hand-written wrapper: `Client::connect` and `Server::accept` -take `transport::poll::Boxable`, which moq-ffi's `session.rs` and `server.rs` wrap by hand. -Then `impl Future` and `IntoFuture` parameters (`clock::timeout`, `Waiter::poll_future`), -`io::Read` parameters, trait objects, associated types (`mux::catalog::Stream`, -`transport::poll::Session`), supertraits, `time::Driver`'s callback signature, and -`clock::drive`'s kept closure. +Generic parameters and traits already export: a parameter bounded by a blanket impl's trait, +as `Client::connect`'s `Boxable` is, exports at each type implementing what the impl requires; +a parameter whose bounds name an earlier one, as `track`'s `impl Into>` does, is +chosen at each choice of it; `impl io::Read` takes bytes and `impl IntoIterator` a +slice; `Bytes` and `Duration` are type arguments; and a trait C cannot implement is left out +and listed in the census. moq-mux's `codec::annexb::build_prefix` exports once moq stops +re-exporting `bytes` whole, as [moq-pin](/quest/m1/ffi-export/refusals/moq-pin.md) has it, +since the re-export makes `Bytes` a handle. + +## Waiting -A generic exports at the instances the facade names, as generic types already do; a trait a -language implements crosses as the vtable the exporter already gives callback traits. -Where a construct has no C shape, the census keeps refusing it with its span, and moq's -signature is what changes: report it to the maintainer rather than add config. +- Maintainer: how a future or a kept closure crosses from C. Options: (a) the exporter gains + a foreign future, a poll function, its data, and `free`, the inverse of the futures it + returns, and a kept closure, a function, its data, and `free`, each wrapped in every + language's native future and function, so moq changes nothing; (b) moq's signatures + change, such as the stand-in re-exporting only kio's `Instant` from `clock`, and kio's and + moq-tokio's runtime helpers leaving what the facade reaches. Recommendation: (a), since + each has a C shape and AGENTS.md counts a source change a consumer needs as a gap; `repair` + then takes a foreign `Read + Seek`, while `panic::message` and `tls::Serve::resolver`, which + name what C can never hold, go to moq to wrap or drop, as the epic decided for third-party + types. From 0aa14bfee54d1a1daa88803cb47b558c156245b2 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:18:51 -0800 Subject: [PATCH 4/9] fix(ffi): take a slice for borrowed iterator items only when borrowed for the call Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/reject/expected.txt | 32 +++++++++++++++++--------------- ffi/fixtures/reject/src/lib.rs | 10 ++++++++++ ffi/src/collect.rs | 17 +++++++++++++++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index e57381ba6..097e7b917 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -60,24 +60,26 @@ fixtures/reject/src/lib.rs:49:1: rust2-ffi: cannot export macro `noop`: macro; e fixtures/reject/src/lib.rs:502:1: rust2-ffi: cannot export fn `eventually`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:512:1: rust2-ffi: cannot export fn `drain`: trait object `dyn Source`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:507:1: note: C cannot implement `Source`: callback signature `items` -fixtures/reject/src/lib.rs:517:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:522:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:539:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:556:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:579:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:517:1: rust2-ffi: cannot export fn `summed_forever`: generic parameter `std::iter::IntoIterator`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:522:1: rust2-ffi: cannot export fn `first_of`: generic parameter `std::iter::IntoIterator`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:527:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:532:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:549:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:566:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:57:1: rust2-ffi: cannot export fn `excluded`: excluded type `Excluded`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:584:1: rust2-ffi: cannot export struct `Token`: object no export returns; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:593:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:598:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:605:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:610:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:615:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:635:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:642:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:666:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:684:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:589:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:594:1: rust2-ffi: cannot export struct `Token`: object no export returns; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:603:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:608:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:615:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:620:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:625:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:645:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:652:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:676:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:68:2: rust2-ffi: fn `reject::audio::encoder_finish` and fn `reject::audio_encoder::finish` are both `reject_audio_encoder_finish` in C fixtures/reject/src/lib.rs:63:2: note: fn `reject::audio_encoder::finish` is here +fixtures/reject/src/lib.rs:694:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:6:1: rust2-ffi: cannot export struct `Wrap`: generic type; exclude it in the config to leave it out fixtures/reject/src/lib.rs:72:1: rust2-ffi: cannot export fn `letters`: Range `char`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:81:2: rust2-ffi: cannot export method `Text::new`: 'static borrow `&'a str`; exclude it in the config to leave it out diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index fda353eb6..461601753 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -513,6 +513,16 @@ pub fn drain(source: Box) -> usize { source.items().count() } +/// The sum of items borrowed forever, which a slice C lends for the call cannot be. +pub fn summed_forever>(items: I) -> u32 { + items.into_iter().sum() +} + +/// The first of items whose borrow it returns, which outlives what C lends. +pub fn first_of<'a, I: IntoIterator>(items: I) -> Option<&'a String> { + items.into_iter().next() +} + /// One byte forever. pub fn ones() -> std::iter::Repeat { std::iter::repeat(1) diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index 1804b6196..b0271fe0f 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -2320,9 +2320,22 @@ impl<'tcx> Collector<'tcx> { } else if Some(tr) == self.range_bounds { candidates.extend(self.bounds(target()).map(|ty| (ty, None))); } else if tcx.is_diagnostic_item(sym::IntoIterator, tr) { - // Items borrowed for the call iterate a slice C lends. + // Items borrowed for the call iterate a slice C lends: borrowed for a lifetime of + // the function's own that nothing returned names and that outlives no other, as + // `'static` or `'a: 'b` would ask more than the call of C's slice. + let lent = |region: ty::Region<'tcx>| { + if !matches!(tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) { + return false; + } + let output = tcx.fn_sig(def_id).instantiate_identity().skip_norm_wip().skip_binder().output(); + matches!(region.kind(), ty::ReEarlyParam(_)) + && !output.walk().any(|arg| arg.as_region() == Some(region)) + && !(self.implied(def_id, base).iter()) + .filter_map(|clause| clause.as_region_outlives_clause()) + .any(|pred| pred.skip_binder().0 == region) + }; let items = self.items(&identity.clauses, param).and_then(|item| match item.kind() { - ty::Ref(_, elem, ty::Mutability::Not) if !outlives => { + ty::Ref(region, elem, ty::Mutability::Not) if !outlives && lent(*region) => { Some(Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, Ty::new_slice(tcx, *elem))) } _ => self.vec_of(item), From 5d89a6b18913d851fa69fa084e26d055fd2e8b3c Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:25:12 -0800 Subject: [PATCH 5/9] style(ffi): format --- ffi/src/collect.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index b0271fe0f..85e1284a0 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -2327,7 +2327,12 @@ impl<'tcx> Collector<'tcx> { if !matches!(tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) { return false; } - let output = tcx.fn_sig(def_id).instantiate_identity().skip_norm_wip().skip_binder().output(); + let output = tcx + .fn_sig(def_id) + .instantiate_identity() + .skip_norm_wip() + .skip_binder() + .output(); matches!(region.kind(), ty::ReEarlyParam(_)) && !output.walk().any(|arg| arg.as_region() == Some(region)) && !(self.implied(def_id, base).iter()) From d76e8e991475bbfc77b292cb9c665818ae480192 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:28:44 -0800 Subject: [PATCH 6/9] fix(ffi): refuse a slice for iterator items whose lifetime another parameter names Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/reject/expected.txt | 31 ++++++++++++++++--------------- ffi/fixtures/reject/src/lib.rs | 7 +++++++ ffi/src/collect.rs | 17 +++++++---------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index 097e7b917..8818dfce2 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -62,25 +62,26 @@ fixtures/reject/src/lib.rs:512:1: rust2-ffi: cannot export fn `drain`: trait obj fixtures/reject/src/lib.rs:507:1: note: C cannot implement `Source`: callback signature `items` fixtures/reject/src/lib.rs:517:1: rust2-ffi: cannot export fn `summed_forever`: generic parameter `std::iter::IntoIterator`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:522:1: rust2-ffi: cannot export fn `first_of`: generic parameter `std::iter::IntoIterator`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:527:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:532:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:549:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:566:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:527:1: rust2-ffi: cannot export fn `retext_last`: generic parameter `std::iter::IntoIterator`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:534:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:539:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:556:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:573:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:57:1: rust2-ffi: cannot export fn `excluded`: excluded type `Excluded`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:589:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:594:1: rust2-ffi: cannot export struct `Token`: object no export returns; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:603:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:608:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:615:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:620:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:625:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:645:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:652:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:676:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:596:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:601:1: rust2-ffi: cannot export struct `Token`: object no export returns; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:610:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:615:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:622:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:627:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:632:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:652:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:659:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:683:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:68:2: rust2-ffi: fn `reject::audio::encoder_finish` and fn `reject::audio_encoder::finish` are both `reject_audio_encoder_finish` in C fixtures/reject/src/lib.rs:63:2: note: fn `reject::audio_encoder::finish` is here -fixtures/reject/src/lib.rs:694:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:6:1: rust2-ffi: cannot export struct `Wrap`: generic type; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:701:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:72:1: rust2-ffi: cannot export fn `letters`: Range `char`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:81:2: rust2-ffi: cannot export method `Text::new`: 'static borrow `&'a str`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:79:6: note: `'a` is `'static`, as a handle outlives every call diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index 461601753..812014566 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -523,6 +523,13 @@ pub fn first_of<'a, I: IntoIterator>(items: I) -> Option<&'a items.into_iter().next() } +/// Items `text` could keep, as `retext` keeps its borrow. +pub fn retext_last<'a, I: IntoIterator>(text: &mut Text<'a>, items: I) { + if let Some(item) = items.into_iter().last() { + text.0 = item; + } +} + /// One byte forever. pub fn ones() -> std::iter::Repeat { std::iter::repeat(1) diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index 85e1284a0..dba96515d 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -2321,23 +2321,20 @@ impl<'tcx> Collector<'tcx> { candidates.extend(self.bounds(target()).map(|ty| (ty, None))); } else if tcx.is_diagnostic_item(sym::IntoIterator, tr) { // Items borrowed for the call iterate a slice C lends: borrowed for a lifetime of - // the function's own that nothing returned names and that outlives no other, as - // `'static` or `'a: 'b` would ask more than the call of C's slice. + // the function's own that no other parameter, return, or lifetime bound names, + // since any of them, as `'static`, a returned borrow, `'a: 'b`, or a handle's + // `Text<'a>` would, may ask more than the call of C's slice. let lent = |region: ty::Region<'tcx>| { if !matches!(tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) { return false; } - let output = tcx - .fn_sig(def_id) - .instantiate_identity() - .skip_norm_wip() - .skip_binder() - .output(); + let sig = tcx.fn_sig(def_id).instantiate_identity().skip_norm_wip().skip_binder(); + let named = |ty: Ty<'tcx>| ty.walk().any(|arg| arg.as_region() == Some(region)); matches!(region.kind(), ty::ReEarlyParam(_)) - && !output.walk().any(|arg| arg.as_region() == Some(region)) + && !sig.inputs_and_output.iter().any(named) && !(self.implied(def_id, base).iter()) .filter_map(|clause| clause.as_region_outlives_clause()) - .any(|pred| pred.skip_binder().0 == region) + .any(|pred| pred.skip_binder().0 == region || pred.skip_binder().1 == region) }; let items = self.items(&identity.clauses, param).and_then(|item| match item.kind() { ty::Ref(region, elem, ty::Mutability::Not) if !outlives && lent(*region) => { From 4fa8d47179f8c7b2677051950ab33fc0aff26666 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 22:13:17 -0800 Subject: [PATCH 7/9] fix(ffi): borrow an iterator's slice for its items' own lifetime `impl IntoIterator` takes `&'a [X]` rather than a slice whose borrow is erased, so the borrows analysis of the instantiated signature decides whether C's slice lives long enough, as it does for any `&'a` parameter. The hand-written `lent` guard goes, along with the cases it missed: a lifetime that a later-chosen handle parameter pins to `'static`. Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/reject/src/lib.rs | 16 ++++++++++++++++ ffi/src/collect.rs | 24 ++++++------------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index 51f929764..f9ca00380 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -518,6 +518,22 @@ pub fn retext_last<'a, I: IntoIterator>(text: &mut Text<'a>, } } +/// Items outliving `text`'s borrow, which is `'static` in a handle, so they are too. +pub fn outlast<'a, 'b: 'a, I: IntoIterator>(text: &mut Text<'a>, items: I) { + if let Some(item) = items.into_iter().last() { + text.0 = item; + } +} + +/// Items a text taken by value could keep, once its parameter is chosen as a handle. +pub fn retext_into<'a, T: Into>, I: IntoIterator>(text: T, items: I) -> usize { + let mut text = text.into(); + if let Some(item) = items.into_iter().last() { + text.0 = item; + } + text.0.len() +} + /// One byte forever. pub fn ones() -> std::iter::Repeat { std::iter::repeat(1) diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index e9e7cb5cd..5ddcd521a 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -2365,25 +2365,13 @@ impl<'tcx> Collector<'tcx> { } else if Some(tr) == self.range_bounds { candidates.extend(self.bounds(target()).map(|ty| (ty, None))); } else if tcx.is_diagnostic_item(sym::IntoIterator, tr) { - // Items borrowed for the call iterate a slice C lends: borrowed for a lifetime of - // the function's own that no other parameter, return, or lifetime bound names, - // since any of them, as `'static`, a returned borrow, `'a: 'b`, or a handle's - // `Text<'a>` would, may ask more than the call of C's slice. - let lent = |region: ty::Region<'tcx>| { - if !matches!(tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) { - return false; - } - let sig = tcx.fn_sig(def_id).instantiate_identity().skip_norm_wip().skip_binder(); - let named = |ty: Ty<'tcx>| ty.walk().any(|arg| arg.as_region() == Some(region)); - matches!(region.kind(), ty::ReEarlyParam(_)) - && !sig.inputs_and_output.iter().any(named) - && !(self.implied(def_id, base).iter()) - .filter_map(|clause| clause.as_region_outlives_clause()) - .any(|pred| pred.skip_binder().0 == region || pred.skip_binder().1 == region) - }; + // Borrowed items iterate a slice C lends, borrowed for the items' own lifetime, + // so the signature's borrows decide whether C's slice lives long enough. let items = self.items(&identity.clauses, param).and_then(|item| match item.kind() { - ty::Ref(region, elem, ty::Mutability::Not) if !outlives && lent(*region) => { - Some(Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, Ty::new_slice(tcx, *elem))) + ty::Ref(region, elem, ty::Mutability::Not) + if !outlives && !matches!(region.kind(), ty::ReBound(..)) => + { + Some(Ty::new_imm_ref(tcx, *region, Ty::new_slice(tcx, *elem))) } _ => self.vec_of(item), }); From c5a5c8f51ee59d86c2ba290c1ee6b5cd0d621cc4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 03:58:17 -0800 Subject: [PATCH 8/9] test(ffi): export first_of now that its borrow is the items' own With the slice borrowed for the items' lifetime, `first_of`'s returned borrow is copied out before the call ends, so it exports soundly: move it to the api fixture and test it in every language. Regenerate the reject fixture's expected refusals after merging main. Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/examples/transcript.rs | 2 ++ ffi/fixtures/api/src/lib.rs | 5 +++ ffi/fixtures/reject/expected.txt | 44 +++++++++++++++---------- ffi/fixtures/reject/src/lib.rs | 5 --- ffi/tests/api.c | 3 ++ ffi/tests/api.cpp | 2 ++ ffi/tests/api.dart | 1 + ffi/tests/api.go | 5 +++ ffi/tests/api.kt | 1 + ffi/tests/api.py | 1 + ffi/tests/api.swift | 1 + 11 files changed, 47 insertions(+), 23 deletions(-) diff --git a/ffi/fixtures/api/examples/transcript.rs b/ffi/fixtures/api/examples/transcript.rs index aed6af8be..973b451a1 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -491,6 +491,8 @@ fn main() { api::longest(["a".to_string(), "abc".to_string()]), api::longest([]) ); + let words = ["a".to_string(), "b".to_string()]; + println!("first of {:?} {:?}", api::first_of(&words), api::first_of(&[])); let limits = api::byte_limits(Bytes::from_static(b"a"), Bytes::from_static(b"bc")); let waits = api::Limits { low: std::time::Duration::from_millis(5), diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 3f8da41c7..52e7fd656 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -1724,6 +1724,11 @@ pub fn joined<'a, I: IntoIterator>(parts: I) -> Bytes { parts.into_iter().flat_map(|part| part.iter().copied()).collect() } +/// The first of `items`, a borrow of what C lends that is copied out before the call ends. +pub fn first_of<'a, I: IntoIterator>(items: I) -> Option<&'a String> { + items.into_iter().next() +} + /// How many bytes `reader` holds: bytes C lends for the call. pub fn read_len(mut reader: impl std::io::Read) -> u64 { std::io::copy(&mut reader, &mut std::io::sink()).expect("a slice reads") diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index 377409d79..9fdea5f6c 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -54,29 +54,37 @@ fixtures/reject/src/lib.rs:475:1: rust2-ffi: cannot export fn `naturals`: foreig fixtures/reject/src/lib.rs:480:1: rust2-ffi: cannot export fn `opaques`: borrowed return that is not Clone `Opaque`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:485:1: rust2-ffi: cannot export fn `tried`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:490:1: rust2-ffi: cannot export fn `eventually`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:495:1: rust2-ffi: cannot export trait `Source`: callback signature `items`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:500:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:505:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:522:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:539:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:500:1: rust2-ffi: cannot export fn `drain`: trait object `dyn Source`; exclude it in the config to leave it out + fixtures/reject/src/lib.rs:495:1: note: C cannot implement `Source`: callback signature `items` +fixtures/reject/src/lib.rs:505:1: rust2-ffi: cannot export fn `summed_forever`: 'static borrow `&'static [u32]`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:510:1: rust2-ffi: cannot export fn `retext_last`: 'static borrow `&'a [std::string::String]`; exclude it in the config to leave it out + fixtures/reject/src/lib.rs:510:20: note: `'a` is `'static`, as a handle outlives every call +fixtures/reject/src/lib.rs:517:1: rust2-ffi: cannot export fn `outlast`: 'static borrow `&'b [std::string::String]`; exclude it in the config to leave it out + fixtures/reject/src/lib.rs:517:20: note: `'b` is `'static`, as a handle outlives every call +fixtures/reject/src/lib.rs:524:1: rust2-ffi: cannot export fn `retext_into`: 'static borrow `&'a [std::string::String]`; exclude it in the config to leave it out + fixtures/reject/src/lib.rs:524:20: note: `'a` is `'static`, as a handle outlives every call +fixtures/reject/src/lib.rs:533:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:538:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:54:1: rust2-ffi: cannot export fn `excluded`: excluded type `Excluded`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:562:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:576:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:581:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:588:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:594:1: rust2-ffi: cannot export fn `floats`: collection not built from its items `std::collections::BTreeSet`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:599:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:604:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:624:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:631:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:655:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:555:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:572:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:595:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:609:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:614:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:621:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:627:1: rust2-ffi: cannot export fn `floats`: collection not built from its items `std::collections::BTreeSet`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:632:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:637:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:657:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:65:2: rust2-ffi: fn `reject::audio::encoder_finish` and fn `reject::audio_encoder::finish` are both `reject_audio_encoder_finish` in C fixtures/reject/src/lib.rs:60:2: note: fn `reject::audio_encoder::finish` is here -fixtures/reject/src/lib.rs:673:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:684:9: rust2-ffi: cannot export use `dep`: re-export of a whole crate `dep`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:687:9: rust2-ffi: cannot export use `renamed`: re-export of a whole crate `dep`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:664:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:688:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:69:1: rust2-ffi: cannot export fn `letters`: Range `char`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:6:1: rust2-ffi: cannot export fn `pair`: tuple of references `(&str, u8)`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:706:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:717:9: rust2-ffi: cannot export use `dep`: re-export of a whole crate `dep`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:720:9: rust2-ffi: cannot export use `renamed`: re-export of a whole crate `dep`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:78:2: rust2-ffi: cannot export method `Text::new`: 'static borrow `&'a str`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:76:6: note: `'a` is `'static`, as a handle outlives every call fixtures/reject/src/lib.rs:83:2: rust2-ffi: cannot export method `Text::view`: returned object that borrows `Text<'_>`; exclude it in the config to leave it out diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index f9ca00380..d8c17415a 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -506,11 +506,6 @@ pub fn summed_forever>(items: I) -> u32 { items.into_iter().sum() } -/// The first of items whose borrow it returns, which outlives what C lends. -pub fn first_of<'a, I: IntoIterator>(items: I) -> Option<&'a String> { - items.into_iter().next() -} - /// Items `text` could keep, as `retext` keeps its borrow. pub fn retext_last<'a, I: IntoIterator>(text: &mut Text<'a>, items: I) { if let Some(item) = items.into_iter().last() { diff --git a/ffi/tests/api.c b/ffi/tests/api.c index 6df1ec33a..f8baf9b99 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -857,6 +857,9 @@ static int script(void) { size_t most = api_longest((api_vec_string){.ptr = longest_words, .len = 2}); size_t no_longest = api_longest((api_vec_string){.ptr = longest_words, .len = 0}); printf("longest %zu %zu\n", most, no_longest); + api_buf no_first = api_first_of((api_vec_string){.ptr = longest_words, .len = 0}); + print("first of Some(\"", api_first_of((api_vec_string){.ptr = longest_words, .len = 2}), "\") "); + printf("%s\n", no_first.ptr == NULL ? "None" : "Some"); api_Limits_bytes limits = api_byte_limits(str("a"), str("bc")); api_duration breadth = api_breadth((api_Limits_duration){.low = {.secs = 0, .nanos = 5000000}, .high = {.secs = 2, .nanos = 0}}); printf("limits %.*s %.*s breadth %llu\n", (int)limits.low.len, (const char *)limits.low.ptr, (int)limits.high.len, diff --git a/ffi/tests/api.cpp b/ffi/tests/api.cpp index 88c2e0bf1..20709e367 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -698,6 +698,8 @@ int script() { auto none_summed = api::summed({}); std::printf("summed %d %d %d %d\n", summed.x, summed.y, none_summed.x, none_summed.y); std::printf("longest %zu %zu\n", api::longest({"a", "abc"}), api::longest({})); + print("first of Some(\"", api::first_of({"a", "b"}).value(), "\") "); + std::printf("%s\n", api::first_of({}) ? "Some" : "None"); auto limits = api::byte_limits("a", "bc"); print("limits ", limits.low, " "); print("", limits.high, " "); diff --git a/ffi/tests/api.dart b/ffi/tests/api.dart index 5649379f8..6be7f02d7 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -474,6 +474,7 @@ Future script() async { final noneSummed = api.summed([]); print('summed ${summed.x} ${summed.y} ${noneSummed.x} ${noneSummed.y}'); print('longest ${api.longest(['a', 'abc'])} ${api.longest([])}'); + print('first of Some("${api.firstOf(['a', 'b'])}") ${api.firstOf([]) ?? 'None'}'); final limits = api.byteLimits(utf8.encode('a'), utf8.encode('bc')); final waits = api.breadth(const api.LimitsDuration(low: Duration(milliseconds: 5), high: Duration(seconds: 2))); print('limits ${utf8.decode(limits.low)} ${utf8.decode(limits.high)} breadth ${waits.inMilliseconds}'); diff --git a/ffi/tests/api.go b/ffi/tests/api.go index 6bccc3108..149528356 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -560,6 +560,11 @@ func script() { none := api.Summed(nil) fmt.Printf("summed %d %d %d %d\n", sum.X, sum.Y, none.X, none.Y) fmt.Printf("longest %d %d\n", api.Longest([]string{"a", "abc"}), api.Longest(nil)) + noFirst := "None" + if api.FirstOf(nil) != nil { + noFirst = "Some" + } + fmt.Printf("first of Some(%s) %s\n", quoted(*api.FirstOf([]string{"a", "b"})), noFirst) limits := api.ByteLimits([]byte("a"), []byte("bc")) breadth := api.Breadth(api.LimitsDuration{Low: 5 * time.Millisecond, High: 2 * time.Second}) fmt.Printf("limits %s %s breadth %d\n", limits.Low.Copy(), limits.High.Copy(), breadth.Milliseconds()) diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index 6146d70d7..9ba4043cd 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -385,6 +385,7 @@ fun script() { val noneSummed = summed(emptyList()) println("summed ${summed.x} ${summed.y} ${noneSummed.x} ${noneSummed.y}") println("longest ${longest(listOf("a", "abc"))} ${longest(emptyList())}") + println("first of Some(\"${firstOf(listOf("a", "b"))}\") ${firstOf(emptyList()) ?: "None"}") val limits = byteLimits("a".encodeToByteArray(), "bc".encodeToByteArray()) val waits = breadth(LimitsDuration(5.milliseconds, 2.seconds)) println("limits ${limits.low.toByteArray().decodeToString()} ${limits.high.toByteArray().decodeToString()} breadth ${waits.inWholeMilliseconds}") diff --git a/ffi/tests/api.py b/ffi/tests/api.py index cc801a54a..ee1a83a78 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -475,6 +475,7 @@ async def block(): none_summed = api.summed([]) print(f"summed {summed.x} {summed.y} {none_summed.x} {none_summed.y}") print(f"longest {api.longest(['a', 'abc'])} {api.longest([])}") + print(f'first of Some("{api.first_of(["a", "b"])}") {api.first_of([]) or "None"}') limits = api.byte_limits(b'a', b'bc') waits = api.LimitsDuration(datetime.timedelta(milliseconds=5), datetime.timedelta(seconds=2)) breadth = api.breadth(waits) // datetime.timedelta(milliseconds=1) diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index 87c4fec7e..4e1e27650 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -514,6 +514,7 @@ func script() { let noneSum = summed([]) print("summed \(sum.x) \(sum.y) \(noneSum.x) \(noneSum.y)") print("longest \(longest(["a", "abc"])) \(longest([]))") + print("first of Some(\"\(firstOf(["a", "b"])!)\") \(firstOf([]) ?? "None")") let limits = byteLimits(Data("a".utf8), Data("bc".utf8)) let waits = breadth(LimitsDuration(low: .milliseconds(5), high: .seconds(2))) let low = String(decoding: limits.low, as: UTF8.self) From 3c278e884e801b16f93a9073a4d5bba921cf1d6a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 10 Nov 2026 05:57:59 -0800 Subject: [PATCH 9/9] fix(ffi): check a bound naming a kept parameter at each choice A parameter kept for a foreign implementation stayed a parameter in every bound, so `unmet` skipped `B: From` and kept a choice of `B` whose glue does not compile. A bound that names only kept parameters, other than one of them as its own subject, which what the call passes meets, now holds only as the item's own environment proves it for every type passed. Co-Authored-By: Claude Opus 5.5 --- ffi/fixtures/api/src/lib.rs | 38 ++++++++++++++++++++++++ ffi/fixtures/reject/expected.txt | 51 ++++++++++++++++---------------- ffi/fixtures/reject/src/lib.rs | 21 +++++++++++++ ffi/src/collect.rs | 44 +++++++++++++++++++-------- 4 files changed, 117 insertions(+), 37 deletions(-) diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 52e7fd656..e2e94221f 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -1456,6 +1456,44 @@ pub fn report(pair: Pair, log: L) -> u32 { .count() as u32 } +/// What a log kept, made from any log. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct LogCount { + pub kept: u32, +} + +/// A count, which C cannot implement as it needs `Copy`: a parameter it bounds exports at +/// each type implementing it. +pub trait Tallied: Copy { + fn tally(self) -> u32; +} + +impl Tallied for LogCount { + fn tally(self) -> u32 { + self.kept + } +} + +impl Tallied for Point { + fn tally(self) -> u32 { + self.x.unsigned_abs() + } +} + +impl From for LogCount { + fn from(log: L) -> Self { + Self { + kept: u32::from(log.log(3, "tally")), + } + } +} + +/// What `log` kept, as each tally that converts from a log: `LogCount`, and not `Point`, +/// which is `Tallied` too but converts from no log. +pub fn tally, L: Log>(log: L) -> u32 { + T::from(log).tally() +} + /// A wrapped value, which only its impl of `Seed` at `Wrap` names: each /// function taking a seed infers that instance. #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index 9fdea5f6c..bf0642ded 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -54,37 +54,38 @@ fixtures/reject/src/lib.rs:475:1: rust2-ffi: cannot export fn `naturals`: foreig fixtures/reject/src/lib.rs:480:1: rust2-ffi: cannot export fn `opaques`: borrowed return that is not Clone `Opaque`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:485:1: rust2-ffi: cannot export fn `tried`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:490:1: rust2-ffi: cannot export fn `eventually`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:500:1: rust2-ffi: cannot export fn `drain`: trait object `dyn Source`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:516:1: rust2-ffi: cannot export fn `tallied`: unmet bound `u32: std::convert::From`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:521:1: rust2-ffi: cannot export fn `drain`: trait object `dyn Source`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:495:1: note: C cannot implement `Source`: callback signature `items` -fixtures/reject/src/lib.rs:505:1: rust2-ffi: cannot export fn `summed_forever`: 'static borrow `&'static [u32]`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:510:1: rust2-ffi: cannot export fn `retext_last`: 'static borrow `&'a [std::string::String]`; exclude it in the config to leave it out - fixtures/reject/src/lib.rs:510:20: note: `'a` is `'static`, as a handle outlives every call -fixtures/reject/src/lib.rs:517:1: rust2-ffi: cannot export fn `outlast`: 'static borrow `&'b [std::string::String]`; exclude it in the config to leave it out - fixtures/reject/src/lib.rs:517:20: note: `'b` is `'static`, as a handle outlives every call -fixtures/reject/src/lib.rs:524:1: rust2-ffi: cannot export fn `retext_into`: 'static borrow `&'a [std::string::String]`; exclude it in the config to leave it out - fixtures/reject/src/lib.rs:524:20: note: `'a` is `'static`, as a handle outlives every call -fixtures/reject/src/lib.rs:533:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:538:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:526:1: rust2-ffi: cannot export fn `summed_forever`: 'static borrow `&'static [u32]`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:531:1: rust2-ffi: cannot export fn `retext_last`: 'static borrow `&'a [std::string::String]`; exclude it in the config to leave it out + fixtures/reject/src/lib.rs:531:20: note: `'a` is `'static`, as a handle outlives every call +fixtures/reject/src/lib.rs:538:1: rust2-ffi: cannot export fn `outlast`: 'static borrow `&'b [std::string::String]`; exclude it in the config to leave it out + fixtures/reject/src/lib.rs:538:20: note: `'b` is `'static`, as a handle outlives every call +fixtures/reject/src/lib.rs:545:1: rust2-ffi: cannot export fn `retext_into`: 'static borrow `&'a [std::string::String]`; exclude it in the config to leave it out + fixtures/reject/src/lib.rs:545:20: note: `'a` is `'static`, as a handle outlives every call fixtures/reject/src/lib.rs:54:1: rust2-ffi: cannot export fn `excluded`: excluded type `Excluded`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:555:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:572:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:595:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:609:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:614:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:621:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:627:1: rust2-ffi: cannot export fn `floats`: collection not built from its items `std::collections::BTreeSet`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:632:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:637:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:657:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:554:1: rust2-ffi: cannot export fn `ones`: foreign type `std::iter::Repeat`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:559:1: rust2-ffi: cannot export fn `words`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:576:1: rust2-ffi: cannot export fn `launched`: foreign type `std::iter::Chain`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:593:1: rust2-ffi: cannot export fn `launches`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:616:1: rust2-ffi: cannot export fn `pumped`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:630:1: rust2-ffi: cannot export fn `skipped`: foreign type `std::iter::Skip`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:635:1: rust2-ffi: cannot export fn `merged`: foreign type `std::iter::Copied`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:642:1: rust2-ffi: cannot export fn `values`: foreign type `std::collections::btree_map::IntoValues`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:648:1: rust2-ffi: cannot export fn `floats`: collection not built from its items `std::collections::BTreeSet`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:653:1: rust2-ffi: cannot export fn `zeros`: foreign type `std::iter::RepeatN`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:658:1: rust2-ffi: cannot export fn `successes`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:65:2: rust2-ffi: fn `reject::audio::encoder_finish` and fn `reject::audio_encoder::finish` are both `reject_audio_encoder_finish` in C fixtures/reject/src/lib.rs:60:2: note: fn `reject::audio_encoder::finish` is here -fixtures/reject/src/lib.rs:664:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:688:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:678:1: rust2-ffi: cannot export fn `allocated`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:685:1: rust2-ffi: cannot export fn `flattened`: foreign type `std::iter::Flatten`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:69:1: rust2-ffi: cannot export fn `letters`: Range `char`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:6:1: rust2-ffi: cannot export fn `pair`: tuple of references `(&str, u8)`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:706:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:717:9: rust2-ffi: cannot export use `dep`: re-export of a whole crate `dep`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:720:9: rust2-ffi: cannot export use `renamed`: re-export of a whole crate `dep`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:709:1: rust2-ffi: cannot export fn `lent`: foreign type `std::vec::IntoIter`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:727:1: rust2-ffi: cannot export fn `halves`: foreign type `std::str::Split`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:738:9: rust2-ffi: cannot export use `dep`: re-export of a whole crate `dep`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:741:9: rust2-ffi: cannot export use `renamed`: re-export of a whole crate `dep`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:78:2: rust2-ffi: cannot export method `Text::new`: 'static borrow `&'a str`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:76:6: note: `'a` is `'static`, as a handle outlives every call fixtures/reject/src/lib.rs:83:2: rust2-ffi: cannot export method `Text::view`: returned object that borrows `Text<'_>`; exclude it in the config to leave it out diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index d8c17415a..4f2156def 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -496,6 +496,27 @@ pub trait Source: Send + Sync { fn items(&self) -> std::vec::IntoIter; } +/// A count, which C cannot implement as it needs `Copy`. +pub trait Tallied: Copy { + fn tally(self) -> u32; +} + +impl Tallied for u32 { + fn tally(self) -> u32 { + self + } +} + +/// What a tally counts, which C implements. +pub trait Measured { + fn measure(&self) -> u32; +} + +/// A tally made from `counted`, though no type implementing `Tallied` converts from one. +pub fn tallied, C: Measured>(counted: C) -> u32 { + T::from(counted).tally() +} + /// How many items `source` has: a trait object C cannot make. pub fn drain(source: Box) -> usize { source.items().count() diff --git a/ffi/src/collect.rs b/ffi/src/collect.rs index 5ddcd521a..d8c127d38 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -1882,7 +1882,7 @@ impl<'tcx> Collector<'tcx> { let tcx = self.tcx; let mut args = ty::GenericArgs::identity_for_item(tcx, item).to_vec(); args[..parent.len()].copy_from_slice(parent); - self.holds(item, tcx.mk_args(&args)) + self.holds(item, tcx.mk_args(&args), &[]) } /// A trait's required methods, as a callback interface. @@ -2265,9 +2265,9 @@ impl<'tcx> Collector<'tcx> { let choices: Vec<_> = (choices.into_iter()) .map(|(args, named)| (tcx.mk_args(&args), named)) .collect(); - let unmet = self.unmet(def_id, choices[0].0); + let unmet = self.unmet(def_id, choices[0].0, &kept); let instances: Vec> = (choices.into_iter()) - .filter(|(args, _)| self.holds(def_id, args)) + .filter(|(args, _)| self.holds(def_id, args, &kept)) .map(|(args, named)| Instantiation { args, kept: kept.clone(), @@ -2280,22 +2280,42 @@ impl<'tcx> Collector<'tcx> { } } - /// Whether `def_id`'s bounds hold at `args`. - fn holds(&self, def_id: DefId, args: ty::GenericArgsRef<'tcx>) -> bool { - self.unmet(def_id, args).is_none() + /// Whether `def_id`'s bounds hold at `args`, with `kept` the parameters kept for what + /// the call passes. + fn holds(&self, def_id: DefId, args: ty::GenericArgsRef<'tcx>, kept: &[Ty<'tcx>]) -> bool { + self.unmet(def_id, args, kept).is_none() } - /// The first of `def_id`'s bounds that fails at `args`, skipping each that names a type - /// parameter still one: a kept parameter's, which what the call passes meets, or one - /// not yet chosen's, which the item's own environment states only at its parameters. - fn unmet(&self, def_id: DefId, args: ty::GenericArgsRef<'tcx>) -> Option> { + /// The first of `def_id`'s bounds that fails at `args`. A bound on a kept parameter is + /// met by what the call passes, and one naming a parameter not yet chosen is stated by + /// the item's own environment only at its parameters, so both are skipped. Any other + /// bound naming a kept parameter must hold for each type passed for it, as the item's + /// own environment proves: `B: From` holds for every `U: Callback` only through an + /// impl for each. + fn unmet(&self, def_id: DefId, args: ty::GenericArgsRef<'tcx>, kept: &[Ty<'tcx>]) -> Option> { let tcx = self.tcx; let env = TypingEnv::post_analysis(tcx, def_id); let (infcx, param_env) = tcx.infer_ctxt().build_with_typing_env(env); + let checked = |clause: ty::Clause<'tcx>| { + if !clause.has_non_region_param() { + return true; + } + let (args, term) = match clause.kind().skip_binder() { + ty::ClauseKind::Trait(pred) => (pred.trait_ref.args, None), + ty::ClauseKind::Projection(pred) => (pred.projection_term.args, pred.term.as_type()), + _ => return false, + }; + let mut params = (args.iter().chain(term.map(Into::into))) + .flat_map(|arg| arg.walk()) + .filter_map(|arg| arg.as_type()) + .filter(|ty| matches!(ty.kind(), ty::Param(_))); + let subject = args.first().and_then(|arg| arg.as_type()); + params.all(|ty| kept.contains(&ty)) && !subject.is_some_and(|ty| kept.contains(&ty)) + }; (tcx.clauses_of(def_id).instantiate(tcx, args).clauses.iter()) .map(|clause| clause.skip_norm_wip()) .find(|&clause| { - !clause.has_non_region_param() && { + checked(clause) && { let obligation = Obligation::new(tcx, ObligationCause::dummy(), param_env, clause); !infcx.predicate_must_hold_modulo_regions(&obligation) } @@ -2623,7 +2643,7 @@ impl<'tcx> Collector<'tcx> { let mut args = base.to_vec(); args[p.index as usize] = candidate.into(); let args = tcx.mk_args(&args); - self.holds(def_id, args).then_some(args) + self.holds(def_id, args, &[]).then_some(args) } /// The types a type parameter exports at each of, where no one type is the rules':