diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0c2b2459e..3884f21cd 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -269,3 +269,37 @@ jobs: with: name: census path: ts/target/census + + # A scheduled run's failure opens one issue, or comments on the one still open, and a + # pass closes it, so a suite only this workflow runs cannot fail unseen. Only the + # schedule, which runs main, gets the write token: a dispatch can run any branch. + report: + name: Report + needs: [gate, ts, bench, ffi, ffi-cpp, ffi-android, ffi-bench, census] + if: ${{ !cancelled() && github.event_name == 'schedule' }} + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + issues: write + + steps: + - name: Issue + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + FAILED: ${{ contains(needs.*.result, 'failure') }} + RUN: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SHA: ${{ github.sha }} + run: | + title="The nightly fails" + issue=$(gh issue list --state open --search "\"$title\" in:title" --json number,title --jq ".[] | select(.title == \"$title\") | .number" | head -n 1) + if [ "$FAILED" = true ]; then + body="[This run]($RUN) failed at $SHA." + if [ -n "$issue" ]; then + gh issue comment "$issue" --body "$body" + else + gh issue create --title "$title" --body "$body" + fi + elif [ -n "$issue" ]; then + gh issue close "$issue" --comment "[This run]($RUN) passed at $SHA." + fi diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index 1a9201ec2..b4103ebf7 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -117,6 +117,18 @@ impl Counter { self.value } + /// The value plus ten `handle`s and `h`: parameters named as a binding's receiver and + /// its held handle might be. + pub fn offset(&self, handle: u64, h: u64) -> u64 { + self.value + handle * 10 + h + } + + /// The value plus `close`, consuming the counter: a parameter named as the method a + /// binding closes it with might be. + pub fn into_sum(self, close: u64) -> u64 { + self.value + close + } + pub fn name(&self) -> &str { &self.name } @@ -1894,13 +1906,20 @@ pub fn put_number(value: u32, buf: &mut impl bytes::BufMut) { buf.put_slice(format!("{value},").as_bytes()); } -/// Takes the first byte of `buf` as a decimal digit, as moq's decoders take from an +/// `w235` a second later: a parameter named as Kotlin's generated writer of it would be, +/// whose name the writer must take apart from it. +pub fn second_later(w235: Duration) -> Duration { + w235.saturating_add(Duration::from_secs(1)) +} + +/// Takes the first byte of `r` as a decimal digit, as moq's decoders take from an /// `impl Buf`, which takes `Bytes`. It takes the byte even when it fails, and Rust writes -/// back what the call left on either path. -pub fn digit(buf: &mut impl bytes::Buf) -> Result { - match buf.has_remaining() { +/// back what the call left on either path. Named `r`, as moq's `varint::decode_quic`'s +/// is, the name a binding's reader of what Rust wrote back might take. +pub fn digit(r: &mut impl bytes::Buf) -> Result { + match r.has_remaining() { false => Err(Error::Underflow { by: 1 }), - true => match buf.get_u8() { + true => match r.get_u8() { digit @ b'0'..=b'9' => Ok(digit - b'0'), _ => Err(Error::Overflow), }, diff --git a/ffi/fixtures/dep/src/lib.rs b/ffi/fixtures/dep/src/lib.rs index bb143e1e2..8f89b6bf6 100644 --- a/ffi/fixtures/dep/src/lib.rs +++ b/ffi/fixtures/dep/src/lib.rs @@ -5,6 +5,9 @@ pub struct Size { pub width: u32, pub height: u32, + /// What no build has, which the `configured_out` test excludes. + #[cfg(any())] + pub depth: u32, } /// An area: an object of this crate, which the api fixture's signatures take and return. diff --git a/ffi/moq/moq.cpp b/ffi/moq/moq.cpp index 75cc993d8..2f948eaf8 100644 --- a/ffi/moq/moq.cpp +++ b/ffi/moq/moq.cpp @@ -286,8 +286,8 @@ int main() { CHECK(catalog); CHECK(!catalog->is_claimed_BinaryConfig("state")); int read_catalog = 0; - CHECK(catalog->mutate([&](moq::hang_Catalog_Extra &lent) { - auto json = lent.to_json(); + CHECK(catalog->mutate([&](moq::borrow lent) { + auto json = lent->to_json(); CHECK(json); if (json && json.value().str().substr(0, 1) == "{") { read_catalog++; @@ -318,15 +318,15 @@ int main() { } auto app = moq::Json::parse(R"({"v": [1, true]})"); CHECK(app); - CHECK(catalog->mutate([&](moq::hang_Catalog_Extra &lent) { - moq::mux_catalog_hang_Extra sections = lent.ext_get(); + CHECK(catalog->mutate([&](moq::borrow lent) { + moq::mux_catalog_hang_Extra sections = lent->ext_get(); CHECK(sections.set("app", std::move(*app))); - lent.ext_set(std::move(sections)); + lent->ext_set(std::move(sections)); })); - CHECK(catalog->mutate([&](moq::hang_Catalog_Extra &lent) { - auto json = lent.to_json(); + CHECK(catalog->mutate([&](moq::borrow lent) { + auto json = lent->to_json(); CHECK(json && json.value().str().find("\"status\"") != std::string_view::npos); - auto section = lent.ext_get().get("app"); + auto section = lent->ext_get().get("app"); CHECK(section && is(section->to_text(), R"({"v":[1,true]})")); })); // Malformed text fails to parse, and a reserved section is moq-mux's error. diff --git a/ffi/src/dart.rs b/ffi/src/dart.rs index e4c87a357..f3b5c8d23 100644 --- a/ffi/src/dart.rs +++ b/ffi/src/dart.rs @@ -1702,7 +1702,8 @@ impl<'a> Gen<'a> { args.push("a_.hold(this)".to_string()); let copy = matches!(ty, Type::Object(i) if abi.api.objects[i].copy); if receiver == Receiver::Owned && !copy { - after.push("close();".to_string()); + // Qualified, apart from a parameter named `close`. + after.push("this.close();".to_string()); } } Ty::In(inner) => { diff --git a/ffi/src/driver.rs b/ffi/src/driver.rs index 29f0f3206..291c2c1ec 100644 --- a/ffi/src/driver.rs +++ b/ffi/src/driver.rs @@ -4,14 +4,18 @@ use std::path::{Path, PathBuf}; use std::process::ExitCode; +use rustc_ast as ast; use rustc_data_structures::fx::FxHashSet; use rustc_driver::{Callbacks, Compilation}; use rustc_hir::def::{DefKind, Res}; use rustc_hir::def_id::{DefId, LOCAL_CRATE}; use rustc_interface::interface::Compiler; +use rustc_lexer::{FrontmatterAllowed, TokenKind}; use rustc_middle::middle::resolve::Reexport; use rustc_middle::ty::TyCtxt; -use rustc_span::Span; +use rustc_parse::lexer::StripTokens; +use rustc_parse::parser::{AllowConstBlockItems, ForceCollect}; +use rustc_span::{FileName, Span, sym}; use crate::abi::Abi; use crate::config::{self, Config}; @@ -206,13 +210,105 @@ pub fn write(path: &Path, text: &str) -> Result<(), String> { std::fs::write(path, text).map_err(|err| format!("writing {}: {err}", path.display())) } -/// Whether `path`, from the facade's root, names an item a `cfg` leaves out of this build: -/// the config names it for the builds that have it. Its first segment no module has is -/// stripped from that module or from one the module glob-re-exports. +/// Whether the struct, enum, or union `adt` declares a field or variant `name` under a +/// `cfg` this build left it out by. rustc records only the items a `cfg` strips, not their +/// members, so this reads the declaration from its source. +fn stripped_member(tcx: TyCtxt<'_>, adt: DefId, name: &str) -> bool { + if tcx.adt_def(adt).all_fields().any(|f| f.name.as_str() == name) + || tcx.adt_def(adt).variants().iter().any(|v| v.name.as_str() == name) + { + return false; + } + // The item from its name on, up to the `;` or closing brace that ends it: past it the + // file may close a module the item is in, which the parser would refuse unmatched. Each + // place nesting returns to the item's own level may end it, such as a const generic's + // block before its fields, so the first that parses as an item does. + let source_map = tcx.sess.source_map(); + let head = tcx.def_span(adt); + let source_file = source_map.lookup_source_file(head.lo()); + let Ok(source) = source_map.span_to_snippet(head.with_hi(source_file.end_position())) else { + return false; + }; + let (mut depth, mut at) = (0usize, 0usize); + let mut item = None; + for token in rustc_lexer::tokenize(&source, FrontmatterAllowed::No) { + at += token.len as usize; + let end = match token.kind { + TokenKind::OpenParen | TokenKind::OpenBrace | TokenKind::OpenBracket => { + depth += 1; + false + } + TokenKind::CloseParen | TokenKind::CloseBrace | TokenKind::CloseBracket => { + // A delimiter the item never opened closes the module it is in. + let Some(inner) = depth.checked_sub(1) else { break }; + depth = inner; + depth == 0 && token.kind == TokenKind::CloseBrace + } + TokenKind::Semi => depth == 0, + _ => false, + }; + if end && let Some(parsed) = parse_item(tcx, &source[..at]) { + item = Some(parsed); + break; + } + } + let Some(item) = item else { return false }; + // A `cfg`, or one a `cfg_attr` adds, at any depth: the attributes past its predicate. + fn adds_cfg(items: &[ast::MetaItemInner]) -> bool { + items.iter().skip(1).any(|item| { + item.has_name(sym::cfg) || (item.has_name(sym::cfg_attr) && item.meta_item_list().is_some_and(adds_cfg)) + }) + } + let configured = |attrs: &[ast::Attribute]| { + attrs.iter().any(|attr| { + attr.has_name(sym::cfg) + || (attr.has_name(sym::cfg_attr) && attr.meta_item_list().is_some_and(|items| adds_cfg(&items))) + }) + }; + match &item.kind { + ast::ItemKind::Struct(_, _, data) | ast::ItemKind::Union(_, _, data) => { + (data.fields().iter()).any(|f| f.ident.is_some_and(|ident| ident.as_str() == name) && configured(&f.attrs)) + } + ast::ItemKind::Enum(_, _, def) => { + (def.variants.iter()).any(|v| v.ident.as_str() == name && configured(&v.attrs)) + } + _ => false, + } +} + +/// `source` parsed as one item, or none if it is not one. +fn parse_item(tcx: TyCtxt<'_>, source: &str) -> Option> { + let file = FileName::anon_source_code(source); + let mut parser = match rustc_parse::new_parser_from_source_str( + &tcx.sess.psess, + file, + source.to_string(), + StripTokens::Nothing, + ) { + Ok(parser) => parser, + Err(errors) => { + errors.into_iter().for_each(|e| e.cancel()); + return None; + } + }; + match parser.parse_item(ForceCollect::No, AllowConstBlockItems::DoesNotMatter) { + Ok(item) => item, + Err(e) => { + e.cancel(); + None + } + } +} + +/// Whether `path`, from the facade's root, names an item a `cfg` leaves out of this build, +/// or a field or variant of one: the config names it for the builds that have it. Its first +/// segment no module has is stripped from that module or from one the module +/// glob-re-exports. fn configured_out(tcx: TyCtxt<'_>, path: &str) -> bool { let path = path.strip_prefix("field@").unwrap_or(path); + let segments: Vec<&str> = path.split("::").collect(); let mut module = LOCAL_CRATE.as_def_id(); - for segment in path.split("::") { + for (i, segment) in segments.iter().enumerate() { let name = segment.split('<').next().unwrap_or(segment); let children = match module.as_local() { Some(local) => tcx.module_children_local(local), @@ -224,7 +320,10 @@ fn configured_out(tcx: TyCtxt<'_>, path: &str) -> bool { module = def_id; continue; } - // A member of an item, which no `cfg` this checks strips. + Res::Def(DefKind::Struct | DefKind::Enum | DefKind::Union, def_id) if i + 2 == segments.len() => { + return stripped_member(tcx, def_id, segments[i + 1]); + } + // A method, or a member past one, which no `cfg` this checks strips. _ => return false, } } diff --git a/ffi/src/kotlin.rs b/ffi/src/kotlin.rs index a175f91e4..686b17638 100644 --- a/ffi/src/kotlin.rs +++ b/ffi/src/kotlin.rs @@ -52,6 +52,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result { errors: Vec, /// A counter for fresh Kotlin names. fresh: usize, + /// The Kotlin parameters of the function being written, which a fresh name never takes. + params: Vec, /// The arities a tuple class is written for: one, and past `Triple`. tuples: BTreeSet, /// The Kotlin functions the glue calls, which the rules keep. @@ -303,8 +306,13 @@ struct Gen<'a> { impl<'a> Gen<'a> { fn fresh(&mut self, base: &str) -> String { - self.fresh += 1; - format!("{base}{}", self.fresh) + loop { + self.fresh += 1; + let name = format!("{base}{}", self.fresh); + if !self.params.contains(&name) { + return name; + } + } } /// A C name without the prefix. @@ -1433,6 +1441,7 @@ impl<'a> Gen<'a> { place: Place, ) -> Option { let abi = self.abi; + self.params = locals(&function.params); let export = abi.export(function, owner); let c_name = export.call.name.clone(); let bare = self.bare(&c_name).to_string(); @@ -1478,12 +1487,14 @@ impl<'a> Gen<'a> { n += 1; arg.jni.push("jlong"); arg.native.push("Long".to_string()); - arg.open = Some("handle.hold { h ->".to_string()); - arg.exprs.push("h".to_string()); + // Qualified, and named fresh, apart from the parameters in scope. + let h = self.fresh("h"); + arg.open = Some(format!("this.handle.hold {{ {h} ->")); + arg.exprs.push(h); arg.rust_args.push(format!("{p} as usize as _")); let copy = matches!(ty, Type::Object(i) if abi.api.objects[i].copy); if receiver == Receiver::Owned && !copy { - arg.after.push("close()".to_string()); + arg.after.push("this.close()".to_string()); } arg } @@ -1494,7 +1505,7 @@ impl<'a> Gen<'a> { } let mut kt_params = Vec::new(); let mut closures = Vec::new(); - let names = locals(&function.params); + let names = self.params.clone(); // Each parameter Rust writes back, by its Kotlin name, and what it holds. let mut written: Vec<(String, Type)> = Vec::new(); for (param, name) in function.params.iter().zip(&names) { @@ -1659,16 +1670,17 @@ impl<'a> Gen<'a> { ), ); let mut lines = Vec::new(); + let r = self.fresh("r"); // What Rust wrote back comes first, read whether or not the call failed. for (name, ty) in &written { - let read = self.read_value(ty, "r"); + let read = self.read_value(ty, &r); lines.push(format!("{name}.value = {}", to_param(ty, &read))); } - lines.extend(self.lift_result(&ok, err.as_ref(), exception.as_deref(), "r")); + lines.extend(self.lift_result(&ok, err.as_ref(), exception.as_deref(), &r)); value = match lines.is_empty() { true => call, false => format!( - "Native.Reader({call}).let {{ r ->\n{}}}", + "Native.Reader({call}).let {{ {r} ->\n{}}}", lines.iter().map(|l| format!("\t{l}\n")).collect::() ), }; diff --git a/ffi/src/main.rs b/ffi/src/main.rs index 930d11f0f..b25d2b10e 100644 --- a/ffi/src/main.rs +++ b/ffi/src/main.rs @@ -7,6 +7,7 @@ #![feature(rustc_private)] +extern crate rustc_ast; extern crate rustc_attr_ir; extern crate rustc_data_structures; extern crate rustc_driver; @@ -14,7 +15,9 @@ extern crate rustc_errors; extern crate rustc_hir; extern crate rustc_infer; extern crate rustc_interface; +extern crate rustc_lexer; extern crate rustc_middle; +extern crate rustc_parse; extern crate rustc_session; extern crate rustc_span; extern crate rustc_trait_selection; diff --git a/ffi/tests/api.kt b/ffi/tests/api.kt index 3f8f71d59..75309b1be 100644 --- a/ffi/tests/api.kt +++ b/ffi/tests/api.kt @@ -408,8 +408,10 @@ fun script() { Counter.new("three", 3u).use { three -> println("plus ${two.plus(three)} ${two.plus(null)}") println("add_to ${two.addTo(three)} ${two.addTo(null)} ${three.get()}") + check(two.offset(3u, 4u) == 36uL, "a method's parameters named handle and h are its own") } } + check(Counter.new("five", 5u).intoSum(1u) == 6uL, "a consuming method's parameter named close is its own") val encoded = Mut("0,".encodeToByteArray()) putNumber(1u, encoded) putNumber(23u, encoded) @@ -674,14 +676,7 @@ fun owned() { val sliced = nextPacket(Packet(1u, packet.payload.slice(1, n - 1))).payload check(sliced.toByteArray().contentEquals(payload.copyOfRange(1, n - 1)), "a slice of a record's $n bytes crosses as its range") } - var freed = dropped() - for (n in 1023..1025) { - val bytes = tokenBytes(n.toUInt(), n.toUInt()) - if (n <= 1024) freed++ - check(dropped() == freed, "$n bytes are freed at the call only if copied") - reads(bytes, n.toByte(), "$n bytes") - } - collected(freed + 1u, "the GC frees lent bytes nothing reaches") + collected(lentAround() + 1u, "the GC frees lent bytes nothing reaches") reads(Bytes(ByteArray(1025) { 9 }), 9, "Kotlin's bytes") // A call after close throws, and close frees at once, once. @@ -821,7 +816,22 @@ fun budgeted() { collected(before + 66u, "the GC frees bytes lent once the budget is free") } -// In its own frame, so nothing on the caller's keeps them reachable. +// Rust's bytes either side of the copy limit, returning the drops once the call has freed +// those it copied. In its own frame, which the Android test keeps from R8's inlining, as +// ART's interpreter keeps the last ones reachable from the caller's until it returns. +fun lentAround(): UInt { + var freed = dropped() + for (n in 1023..1025) { + val bytes = tokenBytes(n.toUInt(), n.toUInt()) + if (n <= 1024) freed++ + check(dropped() == freed, "$n bytes are freed at the call only if copied") + reads(bytes, n.toByte(), "$n bytes") + } + return freed +} + +// In its own frame, which the Android test keeps from R8's inlining, so nothing on the +// caller's keeps them reachable. fun lentToBudget(before: UInt) { val mib = 1 shl 20 val held = List(64) { tokenBytes(1u, mib.toUInt()) } diff --git a/ffi/tests/fixtures.rs b/ffi/tests/fixtures.rs index 8b7393ded..e2778ce95 100644 --- a/ffi/tests/fixtures.rs +++ b/ffi/tests/fixtures.rs @@ -446,7 +446,9 @@ fn api_android() { // The app: the script's entry point, kept as an app keeps its own, and everything it // reaches, which R8 shrinks and renames, keeping only what the rules keep: the // generated ones, and those kotlinx-coroutines' jar holds, which an app's build reads - // from it. Kotlin's annotations are on an app's classpath, not in its dex. + // from it. Kotlin's annotations are on an app's classpath, not in its dex. The + // script's functions that hold lent bytes in their own frame are kept too, as R8 would + // inline them into their caller, whose frame ART's interpreter keeps reaching the bytes. let jar = dir.join("app.jar"); run(kotlinc(&out, &jar).arg("-include-runtime")); run(Command::new("jar") @@ -458,7 +460,7 @@ fn api_android() { std::fs::write( &app, format!( - "-keep class ApiKt {{ public static void main(java.lang.String[]); }}\n-dontwarn org.jetbrains.annotations.**\n-printusage {}\n", + "-keep class ApiKt {{ public static void main(java.lang.String[]); static *** lentAround(); static *** lentToBudget*(...); }}\n-dontwarn org.jetbrains.annotations.**\n-printusage {}\n", dir.join("usage.txt").display() ), ) @@ -1488,8 +1490,10 @@ fn foreign_role_names() { /// An exclude may name what a `cfg` leaves out of this build, whether the facade's own /// module or another crate's that the facade re-exports through a glob, or a chain of them, -/// but not what no build has, nor what a module only some of whose items the facade -/// re-exports leaves out. +/// or a field or variant of the facade's or another crate's type, under a `cfg` or one a +/// `cfg_attr` adds, an inline module's type and a const-generic one's included, but not +/// what no build has, nor what a module only some of whose items the facade re-exports +/// leaves out. #[test] fn configured_out() { let package = package("configured_out"); @@ -1507,11 +1511,14 @@ fn configured_out() { "src/lib.rs", "pub use ::dep::Size;\n\npub mod dep {\n\tpub use ::dep::*;\n}\n\n#[cfg(any())]\npub mod local {}\n\n\ mod inner {\n\tpub fn kept() -> u32 {\n\t\t1\n\t}\n\n\t#[cfg(any())]\n\tpub mod gone {}\n}\n\n\ - mod middle {\n\tpub use crate::inner::*;\n}\n\npub mod outer {\n\tpub use crate::middle::*;\n}\n", + mod middle {\n\tpub use crate::inner::*;\n}\n\npub mod outer {\n\tpub use crate::middle::*;\n}\n\n\ + pub struct Shape {\n\tpub n: u32,\n\t#[cfg(any())]\n\tpub gone: u32,\n\t#[cfg_attr(all(), cfg_attr(all(), cfg(any())))]\n\tpub gated: u32,\n}\n\n\ + pub enum Kind {\n\tKept,\n\t#[cfg(any())]\n\tGone,\n}\n\n\ + pub mod nested {\n\tpub struct Inner {\n\t\tpub n: u32,\n\t\t#[cfg(any())]\n\t\tpub gone: u32,\n\t}\n}\n", ), ( "ffi.toml", - "exclude = [\"Gone\", \"dep::gone::Gone\", \"gone::Gone\", \"local::Gone\", \"outer::gone::Gone\"]\n", + "exclude = [\"Gone\", \"Kind::Gone\", \"Shape::gated\", \"Shape::gone\", \"Shape::missing\", \"Size::depth\", \"nested::Inner::gone\", \"dep::gone::Gone\", \"gone::Gone\", \"local::Gone\", \"outer::gone::Gone\"]\n", ), ], ); @@ -1529,6 +1536,7 @@ fn configured_out() { errors, [ "error: rust2-ffi: exclude `Gone` in the config names nothing exported", + "error: rust2-ffi: exclude `Shape::missing` in the config names nothing exported", "error: rust2-ffi: exclude `gone::Gone` in the config names nothing exported", ], "{stderr}" diff --git a/quest/m0/nightly-green.md b/quest/m0/nightly-green.md deleted file mode 100644 index a22d9dddd..000000000 --- a/quest/m0/nightly-green.md +++ /dev/null @@ -1,39 +0,0 @@ -# [M] The nightly passes - -## Goal - -Every job of the scheduled `nightly.yml` run passes on `main`, each failure fixed at its -cause, and a regression in a suite only the nightly runs cannot go unseen for a week again. - -## Plan - -Found in the 2026-11-09 quest audit: the nightly has failed every scheduled run since -2026-10-29, with no quest owning it. Decided with the maintainer on 2026-11-09: ✅ m0, -after the UniFFI fork's deletion removed the `uniffi-*` jobs, so this fixes only what -remains. Rejected: m1. - -What failed on 2026-11-09 (run -[42655190735](https://github.com/moq-dev/rust2/actions/runs/42655190735), at `f7a0c279`): - -- `Gate` and `TypeScript package`: rust2-ts's `bindgen` fixture differs from native in 1 - of 5073 cases, a NaN's payload through the variadic import (`max_of` in - `ts/fixtures/bindgen/src/harness.rs`: native `18446472823932715226`, translated - `9221120237041090560`). "Encoder in Chromium" runs after that step, so the WebCodecs - encoder proof [#681](https://github.com/moq-dev/rust2/pull/681) added has never run in - CI. Reproduce first; the fix is in the translation or the import's glue, not in making - the harness blind to NaN payloads, unless JS cannot keep one through a call, which the - PR then shows. -- "Export to Kotlin on Android" at API 35: `the GC frees lent bytes nothing reaches`, for - at least three nights. -- `just ffi moq` likely fails on `main` since - [#668](https://github.com/moq-dev/rust2/pull/668): the exporter now refuses every - whole-crate `pub use` (`ffi/src/collect.rs`), and `ffi/moq/ffi.toml` excludes nine of - the eleven at the pin but not hang's and moq-tokio's `pub use moq_net;`. Not yet run in - CI, since no nightly ran after the merge. [The pin bump](/quest/m1/ffi-refusals/moq-pin.md) - removes them if it lands first. - -Open: what keeps it green. Recommended: the nightly's failure opens or updates one GitHub -issue the maintainer sees, and `just ffi moq`'s export step joins `just ci` when `ffi/` -or the pin changes, if it fits the gate's budget; measure it and report. - -Public API: none. diff --git a/ts/fixtures/bindgen/expected/harness.ts b/ts/fixtures/bindgen/expected/harness.ts index ba5afcb80..b9939771c 100644 --- a/ts/fixtures/bindgen/expected/harness.ts +++ b/ts/fixtures/bindgen/expected/harness.ts @@ -67,31 +67,53 @@ export function show(out: rs.String, what: string, v: rs.JsValue): void { rs.resultUnwrap(rs.fmtWriteString(out, [args3[0], ": ", args3[1], "\n"]), rs.fmtErrorDebug); } +/** + * A `Math` result's bits, any NaN as `f64::NAN`'s: ECMAScript leaves which NaN a + * function returns to the engine, and V8's varies by CPU and call path, both sides' + * calls being the same. + */ +export function opBits(x: rs.f64): rs.u64 { + let _2!: rs.f64; + if (!Number.isNaN(x)) { + _2 = x; + } else { + _2 = NaN; + } + return rs.f64ToBits(_2); +} + export function math(input: Input.Input, out: rs.String): void { + let fround2!: rs.f32; const _5: [rs.f64, rs.f64] = [Input.f64(input), Input.f64(input)]; const x: rs.f64 = _5[0]; const y: rs.f64 = _5[1]; - const part = rs.f64ToBits(rs.jsToF64(rs.jsGlobal("Math").floor(x))); + const part = opBits(rs.jsToF64(rs.jsGlobal("Math").floor(x))); rs.resultUnwrap(rs.fmtWriteString(out, ["floor ", (f: rs.Formatter) => rs.u64Debug(part, f), "\n"]), rs.fmtErrorDebug); - const part2 = rs.f64ToBits(rs.jsToF64(rs.jsGlobal("Math").trunc(x))); + const part2 = opBits(rs.jsToF64(rs.jsGlobal("Math").trunc(x))); rs.resultUnwrap(rs.fmtWriteString(out, ["trunc ", (f: rs.Formatter) => rs.u64Debug(part2, f), "\n"]), rs.fmtErrorDebug); - const part3 = rs.f64ToBits(rs.jsToF64(rs.jsGlobal("Math").sign(x))); + const part3 = opBits(rs.jsToF64(rs.jsGlobal("Math").sign(x))); rs.resultUnwrap(rs.fmtWriteString(out, ["sign ", (f: rs.Formatter) => rs.u64Debug(part3, f), "\n"]), rs.fmtErrorDebug); - const part4 = rs.f32ToBits(rs.jsToF32(rs.jsGlobal("Math").fround(x))); + const fround: rs.f32 = rs.jsToF32(rs.jsGlobal("Math").fround(x)); + if (!Number.isNaN(fround)) { + fround2 = fround; + } else { + fround2 = NaN; + } + const part4 = rs.f32ToBits(fround2); rs.resultUnwrap(rs.fmtWriteString(out, ["fround ", (f: rs.Formatter) => rs.u32Debug(part4, f), "\n"]), rs.fmtErrorDebug); - const part5 = rs.f64ToBits(rs.jsToF64(rs.jsGlobal("Math").hypot(x, y))); + const part5 = opBits(rs.jsToF64(rs.jsGlobal("Math").hypot(x, y))); rs.resultUnwrap(rs.fmtWriteString(out, ["hypot ", (f: rs.Formatter) => rs.u64Debug(part5, f), "\n"]), rs.fmtErrorDebug); - const part6 = rs.f64ToBits(rs.jsToF64(rs.jsGlobal("Math").atan2(x, y))); + const part6 = opBits(rs.jsToF64(rs.jsGlobal("Math").atan2(x, y))); rs.resultUnwrap(rs.fmtWriteString(out, ["atan2 ", (f: rs.Formatter) => rs.u64Debug(part6, f), "\n"]), rs.fmtErrorDebug); - const part7 = rs.f64ToBits(rs.jsToF64(rs.jsGlobal("Math").pow(x, y))); + const part7 = opBits(rs.jsToF64(rs.jsGlobal("Math").pow(x, y))); rs.resultUnwrap(rs.fmtWriteString(out, ["pow ", (f: rs.Formatter) => rs.u64Debug(part7, f), "\n"]), rs.fmtErrorDebug); - const part8 = rs.f64ToBits(rs.jsToF64(rs.jsGlobal("Math").max(x, y))); + const part8 = opBits(rs.jsToF64(rs.jsGlobal("Math").max(x, y))); rs.resultUnwrap(rs.fmtWriteString(out, ["max ", (f: rs.Formatter) => rs.u64Debug(part8, f), "\n"]), rs.fmtErrorDebug); - const _102: rs.i32 = Input.u32(input) | 0; - const part9 = rs.jsToU32(rs.jsGlobal("Math").clz32(_102)); + const _105: rs.i32 = Input.u32(input) | 0; + const part9 = rs.jsToU32(rs.jsGlobal("Math").clz32(_105)); rs.resultUnwrap(rs.fmtWriteString(out, ["clz32 ", (f: rs.Formatter) => rs.u32Display(part9, f), "\n"]), rs.fmtErrorDebug); - const _118: rs.Slice = rs.sliceRange(rs.sliceFrom([y, Input.f64(input), Input.f64(input)]), 0, rs.u8Rem(Input.u8(input), 4) as rs.usize); - const part11 = rs.f64ToBits(rs.jsToF64(rs.jsSpreadMethod(rs.jsGlobal("Math"), "max", [x], rs.jsTyped(rs.global.Float64Array, _118, false)))); + const _121: rs.Slice = rs.sliceRange(rs.sliceFrom([y, Input.f64(input), Input.f64(input)]), 0, rs.u8Rem(Input.u8(input), 4) as rs.usize); + const part11 = opBits(rs.jsToF64(rs.jsSpreadMethod(rs.jsGlobal("Math"), "max", [x], rs.jsTyped(rs.global.Float64Array, _121, false)))); rs.resultUnwrap(rs.fmtWriteString(out, ["max_of ", (f: rs.Formatter) => rs.u64Debug(part11, f), "\n"]), rs.fmtErrorDebug); } diff --git a/ts/fixtures/bindgen/src/harness.rs b/ts/fixtures/bindgen/src/harness.rs index 553f71910..cc2f45ad8 100644 --- a/ts/fixtures/bindgen/src/harness.rs +++ b/ts/fixtures/bindgen/src/harness.rs @@ -187,22 +187,31 @@ fn show(out: &mut String, what: &str, v: &JsValue) { writeln!(out, "{what}: {text:?}").unwrap(); } +/// A `Math` result's bits, any NaN as `f64::NAN`'s: ECMAScript leaves which NaN a +/// function returns to the engine, and V8's varies by CPU and call path, both sides' +/// calls being the same. +fn op_bits(x: f64) -> u64 { + if x.is_nan() { f64::NAN } else { x }.to_bits() +} + fn math(input: &mut Input, out: &mut String) { let (x, y) = (input.f64(), input.f64()); - writeln!(out, "floor {:?}", Math::floor(x).to_bits()).unwrap(); - writeln!(out, "trunc {:?}", Math::trunc(x).to_bits()).unwrap(); - writeln!(out, "sign {:?}", Math::sign(x).to_bits()).unwrap(); - writeln!(out, "fround {:?}", Math::fround(x).to_bits()).unwrap(); - writeln!(out, "hypot {:?}", Math::hypot(x, y).to_bits()).unwrap(); - writeln!(out, "atan2 {:?}", Math::atan2(x, y).to_bits()).unwrap(); - writeln!(out, "pow {:?}", Math::pow(x, y).to_bits()).unwrap(); - writeln!(out, "max {:?}", Math::max(x, y).to_bits()).unwrap(); + writeln!(out, "floor {:?}", op_bits(Math::floor(x))).unwrap(); + writeln!(out, "trunc {:?}", op_bits(Math::trunc(x))).unwrap(); + writeln!(out, "sign {:?}", op_bits(Math::sign(x))).unwrap(); + let fround = Math::fround(x); + let fround = if fround.is_nan() { f32::NAN } else { fround }; + writeln!(out, "fround {:?}", fround.to_bits()).unwrap(); + writeln!(out, "hypot {:?}", op_bits(Math::hypot(x, y))).unwrap(); + writeln!(out, "atan2 {:?}", op_bits(Math::atan2(x, y))).unwrap(); + writeln!(out, "pow {:?}", op_bits(Math::pow(x, y))).unwrap(); + writeln!(out, "max {:?}", op_bits(Math::max(x, y))).unwrap(); writeln!(out, "clz32 {}", Math::clz32(input.u32() as i32)).unwrap(); let rest = [y, input.f64(), input.f64()]; writeln!( out, "max_of {:?}", - imports::max_of(x, &rest[..usize::from(input.u8() % 4)]).to_bits() + op_bits(imports::max_of(x, &rest[..usize::from(input.u8() % 4)])) ) .unwrap(); }