diff --git a/ffi/README.md b/ffi/README.md index cafd58099..022556d6a 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -65,7 +65,7 @@ re-exports its items instead, which export. 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)`; an `E` of another crate that implements `Display`, such as `anyhow::Error` or `io::Error`, is `String`, its message | @@ -90,8 +90,9 @@ re-exports its items instead, which export. 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 @@ -619,7 +620,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, then the `exclude` list that exports the rest, and -what else it left out, such as what C can never hold. It ignores the config's `exclude`. It still fails on an `instance not inferred` +what else it left out, such as what C can never hold and 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 fdf6c446f..973b451a1 100644 --- a/ffi/fixtures/api/examples/transcript.rs +++ b/ffi/fixtures/api/examples/transcript.rs @@ -393,6 +393,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)); @@ -484,6 +491,26 @@ 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), + 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 5e22e1e72..e2e94221f 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -1378,6 +1378,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 { @@ -1438,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)] @@ -1683,6 +1739,39 @@ 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() +} + +/// 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") +} + /// 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 377409d79..bf0642ded 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -54,29 +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: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: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: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: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: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: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: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: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 a44f704a5..4f2156def 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -491,11 +491,65 @@ 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; } +/// 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() +} + +/// 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() +} + +/// 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; + } +} + +/// 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/census.rs b/ffi/src/census.rs index d97950507..23705edb5 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 705b68e98..d8c127d38 100644 --- a/ffi/src/collect.rs +++ b/ffi/src/collect.rs @@ -4,8 +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, and what C can never hold, are listed in the -//! census. A generic type +//! which are no part of the documented API, what C can never hold, 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. @@ -99,6 +99,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. @@ -268,6 +270,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(), unheld: Vec::new(), @@ -722,7 +725,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() { @@ -731,6 +735,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()) } @@ -793,19 +799,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() @@ -1292,6 +1312,8 @@ impl<'tcx> Collector<'tcx> { | Type::F64 | Type::Char | Type::String + | Type::Bytes + | Type::Duration | Type::Record(_) | Type::Enum(_) | Type::Object(_) => Ok(arg), @@ -1509,10 +1531,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", @@ -1854,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. @@ -2161,26 +2189,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 { @@ -2195,31 +2244,30 @@ 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)) .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(), @@ -2232,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) } @@ -2264,7 +2332,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; @@ -2316,8 +2385,21 @@ 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)); + // 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 && !matches!(region.kind(), ty::ReBound(..)) => + { + Some(Ty::new_imm_ref(tcx, *region, 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))); } @@ -2561,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': @@ -3585,10 +3667,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 6ec54f8a9..937ca3fea 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 a5f7f3369..f8baf9b99 100644 --- a/ffi/tests/api.c +++ b/ffi/tests/api.c @@ -671,6 +671,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}; @@ -854,6 +857,19 @@ 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, + (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 300da9491..20709e367 100644 --- a/ffi/tests/api.cpp +++ b/ffi/tests/api.cpp @@ -563,6 +563,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}; @@ -696,6 +698,15 @@ 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, " "); + 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 999b450c9..6be7f02d7 100644 --- a/ffi/tests/api.dart +++ b/ffi/tests/api.dart @@ -399,6 +399,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); @@ -472,6 +474,12 @@ 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}'); + 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 446f3e87c..149528356 100644 --- a/ffi/tests/api.go +++ b/ffi/tests/api.go @@ -466,6 +466,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} @@ -558,6 +560,16 @@ 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()) + 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 f86d7e09d..9ba4043cd 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 @@ -321,6 +322,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) @@ -382,6 +385,12 @@ 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}") + 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 2b8b845eb..ee1a83a78 100644 --- a/ffi/tests/api.py +++ b/ffi/tests/api.py @@ -407,6 +407,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) @@ -473,6 +475,13 @@ 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) + 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 9386eac0a..4e1e27650 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -448,6 +448,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) @@ -512,6 +514,14 @@ 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) + 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))") diff --git a/quest/m1/ffi-export/refusals/README.md b/quest/m1/ffi-export/refusals/README.md index 6a7f0ff65..bcf32c5af 100644 --- a/quest/m1/ffi-export/refusals/README.md +++ b/quest/m1/ffi-export/refusals/README.md @@ -64,7 +64,7 @@ Public API: the exporter's command line and the C API of every item that exports ## Required - [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::*;` -- [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) - `Filter`, serde_json's map iterators, `Instant`, and tracing's levels, once their maintainer decisions land - [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.