From 9752a6f17e0671fe39a8048dbcf52ec4de4fe7df Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:14:26 -0700 Subject: [PATCH 1/7] chore(quest): claim quest/m2/ts-model/crates.md Quest-Claim: cb1390a9d51b69f6 From 5d6d8f036a5634dd3046cb7e04ced5e3ea17720e Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:56:16 -0700 Subject: [PATCH 2/7] feat(ts): translate the dependencies ts.toml names with the package `ts.toml`'s `dependencies` lists packages whose libraries translate with the package as one program. Cargo compiles each with every function's MIR and the package's flags, `--cfg rust2_ts` included, through profile rustflags its fingerprint sees. The translator treats their items as the package's own: types, calls, drops, copies, and the analysis of what is written in place, which now reads their bodies too. Their modules go under their crate's name. moq-net's model census drops from 3,203 refusals to 1,412, with no call into kio, moq-clock, or moq-pattern refused. Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-async.md | 2 +- quest/m2/ts-model/README.md | 1 - quest/m2/ts-model/crates.md | 33 ----- quest/m3/ts-duration.md | 2 +- ts/Cargo.lock | 11 ++ ts/Cargo.toml | 4 + ts/README.md | 26 +++- ts/fixtures/crates/Cargo.toml | 21 +++ ts/fixtures/crates/bind/last.ts | 8 ++ ts/fixtures/crates/dep/Cargo.toml | 10 ++ ts/fixtures/crates/dep/src/lib.rs | 79 +++++++++++ ts/fixtures/crates/expected/bind/last.ts | 9 ++ .../crates/expected/crates_dep.Guard.ts | 40 ++++++ .../crates/expected/crates_dep.Point.ts | 16 +++ ts/fixtures/crates/expected/crates_dep.ts | 38 ++++++ ts/fixtures/crates/expected/harness.Item.ts | 14 ++ ts/fixtures/crates/expected/harness.ts | 100 ++++++++++++++ ts/fixtures/crates/expected/lib.ts | 9 ++ ts/fixtures/crates/refused.toml | 5 + ts/fixtures/crates/refused.txt | 3 + ts/fixtures/crates/shared.txt | 1 + ts/fixtures/crates/src/harness.rs | 45 +++++++ ts/fixtures/crates/src/lib.rs | 12 ++ ts/fixtures/crates/ts.toml | 6 + ts/fixtures/crates/unknown.toml | 4 + ts/fixtures/crates/unknown.txt | 1 + ts/moq/model.toml | 2 + ts/src/census.rs | 1 + ts/src/cli.rs | 24 ++++ ts/src/config.rs | 3 + ts/src/driver.rs | 22 +-- ts/src/lower/bind.rs | 6 +- ts/src/lower/copy.rs | 22 +-- ts/src/lower/drop.rs | 6 +- ts/src/lower/library.rs | 2 +- ts/src/lower/mir/coroutine.rs | 14 +- ts/src/lower/mir/decided.rs | 26 ++-- ts/src/lower/mir/glue.rs | 2 +- ts/src/lower/mir/mod.rs | 7 +- ts/src/lower/mir/place.rs | 2 +- ts/src/lower/mod.rs | 127 +++++++++++++++--- ts/src/main.rs | 1 + ts/tests/fixtures.rs | 24 ++++ 43 files changed, 683 insertions(+), 108 deletions(-) delete mode 100644 quest/m2/ts-model/crates.md create mode 100644 ts/fixtures/crates/Cargo.toml create mode 100644 ts/fixtures/crates/bind/last.ts create mode 100644 ts/fixtures/crates/dep/Cargo.toml create mode 100644 ts/fixtures/crates/dep/src/lib.rs create mode 100644 ts/fixtures/crates/expected/bind/last.ts create mode 100644 ts/fixtures/crates/expected/crates_dep.Guard.ts create mode 100644 ts/fixtures/crates/expected/crates_dep.Point.ts create mode 100644 ts/fixtures/crates/expected/crates_dep.ts create mode 100644 ts/fixtures/crates/expected/harness.Item.ts create mode 100644 ts/fixtures/crates/expected/harness.ts create mode 100644 ts/fixtures/crates/expected/lib.ts create mode 100644 ts/fixtures/crates/refused.toml create mode 100644 ts/fixtures/crates/refused.txt create mode 100644 ts/fixtures/crates/shared.txt create mode 100644 ts/fixtures/crates/src/harness.rs create mode 100644 ts/fixtures/crates/src/lib.rs create mode 100644 ts/fixtures/crates/ts.toml create mode 100644 ts/fixtures/crates/unknown.toml create mode 100644 ts/fixtures/crates/unknown.txt diff --git a/quest/m2/ts-async.md b/quest/m2/ts-async.md index aa57a4070..77310056a 100644 --- a/quest/m2/ts-async.md +++ b/quest/m2/ts-async.md @@ -27,7 +27,7 @@ What moq-net still needs, by its census from MIR at the pinned revision: - Async closures. - kio's own API, reached across crates (`Pending`, `Consumer::poll`, `Waiter::poll_future`, `Tasks`), translates through - [safe dependencies](/quest/m2/ts-model/crates.md), and kio's `RawWaker` vtable and + `ts.toml`'s `dependencies`, and kio's `RawWaker` vtable and `Box` tasks are [bindings](/ts/README.md#bindings). The model itself (its census of 2026-10-27) needs only `Poll`'s helpers and diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index db75e7f54..d240ca6d0 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -46,7 +46,6 @@ Public API: the translator's output for moq-net's model. - [Drop in the TypeScript translator](/quest/m2/ts-drop.md) - kio closes a channel when its last producer drops - [Shared ownership](/quest/m2/ts-shared.md) - kio's waiters are `Arc`/`Weak` with observable counts, and its atomics and locks are plain values -- [Translate safe dependencies with the package](/quest/m2/ts-model/crates.md) - kio, moq-clock, moq-pattern, and rangemap translate as one program with moq-net - [Std gaps the model reaches](/quest/m2/ts-model/std-more.md) - ranges, wrappers, and `Vec`'s editing methods - [Options of values that can be undefined](/quest/m2/ts-model/nested-option.md) - `Option>` and `Option<()>` keep `None` apart from `Some(None)` - [Async in the TypeScript translator](/quest/m2/ts-async.md) - kio's producers and consumers are futures diff --git a/quest/m2/ts-model/crates.md b/quest/m2/ts-model/crates.md deleted file mode 100644 index dc11b578a..000000000 --- a/quest/m2/ts-model/crates.md +++ /dev/null @@ -1,33 +0,0 @@ -# [L] Translate safe dependencies with the package - -## Goal - -[rust2-ts](/ts/README.md) translates the dependencies `ts.toml` names together with the -package, as one program: their types and calls translate as the package's own do, proven -by differential tests against native Rust. moq-net translates with kio, moq-clock, -moq-pattern, and rangemap. - -## Plan - -Decided with the maintainer on 2026-10-27, from the -[model's](/quest/m2/ts-model/README.md) census: its roots refuse calls into kio, -moq-clock, and moq-pattern 2,588 times (`kio::Waiter`, `Ref::deref`, `Shared::lock`, -`moq_clock::Instant`), and 424 of the 1,112 items that refuse anything refuse nothing -else. rangemap adds 266. - -- One program: the driver lowers the named crates' MIR with the package's. A - `cargo check` encodes no MIR for a dependency's functions, and the driver wraps every - rustc, so it gives the named crates `-Zalways-encode-mir`. -- The driver passes `--cfg rust2_ts` to the package and the named crates, so moq's safe - twins of its internal `unsafe` are what translates. -- Each crate's output goes in its own module directory, and the instances a crate's - generics get from the package go with the crate that defines them. -- The rule is general: a dependency without `unsafe` translates from source; one with - `unsafe` is refused with a span unless the consumer [binds](/ts/README.md#bindings) - it. -- A named crate's own refusals belong to other quests: kio's atomics and `thread_local!` - to [shared ownership](/quest/m2/ts-shared.md), and its `RawWaker` vtable to bindings. -- Report the model's census before and after. - -Public API: `ts.toml` names the dependencies translated with the package; the output gains -their modules. diff --git a/quest/m3/ts-duration.md b/quest/m3/ts-duration.md index 99361c53b..ec717ea80 100644 --- a/quest/m3/ts-duration.md +++ b/quest/m3/ts-duration.md @@ -14,7 +14,7 @@ and panics, proven by differential tests against native Rust: compound assignmen Decided with the maintainer on 2026-10-27: rust2 aims to support all of std, so generic gaps are planned without a moq consumer. Nothing in moq-net, moq-clock, or kio uses these on a `Duration` itself today; moq-clock's `Instant += Duration` is its own `AddAssign`, -which translates from source with [crates](/quest/m2/ts-model/crates.md). +which translates from source as a [dependency](/ts/README.md). - `rt.Duration` is immutable, so compound assignment assigns a new value to the place, as `String::push` does for a JS string. diff --git a/ts/Cargo.lock b/ts/Cargo.lock index e16200c2a..18f7f76ed 100644 --- a/ts/Cargo.lock +++ b/ts/Cargo.lock @@ -34,6 +34,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "crates-dep" +version = "0.0.0" + +[[package]] +name = "crates-fixture" +version = "0.0.0" +dependencies = [ + "crates-dep", +] + [[package]] name = "equivalent" version = "1.0.3" diff --git a/ts/Cargo.toml b/ts/Cargo.toml index 581b54cbc..bd826a593 100644 --- a/ts/Cargo.toml +++ b/ts/Cargo.toml @@ -23,6 +23,10 @@ edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/kixelated/rust2" +[workspace.lints.rust] +# What rust2-ts sets where it translates, for a safe twin of `unsafe`. +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(rust2_ts)"] } + [workspace.lints.clippy] # Every suppression says why, so the next lint audit can judge it. allow_attributes_without_reason = "warn" diff --git a/ts/README.md b/ts/README.md index 03c6410ae..76650f4f2 100644 --- a/ts/README.md +++ b/ts/README.md @@ -10,7 +10,11 @@ It runs `cargo check` on the package with itself as `RUSTC_WRAPPER`, for `wasm32-unknown-unknown`, with std built from source for it, unwinding and with every function's MIR, which needs the toolchain's `rust-src`. Every crate compiles as rustc would; the configured library is also translated, after type and borrow checking, starting from its roots and following -what they reach. A construct outside the subset is a compile error with its span, and +what they reach. The dependencies the config names translate with it as one program: +cargo compiles each with every function's MIR, and their types and calls translate as +the package's own do. The package and those dependencies compile with `--cfg rust2_ts`, +so a crate can give translation a safe twin of its `unsafe`. Their `unsafe` is refused +where it is, as the package's is, unless a binding stands in for the item holding it. A construct outside the subset is a compile error with its span, and then nothing is written. A type without a JS value is refused where a value of it is made or received, such as a parameter, a field, or a call, rather than again at each variable holding one; any variable or temporary that holds a type whose JS value is ambiguous, such @@ -19,6 +23,9 @@ as `Option<()>`, is refused at its own span. ```toml # ts.toml, beside the package's Cargo.toml unless --manifest-path says otherwise. package = "moq-net" +# Packages it depends on whose libraries translate with it. Any other crate's function is +# std's mapping, a binding, or refused. +dependencies = ["kio", "moq-clock"] # Paths from the crate root: a function, or a type with its non-generic methods and its # impls of the crate's own traits, blanket ones included. roots = ["coding::Decoder", "lite::Group"] @@ -50,8 +57,9 @@ what they are, since each instance decides that. A census succeeds whatever it f translation still fails on any refusal. `just ts census` runs it at the pinned moq revision, into `target/census`, on moq-net -whole (`moq/net.toml`), on its model's public types with their boundaries bound -(`moq/model.toml`), and on kio, and the nightly job publishes their tables. +whole (`moq/net.toml`), on its model's public types with their boundaries bound and the +crates they call translated with them (`moq/model.toml`), and on kio, and the nightly job +publishes their tables. ## Output @@ -63,7 +71,10 @@ runtime: what std does that JS does not, with iterators in `rt/iter.ts`, `str`'s in `rt/str.ts`, collections in `rt/collections.ts`, ownership and async in `rt/task.ts`, and `Duration` in `rt/time.ts`. What translates from std's own MIR, `Option`'s and `Result`'s combinators, goes in a module under its crate's name, as -`core/option.ts` holds `map$Option_u64_u64_rescale_closure0`. +`core/option.ts` holds `map$Option_u64_u64_rescale_closure0`. A dependency's modules go +under its crate's name too, `kio.ts` and `kio/lock.ts`, with the instances of its generic +functions that the package's types make, so a module of the package's named for a +translated crate is refused. | Rust | TypeScript | | --- | --- | @@ -492,6 +503,13 @@ built natively for wasm32 and run beside its translation on the same inputs by differential test runs them against native Rust. `wrong.toml` binds TS that doesn't match the Rust signatures, which `tsc` refuses at each item, with the errors in `wrong.txt`. `refused.toml` holds bindings the config can't make, with the errors in `refused.txt`. +- `crates` translates with `crates/dep`, which writes into a `Copy` type only the package + copies, ranks the package's type in a generic function, drops, and reads through a raw + pointer behind a safe twin under `rust2_ts` and in a function `bind/last.ts` stands in + for. Its differential test runs them against native Rust. `refused.toml` drops the + binding, `unknown.toml` names a package it doesn't depend on, and its `shared` feature + adds a module of the dependency's name, with the errors in `refused.txt`, `unknown.txt`, + and `shared.txt`. - `reject` holds constructs outside the subset; its expected errors are `expected.txt`, and its expected census is `census.md`. diff --git a/ts/fixtures/crates/Cargo.toml b/ts/fixtures/crates/Cargo.toml new file mode 100644 index 000000000..1caefa410 --- /dev/null +++ b/ts/fixtures/crates/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "crates-fixture" +description = "A package that translates with a dependency as one program" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false + +[lib] +# The cdylib is the native side of the differential test, built for wasm32. +crate-type = ["lib", "cdylib"] + +[features] +# A module of the dependency's name, whose files would be the dependency's. +shared = [] + +[dependencies] +crates-dep = { path = "dep" } + +[lints] +workspace = true diff --git a/ts/fixtures/crates/bind/last.ts b/ts/fixtures/crates/bind/last.ts new file mode 100644 index 000000000..c7255edbb --- /dev/null +++ b/ts/fixtures/crates/bind/last.ts @@ -0,0 +1,8 @@ +// `crates_dep::last`, which reads through a raw pointer. + +import * as rt from "../rt.ts"; + +export function last(bytes: rt.ByteSlice): rt.u8 { + if (bytes.length === 0) rt.panic("no last byte"); + return bytes.data[bytes.start + bytes.length - 1] as rt.u8; +} diff --git a/ts/fixtures/crates/dep/Cargo.toml b/ts/fixtures/crates/dep/Cargo.toml new file mode 100644 index 000000000..f715c28b6 --- /dev/null +++ b/ts/fixtures/crates/dep/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "crates-dep" +description = "The dependency crates-fixture translates with" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false + +[lints] +workspace = true diff --git a/ts/fixtures/crates/dep/src/lib.rs b/ts/fixtures/crates/dep/src/lib.rs new file mode 100644 index 000000000..989445ace --- /dev/null +++ b/ts/fixtures/crates/dep/src/lib.rs @@ -0,0 +1,79 @@ +//! The dependency `crates-fixture` translates with: a type the package copies, which only +//! this crate writes into, a generic function over the package's type, drops, and +//! `unsafe` with a safe twin where it translates and without one. + +use std::sync::{Arc, Mutex}; + +#[derive(Clone, Copy)] +pub struct Point { + pub x: i32, + pub y: i32, +} + +impl Point { + /// Move it by `(dx, dy)`: the only write into a `Point` in either crate. + pub fn shift(&mut self, dx: i32, dy: i32) { + self.x += dx; + self.y += dy; + } +} + +/// What [`heaviest`] ranks by. +pub trait Weigh { + fn weight(&self) -> u32; +} + +/// The heaviest of `items`, the first of equals. +pub fn heaviest(items: &[T]) -> Option<&T> { + let mut best: Option<&T> = None; + for item in items { + match best { + Some(b) if b.weight() >= item.weight() => {} + _ => best = Some(item), + } + } + best +} + +/// Notes its id in a log when it drops. +pub struct Guard { + id: u8, + log: Arc>>, +} + +impl Guard { + pub fn new(id: u8, log: &Arc>>) -> Self { + Self { id, log: log.clone() } + } +} + +impl Drop for Guard { + fn drop(&mut self) { + self.log.lock().unwrap().push(self.id); + } +} + +/// The first byte of `bytes`. +pub fn first(bytes: &[u8]) -> u8 { + assert!(!bytes.is_empty(), "no first byte"); + read(bytes) +} + +#[cfg(not(rust2_ts))] +fn read(bytes: &[u8]) -> u8 { + // SAFETY: `first` checked it holds a byte. + unsafe { *bytes.as_ptr() } +} + +/// `read`'s safe twin, which translates. +#[cfg(rust2_ts)] +fn read(bytes: &[u8]) -> u8 { + bytes[0] +} + +/// The last byte of `bytes`, which only a binding translates. +pub fn last(bytes: &[u8]) -> u8 { + assert!(!bytes.is_empty(), "no last byte"); + // SAFETY: it holds a byte. + unsafe { *bytes.as_ptr().add(bytes.len() - 1) } +} diff --git a/ts/fixtures/crates/expected/bind/last.ts b/ts/fixtures/crates/expected/bind/last.ts new file mode 100644 index 000000000..7ada99ffe --- /dev/null +++ b/ts/fixtures/crates/expected/bind/last.ts @@ -0,0 +1,9 @@ +// Generated by rust2-ts. Do not edit. +// `crates_dep::last`, which reads through a raw pointer. + +import * as rt from "../rt.ts"; + +export function last(bytes: rt.ByteSlice): rt.u8 { + if (bytes.length === 0) rt.panic("no last byte"); + return bytes.data[bytes.start + bytes.length - 1] as rt.u8; +} diff --git a/ts/fixtures/crates/expected/crates_dep.Guard.ts b/ts/fixtures/crates/expected/crates_dep.Guard.ts new file mode 100644 index 000000000..33cbc39e3 --- /dev/null +++ b/ts/fixtures/crates/expected/crates_dep.Guard.ts @@ -0,0 +1,40 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +/** Notes its id in a log when it drops. */ +export class Guard { + id: rt.u8; + log: rt.Arc>; + + constructor(fields: { id: rt.u8; log: rt.Arc> }) { + this.id = fields.id; + this.log = fields.log; + } + + [Symbol.dispose](): void { + let dropped: number = 0; + try { + drop$Drop(this); + dropped = 1; + rt.drop(this.log); + } catch (e) { + if (dropped < 1) { + rt.drop(this.log); + } + throw e; + } + } +} + +function new_(id: rt.u8, log: rt.Arc>): Guard { + return new Guard({ id, log: rt.arcClone(log) }); +} + +export function drop$Drop(self: Guard): void { + rt.byteVecPush(self.log.value.value, self.id); +} + +export { new_ as new }; + +//# sourceMappingURL=crates_dep.Guard.ts.map diff --git a/ts/fixtures/crates/expected/crates_dep.Point.ts b/ts/fixtures/crates/expected/crates_dep.Point.ts new file mode 100644 index 000000000..feb16da0c --- /dev/null +++ b/ts/fixtures/crates/expected/crates_dep.Point.ts @@ -0,0 +1,16 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +export interface Point { + x: rt.i32; + y: rt.i32; +} + +/** Move it by `(dx, dy)`: the only write into a `Point` in either crate. */ +export function shift(self: Point, dx: rt.i32, dy: rt.i32): void { + self.x = rt.i32Add(self.x, dx); + self.y = rt.i32Add(self.y, dy); +} + +//# sourceMappingURL=crates_dep.Point.ts.map diff --git a/ts/fixtures/crates/expected/crates_dep.ts b/ts/fixtures/crates/expected/crates_dep.ts new file mode 100644 index 000000000..0c416c121 --- /dev/null +++ b/ts/fixtures/crates/expected/crates_dep.ts @@ -0,0 +1,38 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import * as Item from "./harness.Item.ts"; + +/** The heaviest of `items`, the first of equals. */ +export function heaviest$Item(items: Item.Item[]): Item.Item | undefined { + let best = undefined as Item.Item | undefined; + const iterS: Item.Item[] = items; + let iterI: number = 0; + while (true) { + const _5: Item.Item | rt.Done = iterI < iterS.length ? iterS[iterI++] : rt.DONE; + if (_5 === rt.DONE) { + return best; + } + if (best !== undefined) { + if (Item.weight$Weigh(best) >= Item.weight$Weigh(_5)) { + continue; + } + } + best = _5; + } +} + +/** The first byte of `bytes`. */ +export function first(bytes: rt.ByteSlice): rt.u8 { + if (!(bytes.length === 0)) { + return read(bytes); + } + rt.panicFmt(["no first byte"]); +} + +export function read(bytes: rt.ByteSlice): rt.u8 { + rt.idx(bytes, 0); + return bytes.data[bytes.start]; +} + +//# sourceMappingURL=crates_dep.ts.map diff --git a/ts/fixtures/crates/expected/harness.Item.ts b/ts/fixtures/crates/expected/harness.Item.ts new file mode 100644 index 000000000..585336d5b --- /dev/null +++ b/ts/fixtures/crates/expected/harness.Item.ts @@ -0,0 +1,14 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +/** The package's own type, which the dependency's generic function ranks. */ +export interface Item { + _0: rt.u8; +} + +export function weight$Weigh(self: Item): rt.u32 { + return rt.u32Rem(self._0 as rt.u32, 7); +} + +//# sourceMappingURL=harness.Item.ts.map diff --git a/ts/fixtures/crates/expected/harness.ts b/ts/fixtures/crates/expected/harness.ts new file mode 100644 index 000000000..6d6fc01ad --- /dev/null +++ b/ts/fixtures/crates/expected/harness.ts @@ -0,0 +1,100 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; +import { copy$Point } from "./lib.ts"; +import * as last from "./bind/last.ts"; +import { heaviest$Item, first } from "./crates_dep.ts"; +import * as Guard from "./crates_dep.Guard.ts"; +import * as Point from "./crates_dep.Point.ts"; +import type * as Item from "./harness.Item.ts"; + +export function run(input: rt.ByteSlice): rt.ByteVec { + let log!: rt.Arc>; + let guards!: Guard.Guard[]; + let held!: Guard.Guard; + let _late!: Guard.Guard; + let _71!: boolean; + let _72!: boolean; + let unwind: number = 0; + try { + _72 = false; + _71 = false; + const out: rt.ByteVec = new rt.ByteVec(); + if (input.length >= 2) { + const p: Point.Point = { x: input.data[input.start] as rt.i32, y: input.data[input.start + 1] as rt.i32 }; + const q: Point.Point = copy$Point(p); + Point.shift(q, 1, -1); + rt.iterExtendBytes(out, rt.iterSlice(Uint8Array.of(p.x & 0xff, p.y & 0xff, q.x & 0xff, q.y & 0xff))); + } + const _29: Item.Item | undefined = heaviest$Item(rt.iterToArray(rt.iterMap(rt.iterSlice(rt.byteSliceView(input)), (_2: rt.u8): Item.Item => { + return { _0: _2 }; + }))); + if (_29 !== undefined) { + rt.byteVecPush(out, _29._0); + } + log = new rt.Arc(new rt.Mutex(new rt.ByteVec())); + unwind = 41; + guards = rt.iterToArray(rt.iterMap(rt.iterTake(rt.iterSlice(rt.byteSliceView(input)), 3), (_26: rt.u8): Guard.Guard => { + return Guard.new(_26, log); + }), false, undefined, rt.drop); + unwind = 47; + _72 = true; + held = Guard.new(255, log); + unwind = 45; + _71 = true; + _72 = false; + rt.dropAll(guards); + _late = Guard.new(254, log); + unwind = 40; + _71 = false; + rt.drop(held); + unwind = 45; + rt.drop(_late); + unwind = 41; + _71 = false; + _72 = false; + const _60: rt.Mutex = log.value; + unwind = 39; + const _56: rt.Iter = rt.iterSlice(rt.byteSliceView(rt.byteVecSlice(_60.value))); + rt.iterExtendBytes(out, _56); + unwind = 41; + const _66: rt.u8 = first(input); + rt.byteVecPush(out, _66); + const _69: rt.u8 = last.last(input); + rt.byteVecPush(out, _69); + unwind = 0; + rt.drop(log); + return out; + } catch (e) { + b2: { + b3: { + if (unwind === 0) { + throw e; + } + if (unwind === 39) { + break b2; + } + if (unwind === 40) { + rt.drop(_late); + } else { + if (unwind === 41) { + break b2; + } + if (unwind !== 45) { + break b3; + } + } + if (_71) { + rt.drop(held); + } + } + if (_72) { + rt.dropAll(guards); + } + } + rt.drop(log); + throw e; + } +} + +//# sourceMappingURL=harness.ts.map diff --git a/ts/fixtures/crates/expected/lib.ts b/ts/fixtures/crates/expected/lib.ts new file mode 100644 index 000000000..dbf13619b --- /dev/null +++ b/ts/fixtures/crates/expected/lib.ts @@ -0,0 +1,9 @@ +// Generated by rust2-ts. Do not edit. + +import type * as Point from "./crates_dep.Point.ts"; + +export function copy$Point(v: Point.Point): Point.Point { + return { x: v.x, y: v.y }; +} + +//# sourceMappingURL=lib.ts.map diff --git a/ts/fixtures/crates/refused.toml b/ts/fixtures/crates/refused.toml new file mode 100644 index 000000000..a46001a92 --- /dev/null +++ b/ts/fixtures/crates/refused.toml @@ -0,0 +1,5 @@ +# The dependency's `unsafe` without the binding that stands in for it: `refused.txt` holds +# the errors. +package = "crates-fixture" +dependencies = ["crates-dep"] +roots = ["harness::run"] diff --git a/ts/fixtures/crates/refused.txt b/ts/fixtures/crates/refused.txt new file mode 100644 index 000000000..b6befdd97 --- /dev/null +++ b/ts/fixtures/crates/refused.txt @@ -0,0 +1,3 @@ +unsupported call to `core::slice::::as_ptr` at fixtures/crates/dep/src/lib.rs:78:18 +unsupported call to `std::ptr::const_ptr::::add` at fixtures/crates/dep/src/lib.rs:78:27 +a raw pointer dereferenced at fixtures/crates/dep/src/lib.rs:78:11 diff --git a/ts/fixtures/crates/shared.txt b/ts/fixtures/crates/shared.txt new file mode 100644 index 000000000..195eae720 --- /dev/null +++ b/ts/fixtures/crates/shared.txt @@ -0,0 +1 @@ +module `crates_dep` would share its files with the crate of its name at fixtures/crates/src/lib.rs:7:1 diff --git a/ts/fixtures/crates/src/harness.rs b/ts/fixtures/crates/src/harness.rs new file mode 100644 index 000000000..ea00861e1 --- /dev/null +++ b/ts/fixtures/crates/src/harness.rs @@ -0,0 +1,45 @@ +//! The entry point both sides of the differential test run. + +use std::sync::{Arc, Mutex}; + +use crates_dep::{Guard, Point, Weigh}; + +/// The package's own type, which the dependency's generic function ranks. +struct Item(u8); + +impl Weigh for Item { + fn weight(&self) -> u32 { + self.0 as u32 % 7 + } +} + +pub fn run(input: &[u8]) -> Vec { + let mut out = Vec::new(); + // Only the dependency writes into a `Point`, so the copy must not share it. + if let [a, b, ..] = input { + let p = Point { + x: *a as i32, + y: *b as i32, + }; + let mut q = p; + q.shift(1, -1); + out.extend([p.x as u8, p.y as u8, q.x as u8, q.y as u8]); + } + let items: Vec = input.iter().map(|&byte| Item(byte)).collect(); + if let Some(Item(byte)) = crates_dep::heaviest(&items) { + out.push(*byte); + } + // The dependency's drops, where native Rust runs them. + let log = Arc::new(Mutex::new(Vec::new())); + { + let guards: Vec = input.iter().take(3).map(|&id| Guard::new(id, &log)).collect(); + let held = Guard::new(0xff, &log); + drop(guards); + let _late = Guard::new(0xfe, &log); + drop(held); + } + out.extend(log.lock().unwrap().iter()); + out.push(crates_dep::first(input)); + out.push(crates_dep::last(input)); + out +} diff --git a/ts/fixtures/crates/src/lib.rs b/ts/fixtures/crates/src/lib.rs new file mode 100644 index 000000000..106c699e2 --- /dev/null +++ b/ts/fixtures/crates/src/lib.rs @@ -0,0 +1,12 @@ +//! A package that translates with its dependency, `crates-dep`, as one program. +//! +//! `harness::run` is the entry point both sides of the differential test run. + +pub mod harness; +#[cfg(feature = "shared")] +mod crates_dep {} +#[cfg(target_arch = "wasm32")] +#[path = "../../wasm.rs"] +mod wasm; +#[cfg(target_arch = "wasm32")] +use harness::run as entry; diff --git a/ts/fixtures/crates/ts.toml b/ts/fixtures/crates/ts.toml new file mode 100644 index 000000000..6d2909b77 --- /dev/null +++ b/ts/fixtures/crates/ts.toml @@ -0,0 +1,6 @@ +package = "crates-fixture" +dependencies = ["crates-dep"] +roots = ["harness::run"] + +[bind] +"crates_dep::last" = "bind/last.ts" diff --git a/ts/fixtures/crates/unknown.toml b/ts/fixtures/crates/unknown.toml new file mode 100644 index 000000000..c4cda7a2e --- /dev/null +++ b/ts/fixtures/crates/unknown.toml @@ -0,0 +1,4 @@ +# A dependency the package doesn't have: `unknown.txt` holds the error. +package = "crates-fixture" +dependencies = ["crates-dep", "bytes"] +roots = ["harness::run"] diff --git a/ts/fixtures/crates/unknown.txt b/ts/fixtures/crates/unknown.txt new file mode 100644 index 000000000..d9b7210b4 --- /dev/null +++ b/ts/fixtures/crates/unknown.txt @@ -0,0 +1 @@ +`bytes` in `dependencies` is no library the package depends on diff --git a/ts/moq/model.toml b/ts/moq/model.toml index 042528810..219123317 100644 --- a/ts/moq/model.toml +++ b/ts/moq/model.toml @@ -1,6 +1,8 @@ # moq-net's model, for the census: its public types, with what the subset refuses for good # bound. moq writes the binding files; a census reads none. package = "moq-net" +# The workspace crates its model calls, and rangemap, which translate with it. +dependencies = ["kio", "moq-clock", "moq-pattern", "rangemap"] roots = [ "model::origin::Producer", "model::origin::Consumer", diff --git a/ts/src/census.rs b/ts/src/census.rs index 579ed8af0..b053747f7 100644 --- a/ts/src/census.rs +++ b/ts/src/census.rs @@ -165,6 +165,7 @@ const VERBATIM: &[&str] = &[ "break", "collect", "copy_nonoverlapping", + "dependencies", "flatten", "fold", "for", diff --git a/ts/src/cli.rs b/ts/src/cli.rs index 34f93b681..d940d58c0 100644 --- a/ts/src/cli.rs +++ b/ts/src/cli.rs @@ -5,6 +5,7 @@ use std::process::ExitCode; use rust2_driver::cargo::{Args, Check, Output}; use crate::config::{self, Config}; +use crate::driver; /// Translation type-checks for this target: its 32-bit `usize` is what a JS number /// holds exactly, and its `cfg`s are the browser's. @@ -45,6 +46,7 @@ fn try_run(args: &[String]) -> Result { ("CARGO_UNSTABLE_PROFILE_RUSTFLAGS", "true".as_ref()), ]; let mut cargo = std_from_source(); + cargo.extend(dependencies(&config.dependencies)); cargo.extend(args.cargo); Check { tool: "rust2-ts", @@ -72,3 +74,25 @@ fn std_from_source() -> Vec { } args } + +/// Cargo args that compile each dependency that translates with the package as the +/// package compiles, and with every function's MIR, which a `cargo check` encodes for no +/// dependency's functions otherwise. +fn dependencies(packages: &[String]) -> Vec { + let mut flags = vec!["-Zalways-encode-mir"]; + flags.extend(driver::FLAGS); + let flags = flags + .iter() + .map(|flag| format!("{flag:?}")) + .collect::>() + .join(", "); + packages + .iter() + .flat_map(|package| { + [ + "--config".into(), + format!("profile.dev.package.{package:?}.rustflags=[{flags}]"), + ] + }) + .collect() +} diff --git a/ts/src/config.rs b/ts/src/config.rs index 5330c9bab..9a0601140 100644 --- a/ts/src/config.rs +++ b/ts/src/config.rs @@ -11,6 +11,9 @@ use serde::Deserialize; pub struct Config { /// The cargo package whose library is translated. pub package: String, + /// Cargo packages it depends on whose libraries translate with it, as one program. + #[serde(default)] + pub dependencies: Vec, /// Paths from the crate root to the items to translate: a function, or a type with /// its non-generic methods. Everything they reach comes along. pub roots: Vec, diff --git a/ts/src/driver.rs b/ts/src/driver.rs index bf146bd9f..9f91a1984 100644 --- a/ts/src/driver.rs +++ b/ts/src/driver.rs @@ -11,20 +11,24 @@ use rustc_middle::ty::TyCtxt; use crate::config::{self, Config}; use crate::{lower, program}; +/// What rustc compiles each crate that translates with: the package here, and the +/// dependencies it names through cargo's profile, which cargo's fingerprint sees. +/// +/// MIR without the checks of undefined behavior that debug builds add around raw +/// pointers, such as a box's: JS has no pointers to check. And with the cleanups a panic +/// unwinds through, which wasm32's default of aborting leaves out: a JS caller can catch +/// a panic and go on. And with every call where the source has it, as a release build's +/// inlining would not keep it, since a call is what a binding or a std function's +/// translation stands in for. And with `rust2_ts` set, so a crate can give translation +/// a safe twin of its `unsafe`. +pub const FLAGS: &[&str] = &["-Zub-checks=no", "-Cpanic=unwind", "-Zinline-mir=no", "--cfg=rust2_ts"]; + /// Run as rustc, with `args` as cargo passed them: the wrapped rustc's path first. pub fn run(args: Vec) -> ExitCode { rust2_driver::rustc::run("rust2-ts", args, |args| { let job = Translate::for_args(args)?; - // MIR without the checks of undefined behavior that debug builds add around raw - // pointers, such as a box's: JS has no pointers to check. And with the cleanups a - // panic unwinds through, which wasm32's default of aborting leaves out: a JS caller - // can catch a panic and go on. And with every call where the source has it, as a - // release build's inlining would not keep it, since a call is what a binding or a - // std function's translation stands in for. if job.is_some() { - args.push("-Zub-checks=no".into()); - args.push("-Cpanic=unwind".into()); - args.push("-Zinline-mir=no".into()); + args.extend(FLAGS.iter().map(|flag| flag.to_string())); } Ok(job.map(|job| Box::new(job) as rust2_driver::rustc::Job)) }) diff --git a/ts/src/lower/bind.rs b/ts/src/lower/bind.rs index d0400dd4d..469ef43fc 100644 --- a/ts/src/lower/bind.rs +++ b/ts/src/lower/bind.rs @@ -141,7 +141,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }, ); } - self.writes = super::copy::Writes::of_crate(tcx, &|def_id| self.bound(def_id)); + self.writes = super::copy::Writes::of_crate(tcx, &self.crates.bodies(tcx), &|def_id| self.own(def_id)); } /// The item a binding's path names: from the crate root, or else from a crate the crate @@ -196,9 +196,9 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { (adt.did().krate == def_id.krate && self.bound_drops(adt.did()).is_some()).then_some(adt.did()) } - /// Whether `def_id` is the crate's own, which it translates, rather than bound. + /// Whether `def_id` is of a crate that translates, rather than bound. pub fn own(&self, def_id: DefId) -> bool { - def_id.is_local() && !self.bindings.items.contains_key(&def_id) + self.crates.translates(def_id) && !self.bindings.items.contains_key(&def_id) } /// Why `def_id`, which isn't [`own`](Self::own), isn't the crate's to translate. diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 747f33372..a52c6eedf 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -206,7 +206,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } } -/// The types the crate's code writes into in place: through a projection, as `p.x = 1` +/// The types the translated crates' code writes into in place: through a projection, as `p.x = 1` /// writes into `p`; through a reference or a box, as `*r = v` does into what `r` points /// at; or by anything code it doesn't read can do with a mutable reference or pointer to /// one, which counts as a write: one a parameter of a std function the crate names takes, @@ -225,13 +225,13 @@ pub struct Writes<'tcx> { } impl<'tcx> Writes<'tcx> { - /// The writes of every function, closure, and coroutine body in the crate but those - /// `bound` says a binding stands in for, which count as code it doesn't read. - pub fn of_crate(tcx: TyCtxt<'tcx>, bound: &dyn Fn(DefId) -> bool) -> Self { + /// The writes of every function, closure, and coroutine body of `bodies` that `own` + /// says translates: a binding stands in for any other, which counts as code it doesn't + /// read. + pub fn of_crate(tcx: TyCtxt<'tcx>, bodies: &[DefId], own: &dyn Fn(DefId) -> bool) -> Self { let mut writes = Writes::default(); - for &def_id in tcx.mir_keys(()) { - let def_id = def_id.to_def_id(); - if !tcx.def_kind(def_id).is_fn_like() || bound(def_id) { + for &def_id in bodies { + if !tcx.def_kind(def_id).is_fn_like() || !own(def_id) { continue; } let body = tcx.optimized_mir(def_id); @@ -240,7 +240,7 @@ impl<'tcx> Writes<'tcx> { body, def_id, writes: &mut writes, - bound, + own, } .visit_body(body); } @@ -364,7 +364,7 @@ struct BodyWrites<'w, 'm, 'tcx> { body: &'m mir::Body<'tcx>, def_id: DefId, writes: &'w mut Writes<'tcx>, - bound: &'w dyn Fn(DefId) -> bool, + own: &'w dyn Fn(DefId) -> bool, } impl<'tcx> BodyWrites<'_, '_, 'tcx> { @@ -394,13 +394,13 @@ impl<'tcx> BodyWrites<'_, '_, 'tcx> { } } - /// Whether the function item `def_id` with `args` is the crate's own, whose body is + /// Whether the function item `def_id` with `args` translates, whose body is /// read, rather than std's or a trait method only an instance resolves. fn crate_fn(&self, def_id: DefId, args: ty::GenericArgsRef<'tcx>) -> bool { let env = ty::TypingEnv::post_analysis(self.tcx, self.def_id); matches!( ty::Instance::try_resolve(self.tcx, env, def_id, args), - Ok(Some(instance)) if matches!(instance.def, ty::InstanceKind::Item(did) if did.is_local() && !(self.bound)(did)) + Ok(Some(instance)) if matches!(instance.def, ty::InstanceKind::Item(did) if (self.own)(did)) ) } } diff --git a/ts/src/lower/drop.rs b/ts/src/lower/drop.rs index b7c3b2828..aa9c63c3b 100644 --- a/ts/src/lower/drop.rs +++ b/ts/src/lower/drop.rs @@ -55,7 +55,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { false => self.fields_drop(*adt, args, true), }, Some(_) => false, - None if adt.did().is_local() => adt.has_dtor(tcx) || self.fields_drop(*adt, args, true), + None if self.crates.translates(adt.did()) => adt.has_dtor(tcx) || self.fields_drop(*adt, args, true), None => false, }, ty::Tuple(tys) => tys.iter().any(|t| self.drops(t)), @@ -159,7 +159,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { || self.iter_item(ty).is_some_and(|item| self.drop_panics(item, seen)) } Some(_) => args.types().any(|t| self.drop_panics(t, seen)), - None if adt.did().is_local() => { + None if self.crates.translates(adt.did()) => { let env = self.typing_env_pub(); adt.has_dtor(tcx) || adt.all_fields().any(|field| { @@ -279,7 +279,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { fn drop_glue_call(&mut self, value: Expr, ty: Ty<'tcx>, span: Span) -> Option { let rustc = match ty.kind() { ty::Tuple(_) => true, - ty::Adt(adt, _) => adt.is_enum() || adt.did().is_local(), + ty::Adt(adt, _) => adt.is_enum() || self.crates.translates(adt.did()), _ => false, }; if rustc && let Some(sym) = super::mir::glue::shim(self, ty, span) { diff --git a/ts/src/lower/library.rs b/ts/src/lower/library.rs index f8da3d846..ccaa43eb8 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -367,7 +367,7 @@ pub fn clone_copies<'tcx>(t: &super::Translator<'_, 'tcx>, ty: Ty<'tcx>) -> bool ty::Array(elem, _) => clone_copies(t, *elem), // A binding's `Clone` is its own. ty::Adt(adt, _) if t.bound_drops(adt.did()).is_some() => false, - ty::Adt(adt, args) if !adt.did().is_local() => args.types().all(|arg| clone_copies(t, arg)), + ty::Adt(adt, args) if !t.crates.translates(adt.did()) => args.types().all(|arg| clone_copies(t, arg)), ty::Adt(adt, args) => { let Some(clone) = tcx.lang_items().clone_fn() else { return false; diff --git a/ts/src/lower/mir/coroutine.rs b/ts/src/lower/mir/coroutine.rs index 9143cd50a..aaedbc137 100644 --- a/ts/src/lower/mir/coroutine.rs +++ b/ts/src/lower/mir/coroutine.rs @@ -72,14 +72,12 @@ pub fn info<'tcx>(t: &mut Translator<'_, 'tcx>, def_id: DefId, args: GenericArgs init: FieldInit::Value(Expr::lit("0")), }); let coroutine = args.as_coroutine(); - let names: Vec = match def_id.as_local() { - Some(local) => tcx - .closure_captures(local) - .iter() - .map(|c| crate::lower::camel(c.to_symbol().as_str())) - .collect(), - None => Vec::new(), - }; + // Named as debuginfo names them, which marks a capture by reference. + let names: Vec = tcx + .closure_saved_names_of_captured_variables(def_id) + .iter() + .map(|name| crate::lower::camel(name.as_str().trim_start_matches("_ref__"))) + .collect(); let mut upvars = Vec::new(); for (i, ty) in coroutine.upvar_tys().iter().enumerate() { let name = format!("#{}", names.get(i).cloned().unwrap_or_else(|| format!("u{i}"))); diff --git a/ts/src/lower/mir/decided.rs b/ts/src/lower/mir/decided.rs index 441f88900..020ff9a56 100644 --- a/ts/src/lower/mir/decided.rs +++ b/ts/src/lower/mir/decided.rs @@ -22,9 +22,11 @@ use rustc_middle::mir::{ }; use rustc_middle::ty::{self, Instance, Ty, TyCtxt}; +use crate::lower::Crates; + /// Fold each switch in `body` that an enclosing switch decided. -pub fn fold<'tcx>(tcx: TyCtxt<'tcx>, body: &mut mir::Body<'tcx>) { - let flow = Flow::new(tcx, body); +pub fn fold<'tcx>(tcx: TyCtxt<'tcx>, crates: &Crates, body: &mut mir::Body<'tcx>) { + let flow = Flow::new(tcx, crates, body); let entry = flow.solve(body); let mut folded = false; for bb in body.basic_blocks.indices() { @@ -159,8 +161,8 @@ impl<'tcx> Narrowing<'tcx> { } /// Where `body`'s tests and payload reads must see a place's whole type. -pub fn narrowing<'tcx>(tcx: TyCtxt<'tcx>, body: &mir::Body<'tcx>) -> Narrowing<'tcx> { - let mut flow = Flow::new(tcx, body); +pub fn narrowing<'tcx>(tcx: TyCtxt<'tcx>, crates: &Crates, body: &mir::Body<'tcx>) -> Narrowing<'tcx> { + let mut flow = Flow::new(tcx, crates, body); // A widened test narrows nothing, which can leave a later test to widen too. loop { let mut seen = Narrowing::default(); @@ -182,8 +184,9 @@ pub fn narrowing<'tcx>(tcx: TyCtxt<'tcx>, body: &mir::Body<'tcx>) -> Narrowing<' type Decided = (usize, Vec<(u128, BasicBlock)>, BasicBlock); /// What a body's places are, for the dataflow. -struct Flow<'tcx> { +struct Flow<'c, 'tcx> { tcx: TyCtxt<'tcx>, + crates: &'c Crates, /// Each reference local whose one write is an assignment, as the place it borrows or /// the place behind the reference it copies. borrows: IndexVec>>, @@ -198,8 +201,8 @@ struct Flow<'tcx> { widened: FxHashSet, } -impl<'tcx> Flow<'tcx> { - fn new(tcx: TyCtxt<'tcx>, body: &mir::Body<'tcx>) -> Self { +impl<'c, 'tcx> Flow<'c, 'tcx> { + fn new(tcx: TyCtxt<'tcx>, crates: &'c Crates, body: &mir::Body<'tcx>) -> Self { let n = body.local_decls.len(); let mut writes = Writes(IndexVec::from_elem_n(0, n)); writes.visit_body(body); @@ -254,6 +257,7 @@ impl<'tcx> Flow<'tcx> { } let mut flow = Self { tcx, + crates, borrows, kept, escaped: IndexVec::from_elem_n(false, n), @@ -344,7 +348,7 @@ impl<'tcx> Flow<'tcx> { /// is reached through a pointer it holds. fn lent(&self, body: &mir::Body<'tcx>, place: Place<'tcx>) -> Option) -> bool + use<'tcx>> { let ty = place.ty(&body.local_decls, self.tcx).ty; - if !lends(self.tcx, ty) { + if !lends(self.tcx, self.crates, ty) { return None; } let base = self.canon(place); @@ -374,7 +378,7 @@ impl<'tcx> Flow<'tcx> { destination.as_local().is_some_and(|local| self.conditions[local]) && !matches!( Instance::try_resolve(self.tcx, body.typing_env(self.tcx), def, args), - Ok(Some(instance)) if instance.def_id().is_local() + Ok(Some(instance)) if self.crates.translates(instance.def_id()) ) } @@ -719,7 +723,7 @@ fn union(a: &[u128], b: &[u128]) -> Vec { /// Whether a value of `ty` can change what it points at: it is or holds a `&mut` or a /// `*mut`, or hides what it holds. -fn lends<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> bool { +fn lends<'tcx>(tcx: TyCtxt<'tcx>, crates: &Crates, ty: Ty<'tcx>) -> bool { let mut seen = FxHashSet::default(); let mut stack = vec![ty]; while let Some(ty) = stack.pop() { @@ -730,7 +734,7 @@ fn lends<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> bool { | ty::Dynamic(..) | ty::Alias(..) | ty::Param(_) => return true, - ty::Adt(adt, args) if adt.did().is_local() && seen.insert(t) => { + ty::Adt(adt, args) if crates.translates(adt.did()) && seen.insert(t) => { stack.extend(adt.all_fields().map(|field| field.ty(tcx, args).skip_norm_wip())); } _ => {} diff --git a/ts/src/lower/mir/glue.rs b/ts/src/lower/mir/glue.rs index c4ce097ab..ebffca868 100644 --- a/ts/src/lower/mir/glue.rs +++ b/ts/src/lower/mir/glue.rs @@ -38,7 +38,7 @@ pub fn shim<'tcx>(t: &mut Translator<'_, 'tcx>, ty: Ty<'tcx>, span: Span) -> Opt t.drop_shims.insert(ty, sym); let rustc = match ty.kind() { ty::Tuple(_) => true, - ty::Adt(adt, _) => adt.is_enum() || adt.did().is_local(), + ty::Adt(adt, _) => adt.is_enum() || t.crates.translates(adt.did()), _ => false, }; // The shim takes a pointer, which is the value in JS. diff --git a/ts/src/lower/mir/mod.rs b/ts/src/lower/mir/mod.rs index d1bd3c203..a2962ad7a 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -80,13 +80,13 @@ fn instance_mir<'tcx>(t: &mut Translator<'_, 'tcx>, instance: Instance<'tcx>, sp let mut body = instance.instantiate_mir_and_normalize_erasing_regions(tcx, t.typing_env_pub(), EarlyBinder::bind(tcx, body)); vec::literals(tcx, &mut body); - if !def_id.is_local() + if !t.crates.translates(def_id) && let Some(why) = std_refused(tcx, &body) { t.err(span, why); return None; } - decided::fold(tcx, &mut body); + decided::fold(tcx, &t.crates, &mut body); Some(body) } @@ -308,6 +308,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { pub fn new(b: &'m mut Body<'b, 'a, 'tcx>, mir: &'m mir::Body<'tcx>, env: Env, span: Span) -> Self { let tcx = b.t.tcx; let n = mir.local_decls.len(); + let narrowing = decided::narrowing(tcx, &b.t.crates, mir); let mut fx = Fx { b, mir, @@ -362,7 +363,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { scalars: FxHashMap::default(), steady: FxHashSet::default(), scalar_binds: Vec::new(), - narrowing: decided::narrowing(tcx, mir), + narrowing, at: mir::START_BLOCK.start_location(), }; fx.analyze(); diff --git a/ts/src/lower/mir/place.rs b/ts/src/lower/mir/place.rs index 2ce916c83..f07c6799b 100644 --- a/ts/src/lower/mir/place.rs +++ b/ts/src/lower/mir/place.rs @@ -348,7 +348,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { /// discriminants. `tsc` narrows only what a switch tests. fn as_variant(&mut self, base: Expr, ty: Ty<'tcx>, variant: VariantIdx, span: Span) -> Expr { match ty.kind() { - ty::Adt(adt, _) if adt.did().is_local() => match self.variant_ty(ty, variant, span) { + ty::Adt(adt, _) if self.b.t.crates.translates(adt.did()) => match self.variant_ty(ty, variant, span) { Some(variant) => Expr::As(Box::new(base), variant), None => base, }, diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index 7a7c4d8ff..abf398e11 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -23,7 +23,7 @@ mod time; use std::collections::VecDeque; use rustc_hir::def::DefKind; -use rustc_hir::def_id::{DefId, LocalDefId}; +use rustc_hir::def_id::{CrateNum, DefId, LocalDefId}; use rustc_middle::ty::{ self, AdtDef, EarlyBinder, GenericArgsRef, Instance, InstanceKind, Ty, TyCtxt, TypeVisitableExt, TypingEnv, }; @@ -78,9 +78,18 @@ pub fn census<'tcx>(tcx: TyCtxt<'tcx>, config: &Config) -> Option { for (span, message) in t.refusals.take().unwrap_or_default() { let (file, line, col, ..) = sm.span_to_location_info(span.source_callsite()); let file = file.map_or_else(String::new, |file| { - file.name + let file = file + .name .display(rustc_span::RemapPathScopeComponents::DIAGNOSTICS) - .to_string() + .to_string(); + // A crate from a registry by its version, wherever cargo keeps it. + match file + .split_once("/registry/src/") + .and_then(|(_, rest)| rest.split_once('/')) + { + Some((_index, path)) => path.to_string(), + None => file, + } }); let refusal = Refusal { file, @@ -158,6 +167,8 @@ impl<'tcx> Translator<'_, 'tcx> { pub struct Translator<'a, 'tcx> { pub tcx: TyCtxt<'tcx>, config: &'a Config, + /// The crates whose bodies translate. + pub crates: Crates, pub program: Program, instances: rustc_data_structures::fx::FxHashMap, SymId>, queue: VecDeque<(Instance<'tcx>, SymId)>, @@ -196,7 +207,81 @@ pub struct Translator<'a, 'tcx> { bindings: bind::Bindings<'tcx>, } -/// A local struct or enum, and how it is represented. +/// The crates that translate as one program: the package, and the dependencies its config +/// names. Any other crate's function is std's mapping, a binding, or refused. +#[derive(Default)] +pub struct Crates(rustc_data_structures::fx::FxHashSet); + +impl Crates { + /// The dependencies `config` names, or `None` after reporting why they can't translate. + fn new(tcx: TyCtxt<'_>, config: &Config) -> Option { + let mut crates = Self::default(); + for package in &config.dependencies { + // A library is named for its package unless its manifest says otherwise. + let name = package.replace('-', "_"); + let found: Vec = tcx + .crates(()) + .iter() + .copied() + .filter(|&krate| tcx.crate_name(krate).as_str() == name) + .collect(); + let why = match found[..] { + [] => "is no library the package depends on", + [_, _, ..] => "is more than one version of a library, whose items would share their paths", + [krate] => { + crates.0.insert(krate); + continue; + } + }; + tcx.dcx().err(format!("rust2-ts: `{package}` in `dependencies` {why}")); + return None; + } + // Another crate's modules go under its name, which a module of the package's own + // at its root would share. + let shares = |name: &str| { + ["core", "alloc", "std"].contains(&name) + || crates.0.iter().any(|&krate| tcx.crate_name(krate).as_str() == name) + }; + for child in tcx.module_children_local(rustc_hir::def_id::CRATE_DEF_ID) { + if let rustc_hir::def::Res::Def(DefKind::Mod, def_id) = child.res + && def_id.is_local() + && shares(child.ident.as_str()) + { + tcx.dcx().span_err( + tcx.def_span(def_id), + format!( + "rust2-ts: module `{}` would share its files with the crate of its name", + child.ident + ), + ); + return None; + } + } + Some(crates) + } + + /// Whether `def_id`'s crate translates. + pub fn translates(&self, def_id: DefId) -> bool { + def_id.is_local() || self.0.contains(&def_id.krate) + } + + /// Every function, closure, and coroutine body of the crates that translate. + pub fn bodies<'tcx>(&self, tcx: TyCtxt<'tcx>) -> Vec { + let mut bodies: Vec = tcx.mir_keys(()).iter().map(|def_id| def_id.to_def_id()).collect(); + // Another crate's items are numbered from zero, and it lists none of its bodies. + let cstore = rustc_metadata::creader::CStore::from_tcx(tcx); + for &krate in &self.0 { + let items = (0..cstore.num_def_ids_untracked(krate)).map(|index| DefId { + krate, + index: rustc_hir::def_id::DefIndex::from_usize(index), + }); + bodies.extend(items.filter(|&def_id| tcx.def_kind(def_id).is_fn_like() && tcx.is_mir_available(def_id))); + } + bodies + } +} + +/// A translated struct or enum, and how it is represented. #[derive(Clone)] pub struct Adt { pub sym: SymId, @@ -242,6 +327,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { Some(Self { tcx, config, + crates: Crates::new(tcx, config)?, program: Program::default(), instances: Default::default(), queue: VecDeque::new(), @@ -521,7 +607,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { return sym; } let base = camel(tcx.item_name(def_id).as_str()); - // A function of a local type sits in the type's own module, escaped only where it is + // A function of a translated type sits in the type's own module, escaped only where it is // declared, so callers call it by its own name: `Decoder.new`. let (module, base) = match self.owner(instance) { Some(ty) => (self.program.syms[ty].module.clone(), base), @@ -547,7 +633,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// The symbol of an instance a call at `span` reaches, noting where the crate first /// reached one of std's. pub fn callee(&mut self, instance: Instance<'tcx>, span: Span) -> SymId { - if !instance.def_id().is_local() { + if !self.crates.translates(instance.def_id()) { let site = self.site.map_or(span, |(site, _)| site); self.sites.entry(instance).or_insert(site); } @@ -563,13 +649,13 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { self.tcx.trait_of_assoc(def_id).map(|_| instance.args.type_at(0)) } - /// The local type an associated function's impl or trait is for, if any. + /// The translated type an associated function's impl or trait is for, if any. fn owner(&mut self, instance: Instance<'tcx>) -> Option { let self_ty = self.self_ty(instance)?.peel_refs(); let ty::Adt(adt, _) = self_ty.kind() else { return None; }; - if !adt.did().is_local() { + if !self.crates.translates(adt.did()) { return None; } let span = self.tcx.def_span(instance.def_id()); @@ -604,7 +690,9 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { // other self type, such as `u64`'s in a trait impl, is spelled out. if let Some(self_ty) = self.self_ty(instance) { match self_ty.peel_refs().kind() { - ty::Adt(adt, args) if adt.did().is_local() => parts.extend(args.types().map(|t| short_name(tcx, t))), + ty::Adt(adt, args) if self.crates.translates(adt.did()) => { + parts.extend(args.types().map(|t| short_name(tcx, t))) + } _ => parts.push(short_name(tcx, self_ty)), } } @@ -716,7 +804,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { pub fn lowers(&self, instance: Instance<'tcx>) -> bool { match instance.def { InstanceKind::Item(did) => { - self.bound_module(instance).is_none() && (did.is_local() || library::from_mir(self, instance)) + self.bound_module(instance).is_none() + && (self.crates.translates(did) || library::from_mir(self, instance)) } _ => false, } @@ -739,7 +828,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } } - /// A local struct or enum, translating its definition the first time. + /// A translated struct or enum, translating its definition the first time. pub fn adt(&mut self, adt: AdtDef<'tcx>, span: Span) -> Adt { if let Some(info) = self.adts.get(&adt.did()) { return info.clone(); @@ -1250,12 +1339,12 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }; } // Every other iterator std has is an `rt.Iter` of its items. - if !adt.did().is_local() + if !self.crates.translates(adt.did()) && let Some(item) = self.iter_item(Ty::new_adt(tcx, adt, args)) { return Type::Rt("Iter", vec![self.ty(item, span)]); } - if !adt.did().is_local() { + if !self.crates.translates(adt.did()) { self.unrepresented( span, format!("unsupported type `{}` from another crate", tcx.def_path_str(adt.did())), @@ -1322,12 +1411,13 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { self.err(span, format!("`{}` is from another crate", tcx.def_path_str(def_id))); return; } - let site = (!def_id.is_local()).then(|| (self.sites.get(&instance).copied().unwrap_or(span), def_id)); + let std = !self.crates.translates(def_id); + let site = std.then(|| (self.sites.get(&instance).copied().unwrap_or(span), def_id)); let outer = std::mem::replace(&mut self.site, site); let function = self.lower_function(instance, sym, span); self.site = outer; if let Some(function) = function { - if !def_id.is_local() { + if std { self.program.std.insert(sym); } self.program.items.push(Item::Function(function)); @@ -1362,10 +1452,13 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// The doc comment on an item, as written. fn doc(&self, def_id: DefId) -> Option { - let local = def_id.as_local()?; + if !self.crates.translates(def_id) { + return None; + } + #[expect(deprecated, reason = "doc comments are no parsed attribute")] let lines: Vec = self .tcx - .hir_attrs(self.tcx.local_def_id_to_hir_id(local)) + .get_all_attrs(def_id) .iter() .filter_map(|attr| attr.doc_str()) .map(|doc| doc.to_string()) diff --git a/ts/src/main.rs b/ts/src/main.rs index 7cbf30f85..546052f16 100644 --- a/ts/src/main.rs +++ b/ts/src/main.rs @@ -16,6 +16,7 @@ extern crate rustc_hir; extern crate rustc_index; extern crate rustc_infer; extern crate rustc_interface; +extern crate rustc_metadata; extern crate rustc_middle; extern crate rustc_span; extern crate rustc_target; diff --git a/ts/tests/fixtures.rs b/ts/tests/fixtures.rs index e535faa9e..146579b11 100644 --- a/ts/tests/fixtures.rs +++ b/ts/tests/fixtures.rs @@ -515,6 +515,30 @@ fn bind() { ); } +/// A package and the dependency its config names translate as one program, the +/// dependency's safe twin of its `unsafe` included, and run as native Rust does. Its +/// `unsafe` that nothing binds is refused at its span, and so are a dependency the package +/// doesn't have and a module of the package's that would share a dependency's files. +#[test] +fn crates() { + let out = translated("crates"); + snapshot("crates", &out); + typecheck(&out); + differential("crates-fixture", &[], &out, "harness.ts", 1_000, &[]); + let cases: [(&str, &str, &[&str]); 3] = [ + ("refused", "refused.toml", &[]), + ("unknown", "unknown.toml", &[]), + ("shared", "ts.toml", &["--", "--features", "shared"]), + ]; + for (case, config, cargo) in cases { + let out = scratch().join("crates").join(case); + let mut args: Vec<&std::ffi::OsStr> = vec!["--out".as_ref(), out.as_ref()]; + args.extend(cargo.iter().map(std::ffi::OsStr::new)); + let output = driver("crates", &fixture("crates").join(config), &args, "build"); + rejected(&out, &output, &fixture("crates").join(format!("{case}.txt"))); + } +} + /// The errors of a failed translation, each with where it points, against `expected`. fn rejected(out: &Path, output: &Output, expected: &Path) { assert!(!output.status.success(), "translating {} succeeded", expected.display()); From a59d34c60dd183c5fa471449d97b85734424f4d9 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:27:31 -0700 Subject: [PATCH 3/7] wip(ts): translate every non-std dependency the roots reach Removes ts.toml's dependencies key. Gives every non-std target crate rust2-ts's flags, -Cpanic=unwind included, which wasmtimer does not compile with. Reads the writes of only the dependency bodies translation reaches, and finds a dependency's iterator impls. Co-Authored-By: Claude Opus 5.5 --- .../expected/coding/decode.DecodeError.ts | 5 +- .../codec/expected/lib.SessionError.ts | 4 +- .../codec/expected/thiserror/display.ts | 13 ++ ts/fixtures/crates/Cargo.toml | 2 + ts/fixtures/crates/dep/Cargo.toml | 4 + ts/fixtures/crates/dep/src/lib.rs | 39 +++- .../crates/expected/crates_dep.Countdown.ts | 25 +++ ts/fixtures/crates/expected/harness.ts | 75 +++---- ts/fixtures/crates/refused.toml | 1 - ts/fixtures/crates/refused.txt | 6 +- ts/fixtures/crates/rev.txt | 1 + ts/fixtures/crates/src/harness.rs | 5 +- ts/fixtures/crates/ts.toml | 1 - ts/fixtures/crates/unknown.toml | 4 - ts/fixtures/crates/unknown.txt | 1 - ts/fixtures/reject/expected.txt | 2 +- ts/moq/model.toml | 2 - ts/src/census.rs | 1 - ts/src/cli.rs | 23 --- ts/src/config.rs | 3 - ts/src/driver.rs | 35 +++- ts/src/lower/bind.rs | 9 +- ts/src/lower/copy.rs | 19 +- ts/src/lower/drop.rs | 2 +- ts/src/lower/iter.rs | 27 ++- ts/src/lower/library.rs | 8 - ts/src/lower/mod.rs | 186 +++++++++++------- ts/src/main.rs | 1 - ts/tests/fixtures.rs | 10 +- 29 files changed, 312 insertions(+), 202 deletions(-) create mode 100644 ts/fixtures/codec/expected/thiserror/display.ts create mode 100644 ts/fixtures/crates/expected/crates_dep.Countdown.ts create mode 100644 ts/fixtures/crates/rev.txt delete mode 100644 ts/fixtures/crates/unknown.toml delete mode 100644 ts/fixtures/crates/unknown.txt diff --git a/ts/fixtures/codec/expected/coding/decode.DecodeError.ts b/ts/fixtures/codec/expected/coding/decode.DecodeError.ts index 140a7aa7e..07e9a19b0 100644 --- a/ts/fixtures/codec/expected/coding/decode.DecodeError.ts +++ b/ts/fixtures/codec/expected/coding/decode.DecodeError.ts @@ -3,6 +3,7 @@ import * as rt from "../rt.ts"; import type * as BoundsExceeded from "../coding/varint.BoundsExceeded.ts"; import * as SessionError from "../lib.SessionError.ts"; +import { asDisplay$AsDisplay_usize } from "../thiserror/display.ts"; export function fmt$Display(self: DecodeError, __formatter: rt.Formatter): rt.Result { if (self.tag === "Short") { @@ -35,7 +36,9 @@ export function fmt$Display(self: DecodeError, __formatter: rt.Formatter): rt.Re return rt.fmtWriteStr(__formatter, "expected end"); } if (self.tag === "MessageTooLarge") { - const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.usizeDisplay(self.size, f), (f: rt.Formatter) => rt.usizeDisplay(self.max, f)]; + const _26: rt.usize = asDisplay$AsDisplay_usize(self.size); + const _28: rt.usize = asDisplay$AsDisplay_usize(self.max); + const args4: rt.Fmt[] = [(f: rt.Formatter) => rt.usizeDisplay(_26, f), (f: rt.Formatter) => rt.usizeDisplay(_28, f)]; return rt.fmtWriteFmt(__formatter, ["message too large: ", args4[0], " bytes exceeds ", args4[1], " byte limit"]); } if (self.tag === "ExpectedData") { diff --git a/ts/fixtures/codec/expected/lib.SessionError.ts b/ts/fixtures/codec/expected/lib.SessionError.ts index 350a6712a..454e253be 100644 --- a/ts/fixtures/codec/expected/lib.SessionError.ts +++ b/ts/fixtures/codec/expected/lib.SessionError.ts @@ -1,12 +1,14 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; +import { asDisplay$AsDisplay_u16 } from "./thiserror/display.ts"; export function fmt$Display(self: SessionError, __formatter: rt.Formatter): rt.Result { if (self.tag === "ProtocolViolation") { return rt.fmtWriteStr(__formatter, "protocol violation"); } - const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.u16Display(self._0, f)]; + const part2 = asDisplay$AsDisplay_u16(self._0); + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.u16Display(part2, f)]; return rt.fmtWriteFmt(__formatter, ["app code=", args2[0]]); } diff --git a/ts/fixtures/codec/expected/thiserror/display.ts b/ts/fixtures/codec/expected/thiserror/display.ts new file mode 100644 index 000000000..ad0cf4b60 --- /dev/null +++ b/ts/fixtures/codec/expected/thiserror/display.ts @@ -0,0 +1,13 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "../rt.ts"; + +export function asDisplay$AsDisplay_u16(self: rt.u16): rt.u16 { + return self; +} + +export function asDisplay$AsDisplay_usize(self: rt.usize): rt.usize { + return self; +} + +//# sourceMappingURL=display.ts.map diff --git a/ts/fixtures/crates/Cargo.toml b/ts/fixtures/crates/Cargo.toml index 1caefa410..fdb503ee7 100644 --- a/ts/fixtures/crates/Cargo.toml +++ b/ts/fixtures/crates/Cargo.toml @@ -13,6 +13,8 @@ crate-type = ["lib", "cdylib"] [features] # A module of the dependency's name, whose files would be the dependency's. shared = [] +# The dependency's iterator runs from either end, which translation refuses. +rev = ["crates-dep/rev"] [dependencies] crates-dep = { path = "dep" } diff --git a/ts/fixtures/crates/dep/Cargo.toml b/ts/fixtures/crates/dep/Cargo.toml index f715c28b6..fcab35cbf 100644 --- a/ts/fixtures/crates/dep/Cargo.toml +++ b/ts/fixtures/crates/dep/Cargo.toml @@ -6,5 +6,9 @@ edition.workspace = true license.workspace = true publish = false +[features] +# `Countdown` runs from either end. +rev = [] + [lints] workspace = true diff --git a/ts/fixtures/crates/dep/src/lib.rs b/ts/fixtures/crates/dep/src/lib.rs index 989445ace..306203858 100644 --- a/ts/fixtures/crates/dep/src/lib.rs +++ b/ts/fixtures/crates/dep/src/lib.rs @@ -1,6 +1,6 @@ //! The dependency `crates-fixture` translates with: a type the package copies, which only -//! this crate writes into, a generic function over the package's type, drops, and -//! `unsafe` with a safe twin where it translates and without one. +//! this crate writes into, a generic function over the package's type, drops, an +//! iterator, and `unsafe` with a safe twin where it translates and without one. use std::sync::{Arc, Mutex}; @@ -53,6 +53,41 @@ impl Drop for Guard { } } +/// Counts down from where it starts to one. +pub struct Countdown { + next: u8, + last: u8, +} + +impl Countdown { + pub fn new(from: u8) -> Self { + Self { next: from, last: 0 } + } +} + +impl Iterator for Countdown { + type Item = u8; + + fn next(&mut self) -> Option { + if self.next == self.last { + return None; + } + self.next -= 1; + Some(self.next + 1) + } +} + +#[cfg(feature = "rev")] +impl DoubleEndedIterator for Countdown { + fn next_back(&mut self) -> Option { + if self.next == self.last { + return None; + } + self.last += 1; + Some(self.last) + } +} + /// The first byte of `bytes`. pub fn first(bytes: &[u8]) -> u8 { assert!(!bytes.is_empty(), "no first byte"); diff --git a/ts/fixtures/crates/expected/crates_dep.Countdown.ts b/ts/fixtures/crates/expected/crates_dep.Countdown.ts new file mode 100644 index 000000000..431437a3a --- /dev/null +++ b/ts/fixtures/crates/expected/crates_dep.Countdown.ts @@ -0,0 +1,25 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.ts"; + +/** Counts down from where it starts to one. */ +export interface Countdown { + next: rt.u8; + last: rt.u8; +} + +function new_(from: rt.u8): Countdown { + return { next: from, last: 0 }; +} + +export function next$Iterator(self: Countdown): rt.u8 | rt.Done { + if (!(self.next === self.last)) { + self.next = rt.u8Sub(self.next, 1); + return rt.u8Add(self.next, 1); + } + return rt.DONE; +} + +export { new_ as new }; + +//# sourceMappingURL=crates_dep.Countdown.ts.map diff --git a/ts/fixtures/crates/expected/harness.ts b/ts/fixtures/crates/expected/harness.ts index 6d6fc01ad..a966a0779 100644 --- a/ts/fixtures/crates/expected/harness.ts +++ b/ts/fixtures/crates/expected/harness.ts @@ -4,6 +4,7 @@ import * as rt from "./rt.ts"; import { copy$Point } from "./lib.ts"; import * as last from "./bind/last.ts"; import { heaviest$Item, first } from "./crates_dep.ts"; +import * as Countdown from "./crates_dep.Countdown.ts"; import * as Guard from "./crates_dep.Guard.ts"; import * as Point from "./crates_dep.Point.ts"; import type * as Item from "./harness.Item.ts"; @@ -13,12 +14,12 @@ export function run(input: rt.ByteSlice): rt.ByteVec { let guards!: Guard.Guard[]; let held!: Guard.Guard; let _late!: Guard.Guard; - let _71!: boolean; - let _72!: boolean; + let _83!: boolean; + let _84!: boolean; let unwind: number = 0; try { - _72 = false; - _71 = false; + _84 = false; + _83 = false; const out: rt.ByteVec = new rt.ByteVec(); if (input.length >= 2) { const p: Point.Point = { x: input.data[input.start] as rt.i32, y: input.data[input.start + 1] as rt.i32 }; @@ -33,62 +34,70 @@ export function run(input: rt.ByteSlice): rt.ByteVec { rt.byteVecPush(out, _29._0); } log = new rt.Arc(new rt.Mutex(new rt.ByteVec())); - unwind = 41; + unwind = 49; guards = rt.iterToArray(rt.iterMap(rt.iterTake(rt.iterSlice(rt.byteSliceView(input)), 3), (_26: rt.u8): Guard.Guard => { return Guard.new(_26, log); }), false, undefined, rt.drop); - unwind = 47; - _72 = true; + unwind = 55; + _84 = true; held = Guard.new(255, log); - unwind = 45; - _71 = true; - _72 = false; + unwind = 53; + _83 = true; + _84 = false; rt.dropAll(guards); _late = Guard.new(254, log); - unwind = 40; - _71 = false; + unwind = 48; + _83 = false; rt.drop(held); - unwind = 45; + unwind = 53; rt.drop(_late); - unwind = 41; - _71 = false; - _72 = false; + unwind = 49; + _83 = false; + _84 = false; const _60: rt.Mutex = log.value; - unwind = 39; + unwind = 47; const _56: rt.Iter = rt.iterSlice(rt.byteSliceView(rt.byteVecSlice(_60.value))); rt.iterExtendBytes(out, _56); - unwind = 41; - const _66: rt.u8 = first(input); - rt.byteVecPush(out, _66); - const _69: rt.u8 = last.last(input); - rt.byteVecPush(out, _69); + unwind = 49; + const iter: Countdown.Countdown = Countdown.new(rt.min(input.length, 4) & 0xff); + while (true) { + const _70: rt.u8 | rt.Done = Countdown.next$Iterator(iter); + if (_70 === rt.DONE) { + break; + } + rt.byteVecPush(out, _70); + } + const _78: rt.u8 = first(input); + rt.byteVecPush(out, _78); + const _81: rt.u8 = last.last(input); + rt.byteVecPush(out, _81); unwind = 0; rt.drop(log); return out; } catch (e) { - b2: { - b3: { + b4: { + b5: { if (unwind === 0) { throw e; } - if (unwind === 39) { - break b2; + if (unwind === 47) { + break b4; } - if (unwind === 40) { + if (unwind === 48) { rt.drop(_late); } else { - if (unwind === 41) { - break b2; + if (unwind === 49) { + break b4; } - if (unwind !== 45) { - break b3; + if (unwind !== 53) { + break b5; } } - if (_71) { + if (_83) { rt.drop(held); } } - if (_72) { + if (_84) { rt.dropAll(guards); } } diff --git a/ts/fixtures/crates/refused.toml b/ts/fixtures/crates/refused.toml index a46001a92..469496565 100644 --- a/ts/fixtures/crates/refused.toml +++ b/ts/fixtures/crates/refused.toml @@ -1,5 +1,4 @@ # The dependency's `unsafe` without the binding that stands in for it: `refused.txt` holds # the errors. package = "crates-fixture" -dependencies = ["crates-dep"] roots = ["harness::run"] diff --git a/ts/fixtures/crates/refused.txt b/ts/fixtures/crates/refused.txt index b6befdd97..17429cb17 100644 --- a/ts/fixtures/crates/refused.txt +++ b/ts/fixtures/crates/refused.txt @@ -1,3 +1,3 @@ -unsupported call to `core::slice::::as_ptr` at fixtures/crates/dep/src/lib.rs:78:18 -unsupported call to `std::ptr::const_ptr::::add` at fixtures/crates/dep/src/lib.rs:78:27 -a raw pointer dereferenced at fixtures/crates/dep/src/lib.rs:78:11 +unsupported call to `core::slice::::as_ptr` at fixtures/crates/dep/src/lib.rs:113:18 +unsupported call to `std::ptr::const_ptr::::add` at fixtures/crates/dep/src/lib.rs:113:27 +a raw pointer dereferenced at fixtures/crates/dep/src/lib.rs:113:11 diff --git a/ts/fixtures/crates/rev.txt b/ts/fixtures/crates/rev.txt new file mode 100644 index 000000000..9f1cf5c2b --- /dev/null +++ b/ts/fixtures/crates/rev.txt @@ -0,0 +1 @@ +an iterator of the crate implementing `DoubleEndedIterator`, which is not supported yet at fixtures/crates/dep/src/lib.rs:81:1 diff --git a/ts/fixtures/crates/src/harness.rs b/ts/fixtures/crates/src/harness.rs index ea00861e1..fa5900ff7 100644 --- a/ts/fixtures/crates/src/harness.rs +++ b/ts/fixtures/crates/src/harness.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex}; -use crates_dep::{Guard, Point, Weigh}; +use crates_dep::{Countdown, Guard, Point, Weigh}; /// The package's own type, which the dependency's generic function ranks. struct Item(u8); @@ -39,6 +39,9 @@ pub fn run(input: &[u8]) -> Vec { drop(held); } out.extend(log.lock().unwrap().iter()); + for n in Countdown::new(input.len().min(4) as u8) { + out.push(n); + } out.push(crates_dep::first(input)); out.push(crates_dep::last(input)); out diff --git a/ts/fixtures/crates/ts.toml b/ts/fixtures/crates/ts.toml index 6d2909b77..158bbc24b 100644 --- a/ts/fixtures/crates/ts.toml +++ b/ts/fixtures/crates/ts.toml @@ -1,5 +1,4 @@ package = "crates-fixture" -dependencies = ["crates-dep"] roots = ["harness::run"] [bind] diff --git a/ts/fixtures/crates/unknown.toml b/ts/fixtures/crates/unknown.toml deleted file mode 100644 index c4cda7a2e..000000000 --- a/ts/fixtures/crates/unknown.toml +++ /dev/null @@ -1,4 +0,0 @@ -# A dependency the package doesn't have: `unknown.txt` holds the error. -package = "crates-fixture" -dependencies = ["crates-dep", "bytes"] -roots = ["harness::run"] diff --git a/ts/fixtures/crates/unknown.txt b/ts/fixtures/crates/unknown.txt deleted file mode 100644 index d9b7210b4..000000000 --- a/ts/fixtures/crates/unknown.txt +++ /dev/null @@ -1 +0,0 @@ -`bytes` in `dependencies` is no library the package depends on diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 6a5d50716..e71ff6c62 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -1,5 +1,5 @@ -a root must not be generic at fixtures/reject/src/lib.rs:102:1 no item `missing` in the crate +a root must not be generic at fixtures/reject/src/lib.rs:102:1 a printable type must implement `Display` or `Debug` at fixtures/reject/src/lib.rs:100:1 unsupported type `f64` at fixtures/reject/src/lib.rs:7:14 a scalar constant of type `f64` at fixtures/reject/src/lib.rs:8:2 diff --git a/ts/moq/model.toml b/ts/moq/model.toml index 219123317..042528810 100644 --- a/ts/moq/model.toml +++ b/ts/moq/model.toml @@ -1,8 +1,6 @@ # moq-net's model, for the census: its public types, with what the subset refuses for good # bound. moq writes the binding files; a census reads none. package = "moq-net" -# The workspace crates its model calls, and rangemap, which translate with it. -dependencies = ["kio", "moq-clock", "moq-pattern", "rangemap"] roots = [ "model::origin::Producer", "model::origin::Consumer", diff --git a/ts/src/census.rs b/ts/src/census.rs index b053747f7..579ed8af0 100644 --- a/ts/src/census.rs +++ b/ts/src/census.rs @@ -165,7 +165,6 @@ const VERBATIM: &[&str] = &[ "break", "collect", "copy_nonoverlapping", - "dependencies", "flatten", "fold", "for", diff --git a/ts/src/cli.rs b/ts/src/cli.rs index d940d58c0..6e8663ece 100644 --- a/ts/src/cli.rs +++ b/ts/src/cli.rs @@ -5,7 +5,6 @@ use std::process::ExitCode; use rust2_driver::cargo::{Args, Check, Output}; use crate::config::{self, Config}; -use crate::driver; /// Translation type-checks for this target: its 32-bit `usize` is what a JS number /// holds exactly, and its `cfg`s are the browser's. @@ -46,7 +45,6 @@ fn try_run(args: &[String]) -> Result { ("CARGO_UNSTABLE_PROFILE_RUSTFLAGS", "true".as_ref()), ]; let mut cargo = std_from_source(); - cargo.extend(dependencies(&config.dependencies)); cargo.extend(args.cargo); Check { tool: "rust2-ts", @@ -75,24 +73,3 @@ fn std_from_source() -> Vec { args } -/// Cargo args that compile each dependency that translates with the package as the -/// package compiles, and with every function's MIR, which a `cargo check` encodes for no -/// dependency's functions otherwise. -fn dependencies(packages: &[String]) -> Vec { - let mut flags = vec!["-Zalways-encode-mir"]; - flags.extend(driver::FLAGS); - let flags = flags - .iter() - .map(|flag| format!("{flag:?}")) - .collect::>() - .join(", "); - packages - .iter() - .flat_map(|package| { - [ - "--config".into(), - format!("profile.dev.package.{package:?}.rustflags=[{flags}]"), - ] - }) - .collect() -} diff --git a/ts/src/config.rs b/ts/src/config.rs index 9a0601140..5330c9bab 100644 --- a/ts/src/config.rs +++ b/ts/src/config.rs @@ -11,9 +11,6 @@ use serde::Deserialize; pub struct Config { /// The cargo package whose library is translated. pub package: String, - /// Cargo packages it depends on whose libraries translate with it, as one program. - #[serde(default)] - pub dependencies: Vec, /// Paths from the crate root to the items to translate: a function, or a type with /// its non-generic methods. Everything they reach comes along. pub roots: Vec, diff --git a/ts/src/driver.rs b/ts/src/driver.rs index 9f91a1984..8000433d3 100644 --- a/ts/src/driver.rs +++ b/ts/src/driver.rs @@ -11,29 +11,44 @@ use rustc_middle::ty::TyCtxt; use crate::config::{self, Config}; use crate::{lower, program}; -/// What rustc compiles each crate that translates with: the package here, and the -/// dependencies it names through cargo's profile, which cargo's fingerprint sees. +/// What rustc compiles each crate that translates with: the package, and every crate it +/// depends on but std's. /// -/// MIR without the checks of undefined behavior that debug builds add around raw -/// pointers, such as a box's: JS has no pointers to check. And with the cleanups a panic +/// Every function's MIR, which a `cargo check` encodes for no dependency's functions +/// otherwise. Without the checks of undefined behavior that debug builds add around raw +/// pointers, such as a box's: JS has no pointers to check. With the cleanups a panic /// unwinds through, which wasm32's default of aborting leaves out: a JS caller can catch -/// a panic and go on. And with every call where the source has it, as a release build's +/// a panic and go on. With every call where the source has it, as a release build's /// inlining would not keep it, since a call is what a binding or a std function's -/// translation stands in for. And with `rust2_ts` set, so a crate can give translation -/// a safe twin of its `unsafe`. -pub const FLAGS: &[&str] = &["-Zub-checks=no", "-Cpanic=unwind", "-Zinline-mir=no", "--cfg=rust2_ts"]; +/// translation stands in for. And with `rust2_ts` set, so a crate can give translation a +/// safe twin of its `unsafe`. +const FLAGS: &[&str] = &[ + "-Zalways-encode-mir", + "-Zub-checks=no", + "-Cpanic=unwind", + "-Zinline-mir=no", + "--cfg=rust2_ts", +]; /// Run as rustc, with `args` as cargo passed them: the wrapped rustc's path first. pub fn run(args: Vec) -> ExitCode { rust2_driver::rustc::run("rust2-ts", args, |args| { - let job = Translate::for_args(args)?; - if job.is_some() { + if translates(args) { args.extend(FLAGS.iter().map(|flag| flag.to_string())); } + let job = Translate::for_args(args)?; Ok(job.map(|job| Box::new(job) as rust2_driver::rustc::Job)) }) } +/// Whether rustc's `args` compile a crate that may translate: one for the target, not a +/// build script or proc macro, which compile for the host, and not one of std's, which +/// `-Zbuild-std` compiles with every item unstable. +fn translates(args: &[String]) -> bool { + args.iter().any(|arg| arg == "--target" || arg.starts_with("--target=")) + && !args.iter().any(|arg| arg.ends_with("force-unstable-if-unmarked")) +} + /// Compiles the configured crate, translating it after analysis. struct Translate { config: Config, diff --git a/ts/src/lower/bind.rs b/ts/src/lower/bind.rs index 469ef43fc..9130cea33 100644 --- a/ts/src/lower/bind.rs +++ b/ts/src/lower/bind.rs @@ -141,7 +141,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }, ); } - self.writes = super::copy::Writes::of_crate(tcx, &self.crates.bodies(tcx), &|def_id| self.own(def_id)); + let bodies = tcx.mir_keys(()).iter().map(|def_id| def_id.to_def_id()).chain(self.read.iter().copied()); + self.writes = super::copy::Writes::of_bodies(tcx, bodies, &|def_id| self.own(def_id)); } /// The item a binding's path names: from the crate root, or else from a crate the crate @@ -363,9 +364,9 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { if !self.program.bound.contains_key(module) { let file = &self.bindings.files[module]; // A census writes nothing, so it reads no file. - let text = match &self.refusals { - Some(_) => Ok(String::new()), - None => std::fs::read_to_string(self.config.dir.join(&file.path)), + let text = match self.census { + true => Ok(String::new()), + false => std::fs::read_to_string(self.config.dir.join(&file.path)), }; let (namespace, prefix) = (file.namespace.clone(), file.prefix.clone()); let source = self.config.dir.join(&file.path); diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index a52c6eedf..736029927 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -226,15 +226,24 @@ pub struct Writes<'tcx> { impl<'tcx> Writes<'tcx> { /// The writes of every function, closure, and coroutine body of `bodies` that `own` - /// says translates: a binding stands in for any other, which counts as code it doesn't - /// read. - pub fn of_crate(tcx: TyCtxt<'tcx>, bodies: &[DefId], own: &dyn Fn(DefId) -> bool) -> Self { + /// says translates, and of the closures and coroutines they hold: a binding stands in + /// for any other, which counts as code it doesn't read. + pub fn of_bodies(tcx: TyCtxt<'tcx>, bodies: impl IntoIterator, own: &dyn Fn(DefId) -> bool) -> Self { let mut writes = Writes::default(); - for &def_id in bodies { - if !tcx.def_kind(def_id).is_fn_like() || !own(def_id) { + let mut queue: Vec = bodies.into_iter().collect(); + let mut seen = FxHashSet::default(); + while let Some(def_id) = queue.pop() { + if !seen.insert(def_id) || !tcx.def_kind(def_id).is_fn_like() || !own(def_id) { continue; } let body = tcx.optimized_mir(def_id); + // Another crate lists none of its closures' bodies. + for local in body.local_decls.iter() { + queue.extend(local.ty.walk().filter_map(|arg| match *arg.as_type()?.kind() { + ty::Closure(nested, _) | ty::Coroutine(nested, _) | ty::CoroutineClosure(nested, _) => Some(nested), + _ => None, + })); + } BodyWrites { tcx, body, diff --git a/ts/src/lower/drop.rs b/ts/src/lower/drop.rs index aa9c63c3b..2c34bcde1 100644 --- a/ts/src/lower/drop.rs +++ b/ts/src/lower/drop.rs @@ -292,7 +292,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { } // A census lowers a generic function over its type parameters, where only an // instance can say what the glue is. - if rustc && self.refusals.is_some() && (ty.has_param() || ty.has_aliases()) { + if rustc && self.census && (ty.has_param() || ty.has_aliases()) { return Some(Expr::rt("drop", vec![value])); } self.err(span, format!("dropping `{ty}`, which is not supported yet")); diff --git a/ts/src/lower/iter.rs b/ts/src/lower/iter.rs index 1931aa134..f45676e1c 100644 --- a/ts/src/lower/iter.rs +++ b/ts/src/lower/iter.rs @@ -77,24 +77,23 @@ pub fn check_impl(t: &mut Translator<'_, '_>, next_fn: DefId) { let ty::Adt(adt, _) = tcx.type_of(imp).instantiate_identity().skip_norm_wip().kind() else { return; }; - for (&tr, impls) in tcx.all_local_trait_impls(()) { - let path = library::std_path(tcx, tr); - let Some(name) = path.last().filter(|n| { + // Only the type's crate can implement std's traits for it. + for &other in tcx.trait_impls_in_crate(adt.did().krate) { + let self_ty = tcx.type_of(other).instantiate_identity().skip_norm_wip(); + if !matches!(self_ty.kind(), ty::Adt(other, _) if other.did() == adt.did()) { + continue; + } + let path = library::std_path(tcx, tcx.impl_trait_id(other)); + if let Some(name) = path.last().filter(|n| { matches!( n.as_str(), "DoubleEndedIterator" | "ExactSizeIterator" | "FusedIterator" ) - }) else { - continue; - }; - for other in impls { - let self_ty = tcx.type_of(other.to_def_id()).instantiate_identity().skip_norm_wip(); - if matches!(self_ty.kind(), ty::Adt(other, _) if other.did() == adt.did()) { - t.err( - tcx.def_span(other.to_def_id()), - format!("an iterator of the crate implementing `{name}`, which is not supported yet"), - ); - } + }) { + t.err( + tcx.def_span(other), + format!("an iterator of the crate implementing `{name}`, which is not supported yet"), + ); } } } diff --git a/ts/src/lower/library.rs b/ts/src/lower/library.rs index ccaa43eb8..239d26f15 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -578,14 +578,6 @@ pub fn key(tcx: TyCtxt<'_>, def_id: DefId) -> Option<(String, String)> { pub fn call<'tcx>(b: &mut Body<'_, '_, 'tcx>, call: &Call<'tcx>, args: Vec, out: &mut Vec) -> Option { let tcx = b.t.tcx; - // thiserror's `Display` derive reads each field through `AsDisplay`, which is the - // field itself for anything but a path. - if tcx.crate_name(call.def_id.krate).as_str() == "thiserror" - && tcx.item_name(call.def_id).as_str() == "as_display" - && matches!(targ(call.args, 0).kind(), ty::Ref(..)) - { - return args.into_iter().next_back(); - } if !is_std(tcx, call.def_id) { return None; } diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index abf398e11..c9659e744 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -36,15 +36,19 @@ use crate::ts::{self, Ctor, FieldId, Item, Loc, SymId, Type}; /// Translate what the config's roots reach, or `None` after reporting why not. pub fn translate<'tcx>(tcx: TyCtxt<'tcx>, config: &Config) -> Option { - let mut t = Translator::new(tcx, config, None)?; - t.bind(); - for root in &config.roots { - t.root(root); - } - for path in &config.printable { - t.printable(path); + let (mut t, ()) = Translator::settle(tcx, config, false, |t| { + for root in &config.roots { + t.root(root); + } + for path in &config.printable { + t.printable(path); + } + t.drain(); + })?; + for (span, msg) in t.refusals.take().unwrap_or_default() { + tcx.dcx().span_err(span, format!("rust2-ts: {msg}")); } - t.drain(); + t.crate_dirs(); if t.failed { return None; } @@ -57,9 +61,7 @@ pub fn translate<'tcx>(tcx: TyCtxt<'tcx>, config: &Config) -> Option { /// translation does, or where it names none, in what each item reaches as if each were a /// root. pub fn census<'tcx>(tcx: TyCtxt<'tcx>, config: &Config) -> Option { - let mut t = Translator::new(tcx, config, Some(Vec::new()))?; - t.bind(); - let polymorphic = match config.roots.is_empty() { + let (mut t, polymorphic) = Translator::settle(tcx, config, true, |t| match config.roots.is_empty() { true => t.every_item(), false => { for root in &config.roots { @@ -71,7 +73,7 @@ pub fn census<'tcx>(tcx: TyCtxt<'tcx>, config: &Config) -> Option { t.drain(); 0 } - }; + })?; let sm = tcx.sess.source_map(); let mut seen = rustc_data_structures::fx::FxHashSet::default(); let mut refusals = Vec::new(); @@ -179,7 +181,10 @@ pub struct Translator<'a, 'tcx> { /// Where trait bounds come from: none outside a census, which lowers some functions /// over their own type parameters. env: TypingEnv<'tcx>, - /// Refusals collected for a census, rather than reported as errors. + /// Whether this is a census, which lowers some functions over their type parameters and + /// writes nothing. + census: bool, + /// Refusals collected until translation settles, then reported or counted. refusals: Option>, /// Whether each type's drop does something JS can see, as [`Self::drops`] found. drops: rustc_data_structures::fx::FxHashMap, bool>, @@ -194,6 +199,8 @@ pub struct Translator<'a, 'tcx> { copy_shims: rustc_data_structures::fx::FxHashMap, SymId>, /// The types the crate writes into in place, which alone are copied. writes: copy::Writes<'tcx>, + /// The bodies of other crates whose writes count: those the run before reached. + read: rustc_data_structures::fx::FxHashSet, /// The class of each coroutine instance. pub coroutines: rustc_data_structures::fx::FxHashMap, mir::coroutine::Info<'tcx>>, /// Whether [`Self::ty`] is typing a variable: see [`Self::held_ty`]. @@ -207,78 +214,33 @@ pub struct Translator<'a, 'tcx> { bindings: bind::Bindings<'tcx>, } -/// The crates that translate as one program: the package, and the dependencies its config -/// names. Any other crate's function is std's mapping, a binding, or refused. +/// The crates that translate as one program: the package, and every crate it depends on +/// but std's. What the roots reach of them translates; any function of std's is its +/// mapping, a binding, or refused. #[derive(Default)] pub struct Crates(rustc_data_structures::fx::FxHashSet); impl Crates { - /// The dependencies `config` names, or `None` after reporting why they can't translate. - fn new(tcx: TyCtxt<'_>, config: &Config) -> Option { - let mut crates = Self::default(); - for package in &config.dependencies { - // A library is named for its package unless its manifest says otherwise. - let name = package.replace('-', "_"); - let found: Vec = tcx - .crates(()) + fn new(tcx: TyCtxt<'_>) -> Self { + Self( + tcx.crates(()) .iter() .copied() - .filter(|&krate| tcx.crate_name(krate).as_str() == name) - .collect(); - let why = match found[..] { - [] => "is no library the package depends on", - [_, _, ..] => "is more than one version of a library, whose items would share their paths", - [krate] => { - crates.0.insert(krate); - continue; - } - }; - tcx.dcx().err(format!("rust2-ts: `{package}` in `dependencies` {why}")); - return None; - } - // Another crate's modules go under its name, which a module of the package's own - // at its root would share. - let shares = |name: &str| { - ["core", "alloc", "std"].contains(&name) - || crates.0.iter().any(|&krate| tcx.crate_name(krate).as_str() == name) - }; - for child in tcx.module_children_local(rustc_hir::def_id::CRATE_DEF_ID) { - if let rustc_hir::def::Res::Def(DefKind::Mod, def_id) = child.res - && def_id.is_local() - && shares(child.ident.as_str()) - { - tcx.dcx().span_err( - tcx.def_span(def_id), - format!( - "rust2-ts: module `{}` would share its files with the crate of its name", - child.ident - ), - ); - return None; - } - } - Some(crates) + .filter(|&krate| !tcx.crate_dep_kind(krate).macros_only() && !of_std(tcx, krate)) + .collect(), + ) } /// Whether `def_id`'s crate translates. pub fn translates(&self, def_id: DefId) -> bool { def_id.is_local() || self.0.contains(&def_id.krate) } +} - /// Every function, closure, and coroutine body of the crates that translate. - pub fn bodies<'tcx>(&self, tcx: TyCtxt<'tcx>) -> Vec { - let mut bodies: Vec = tcx.mir_keys(()).iter().map(|def_id| def_id.to_def_id()).collect(); - // Another crate's items are numbered from zero, and it lists none of its bodies. - let cstore = rustc_metadata::creader::CStore::from_tcx(tcx); - for &krate in &self.0 { - let items = (0..cstore.num_def_ids_untracked(krate)).map(|index| DefId { - krate, - index: rustc_hir::def_id::DefIndex::from_usize(index), - }); - bodies.extend(items.filter(|&def_id| tcx.def_kind(def_id).is_fn_like() && tcx.is_mir_available(def_id))); - } - bodies - } +/// Whether `krate` is std or a crate std is built from: `-Zbuild-std` compiles those, and +/// only those, with stability on every item. +fn of_std(tcx: TyCtxt<'_>, krate: CrateNum) -> bool { + tcx.lookup_stability(krate.as_def_id()).is_some() } /// A translated struct or enum, and how it is represented. @@ -317,7 +279,42 @@ pub struct Erased { } impl<'a, 'tcx> Translator<'a, 'tcx> { - fn new(tcx: TyCtxt<'tcx>, config: &'a Config, refusals: Option>) -> Option { + /// A translator that has run `reach`, with what it copies settled. Whether code writes + /// into a type in place decides that, and of another crate's code, only what + /// translation reaches runs, which it knows once it has reached it. So it runs again + /// while it reaches a body whose writes it didn't read. + fn settle( + tcx: TyCtxt<'tcx>, + config: &'a Config, + census: bool, + mut reach: impl FnMut(&mut Self) -> R, + ) -> Option<(Self, R)> { + let mut read = rustc_data_structures::fx::FxHashSet::default(); + loop { + let mut t = Self::new(tcx, config, census, read)?; + t.bind(); + let out = reach(&mut t); + let reached: Vec = t + .instances + .keys() + .map(|instance| instance.def_id()) + .filter(|&def_id| !def_id.is_local() && t.crates.translates(def_id) && !t.read.contains(&def_id)) + .collect(); + // The config's errors are reported as they are found, so only once. + if reached.is_empty() || tcx.dcx().has_errors().is_some() { + return Some((t, out)); + } + read = std::mem::take(&mut t.read); + read.extend(reached); + } + } + + fn new( + tcx: TyCtxt<'tcx>, + config: &'a Config, + census: bool, + read: rustc_data_structures::fx::FxHashSet, + ) -> Option { if tcx.data_layout.pointer_size().bits() != 32 { tcx.dcx().err( "rust2-ts: translation needs a 32-bit target, such as wasm32-unknown-unknown, so usize is a number", @@ -327,7 +324,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { Some(Self { tcx, config, - crates: Crates::new(tcx, config)?, + crates: Crates::new(tcx), program: Program::default(), instances: Default::default(), queue: VecDeque::new(), @@ -335,7 +332,8 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { sources: Default::default(), failed: false, env: TypingEnv::fully_monomorphized(), - refusals, + census, + refusals: Some(Vec::new()), drops: Default::default(), drops_open: Default::default(), drops_cycle: false, @@ -345,6 +343,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { copy_shims: Default::default(), // Once the bindings are known. writes: Default::default(), + read, held: false, sites: Default::default(), site: None, @@ -381,12 +380,47 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { /// its bounds are that function's, not the caller's. Translation never reaches one. fn drain(&mut self) { while let Some((instance, sym)) = self.queue.pop_front() { - if self.refusals.is_none() || !instance.args.has_param() { + if !self.census || !instance.args.has_param() { self.function(instance, sym); } } } + /// Refuse a reached crate whose files another crate's would be: another crate's modules + /// go under its name, which a module at the package's root or another version of the + /// crate could share. + fn crate_dirs(&mut self) { + let tcx = self.tcx; + let reached: std::collections::BTreeSet = self + .instances + .keys() + .map(|instance| instance.def_id().krate) + .chain(self.adts.keys().map(|def_id| def_id.krate)) + .filter(|&krate| krate != rustc_hir::def_id::LOCAL_CRATE) + .collect(); + let mut names = rustc_data_structures::fx::FxHashMap::default(); + for &krate in &reached { + let name = tcx.crate_name(krate); + if names.insert(name, krate).is_some() { + self.err( + tcx.def_span(krate.as_def_id()), + format!("more than one version of `{name}` is reached, whose modules would share their files"), + ); + } + } + for child in tcx.module_children_local(rustc_hir::def_id::CRATE_DEF_ID) { + if let rustc_hir::def::Res::Def(DefKind::Mod, def_id) = child.res + && def_id.is_local() + && names.contains_key(&child.ident.name) + { + self.err( + tcx.def_span(def_id), + format!("module `{}` would share its files with the crate of its name", child.ident), + ); + } + } + } + /// Refuse a construct outside the subset: inside a body of std's, at the crate's call. pub fn err(&mut self, span: Span, msg: impl Into) { self.failed = true; diff --git a/ts/src/main.rs b/ts/src/main.rs index 546052f16..7cbf30f85 100644 --- a/ts/src/main.rs +++ b/ts/src/main.rs @@ -16,7 +16,6 @@ extern crate rustc_hir; extern crate rustc_index; extern crate rustc_infer; extern crate rustc_interface; -extern crate rustc_metadata; extern crate rustc_middle; extern crate rustc_span; extern crate rustc_target; diff --git a/ts/tests/fixtures.rs b/ts/tests/fixtures.rs index 146579b11..609c371fe 100644 --- a/ts/tests/fixtures.rs +++ b/ts/tests/fixtures.rs @@ -515,10 +515,10 @@ fn bind() { ); } -/// A package and the dependency its config names translate as one program, the -/// dependency's safe twin of its `unsafe` included, and run as native Rust does. Its -/// `unsafe` that nothing binds is refused at its span, and so are a dependency the package -/// doesn't have and a module of the package's that would share a dependency's files. +/// A package and its dependency translate as one program, the dependency's safe twin of +/// its `unsafe` included, and run as native Rust does. Its `unsafe` that nothing binds is +/// refused at its span, and so are its iterator that runs from either end and a module of +/// the package's that would share the dependency's files. #[test] fn crates() { let out = translated("crates"); @@ -527,7 +527,7 @@ fn crates() { differential("crates-fixture", &[], &out, "harness.ts", 1_000, &[]); let cases: [(&str, &str, &[&str]); 3] = [ ("refused", "refused.toml", &[]), - ("unknown", "unknown.toml", &[]), + ("rev", "ts.toml", &["--", "--features", "rev"]), ("shared", "ts.toml", &["--", "--features", "shared"]), ]; for (case, config, cargo) in cases { From e28f726f57f441657caa50a8e8ab98f9fbc60c35 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:07:53 -0700 Subject: [PATCH 4/7] feat(ts): translate every non-std dependency the roots reach Removes ts.toml's dependencies key. The driver gives every non-std crate built for the target every function's MIR and --cfg rust2_ts; a dependency keeps the panic strategy cargo gives it, since unwinding breaks wasm-bindgen's users such as wasmtimer. Only the writes of the dependency bodies translation reaches decide what it copies, so it reruns until it reaches no body it didn't read. A dependency's iterator impls are found in its own crate, and thiserror's AsDisplay translates from source. Co-Authored-By: Claude Opus 5.5 --- quest/m2/ts-async.md | 4 ++-- ts/README.md | 36 ++++++++++++++++++------------------ ts/src/cli.rs | 1 - ts/src/driver.rs | 22 ++++++++++++++-------- ts/src/lower/bind.rs | 6 +++++- ts/src/lower/copy.rs | 7 ++++++- ts/src/lower/mir/mod.rs | 19 +++++++++++++++++-- ts/src/lower/mod.rs | 8 +++++--- 8 files changed, 67 insertions(+), 36 deletions(-) diff --git a/quest/m2/ts-async.md b/quest/m2/ts-async.md index 77310056a..4c14f484f 100644 --- a/quest/m2/ts-async.md +++ b/quest/m2/ts-async.md @@ -26,8 +26,8 @@ What moq-net still needs, by its census from MIR at the pinned revision: boundary, per the [model](/quest/m2/ts-model/README.md)). - Async closures. - kio's own API, reached across crates (`Pending`, `Consumer::poll`, - `Waiter::poll_future`, `Tasks`), translates through - `ts.toml`'s `dependencies`, and kio's `RawWaker` vtable and + `Waiter::poll_future`, `Tasks`), translates from source as a + [dependency](/ts/README.md), and kio's `RawWaker` vtable and `Box` tasks are [bindings](/ts/README.md#bindings). The model itself (its census of 2026-10-27) needs only `Poll`'s helpers and diff --git a/ts/README.md b/ts/README.md index 76650f4f2..b52069b04 100644 --- a/ts/README.md +++ b/ts/README.md @@ -10,11 +10,14 @@ It runs `cargo check` on the package with itself as `RUSTC_WRAPPER`, for `wasm32-unknown-unknown`, with std built from source for it, unwinding and with every function's MIR, which needs the toolchain's `rust-src`. Every crate compiles as rustc would; the configured library is also translated, after type and borrow checking, starting from its roots and following -what they reach. The dependencies the config names translate with it as one program: -cargo compiles each with every function's MIR, and their types and calls translate as -the package's own do. The package and those dependencies compile with `--cfg rust2_ts`, -so a crate can give translation a safe twin of its `unsafe`. Their `unsafe` is refused -where it is, as the package's is, unless a binding stands in for the item holding it. A construct outside the subset is a compile error with its span, and +what they reach. Every crate it depends on but std's translates with it as one program, +as far as the roots reach: each compiles with every function's MIR, and its types and +calls translate as the package's own do. They compile with `--cfg rust2_ts`, so a crate +can give translation a safe twin of its `unsafe`, and otherwise with the panic strategy +cargo gives them, so a panic drops nothing in a dependency's frames. Their `unsafe` is +refused where it is, as the package's is, unless a binding stands in for the item holding +it. Only the writes of the dependencies' functions translation reaches decide what it +copies, so it runs again when it reaches one it didn't read. A construct outside the subset is a compile error with its span, and then nothing is written. A type without a JS value is refused where a value of it is made or received, such as a parameter, a field, or a call, rather than again at each variable holding one; any variable or temporary that holds a type whose JS value is ambiguous, such @@ -23,9 +26,6 @@ as `Option<()>`, is refused at its own span. ```toml # ts.toml, beside the package's Cargo.toml unless --manifest-path says otherwise. package = "moq-net" -# Packages it depends on whose libraries translate with it. Any other crate's function is -# std's mapping, a binding, or refused. -dependencies = ["kio", "moq-clock"] # Paths from the crate root: a function, or a type with its non-generic methods and its # impls of the crate's own traits, blanket ones included. roots = ["coding::Decoder", "lite::Group"] @@ -57,8 +57,8 @@ what they are, since each instance decides that. A census succeeds whatever it f translation still fails on any refusal. `just ts census` runs it at the pinned moq revision, into `target/census`, on moq-net -whole (`moq/net.toml`), on its model's public types with their boundaries bound and the -crates they call translated with them (`moq/model.toml`), and on kio, and the nightly job +whole (`moq/net.toml`), on its model's public types with their boundaries bound +(`moq/model.toml`), and on kio, and the nightly job publishes their tables. ## Output @@ -73,8 +73,8 @@ in `rt/str.ts`, collections in `rt/collections.ts`, ownership and async in `Option`'s and `Result`'s combinators, goes in a module under its crate's name, as `core/option.ts` holds `map$Option_u64_u64_rescale_closure0`. A dependency's modules go under its crate's name too, `kio.ts` and `kio/lock.ts`, with the instances of its generic -functions that the package's types make, so a module of the package's named for a -translated crate is refused. +functions that the package's types make, so a module at the package's root named for a +crate translation reaches is refused, and so are two versions of one crate it reaches. | Rust | TypeScript | | --- | --- | @@ -504,12 +504,12 @@ built natively for wasm32 and run beside its translation on the same inputs by the Rust signatures, which `tsc` refuses at each item, with the errors in `wrong.txt`. `refused.toml` holds bindings the config can't make, with the errors in `refused.txt`. - `crates` translates with `crates/dep`, which writes into a `Copy` type only the package - copies, ranks the package's type in a generic function, drops, and reads through a raw - pointer behind a safe twin under `rust2_ts` and in a function `bind/last.ts` stands in - for. Its differential test runs them against native Rust. `refused.toml` drops the - binding, `unknown.toml` names a package it doesn't depend on, and its `shared` feature - adds a module of the dependency's name, with the errors in `refused.txt`, `unknown.txt`, - and `shared.txt`. + copies, ranks the package's type in a generic function, drops, iterates, and reads + through a raw pointer behind a safe twin under `rust2_ts` and in a function + `bind/last.ts` stands in for. Its differential test runs them against native Rust. + `refused.toml` drops the binding, its `rev` feature makes the dependency's iterator run + from either end, and its `shared` feature adds a module of the dependency's name, with + the errors in `refused.txt`, `rev.txt`, and `shared.txt`. - `reject` holds constructs outside the subset; its expected errors are `expected.txt`, and its expected census is `census.md`. diff --git a/ts/src/cli.rs b/ts/src/cli.rs index 6e8663ece..34f93b681 100644 --- a/ts/src/cli.rs +++ b/ts/src/cli.rs @@ -72,4 +72,3 @@ fn std_from_source() -> Vec { } args } - diff --git a/ts/src/driver.rs b/ts/src/driver.rs index 8000433d3..551f98516 100644 --- a/ts/src/driver.rs +++ b/ts/src/driver.rs @@ -16,27 +16,33 @@ use crate::{lower, program}; /// /// Every function's MIR, which a `cargo check` encodes for no dependency's functions /// otherwise. Without the checks of undefined behavior that debug builds add around raw -/// pointers, such as a box's: JS has no pointers to check. With the cleanups a panic -/// unwinds through, which wasm32's default of aborting leaves out: a JS caller can catch -/// a panic and go on. With every call where the source has it, as a release build's -/// inlining would not keep it, since a call is what a binding or a std function's -/// translation stands in for. And with `rust2_ts` set, so a crate can give translation a -/// safe twin of its `unsafe`. +/// pointers, such as a box's: JS has no pointers to check. With every call where the +/// source has it, as a release build's inlining would not keep it, since a call is what a +/// binding or a std function's translation stands in for. And with `rust2_ts` set, so a +/// crate can give translation a safe twin of its `unsafe`. const FLAGS: &[&str] = &[ "-Zalways-encode-mir", "-Zub-checks=no", - "-Cpanic=unwind", "-Zinline-mir=no", "--cfg=rust2_ts", ]; /// Run as rustc, with `args` as cargo passed them: the wrapped rustc's path first. +/// +/// The package also compiles with the cleanups a panic unwinds through, which wasm32's +/// default of aborting leaves out: a JS caller can catch a panic and go on. A dependency +/// keeps the strategy cargo gives it, since unwinding sets its `cfg(panic)`, which +/// wasm-bindgen reads to require closures it can unwind through, as wasmtimer's are not. +/// So a panic drops nothing in a dependency's frames, as under wasm32's abort. pub fn run(args: Vec) -> ExitCode { rust2_driver::rustc::run("rust2-ts", args, |args| { + let job = Translate::for_args(args)?; if translates(args) { args.extend(FLAGS.iter().map(|flag| flag.to_string())); } - let job = Translate::for_args(args)?; + if job.is_some() { + args.push("-Cpanic=unwind".into()); + } Ok(job.map(|job| Box::new(job) as rust2_driver::rustc::Job)) }) } diff --git a/ts/src/lower/bind.rs b/ts/src/lower/bind.rs index 9130cea33..ddcd908fd 100644 --- a/ts/src/lower/bind.rs +++ b/ts/src/lower/bind.rs @@ -141,7 +141,11 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }, ); } - let bodies = tcx.mir_keys(()).iter().map(|def_id| def_id.to_def_id()).chain(self.read.iter().copied()); + let bodies = tcx + .mir_keys(()) + .iter() + .map(|def_id| def_id.to_def_id()) + .chain(self.read.iter().copied()); self.writes = super::copy::Writes::of_bodies(tcx, bodies, &|def_id| self.own(def_id)); } diff --git a/ts/src/lower/copy.rs b/ts/src/lower/copy.rs index 736029927..d734fff72 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -233,7 +233,12 @@ impl<'tcx> Writes<'tcx> { let mut queue: Vec = bodies.into_iter().collect(); let mut seen = FxHashSet::default(); while let Some(def_id) = queue.pop() { - if !seen.insert(def_id) || !tcx.def_kind(def_id).is_fn_like() || !own(def_id) { + // One without MIR, such as an import, is refused where it is reached. + if !seen.insert(def_id) + || !tcx.def_kind(def_id).is_fn_like() + || !own(def_id) + || !tcx.is_mir_available(def_id) + { continue; } let body = tcx.optimized_mir(def_id); diff --git a/ts/src/lower/mir/mod.rs b/ts/src/lower/mir/mod.rs index a2962ad7a..28c8a84ba 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -77,8 +77,23 @@ fn instance_mir<'tcx>(t: &mut Translator<'_, 'tcx>, instance: Instance<'tcx>, sp return None; } let body = tcx.optimized_mir(def_id).clone(); - let mut body = - instance.instantiate_mir_and_normalize_erasing_regions(tcx, t.typing_env_pub(), EarlyBinder::bind(tcx, body)); + let mut body = match instance.try_instantiate_mir_and_normalize_erasing_regions( + tcx, + t.typing_env_pub(), + EarlyBinder::bind(tcx, body), + ) { + Ok(body) => body, + Err(err) => { + t.err( + span, + format!( + "`{instance}` names `{}`, which does not normalize", + err.get_type_for_failure() + ), + ); + return None; + } + }; vec::literals(tcx, &mut body); if !t.crates.translates(def_id) && let Some(why) = std_refused(tcx, &body) diff --git a/ts/src/lower/mod.rs b/ts/src/lower/mod.rs index c9659e744..6c2f7a45d 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -217,7 +217,6 @@ pub struct Translator<'a, 'tcx> { /// The crates that translate as one program: the package, and every crate it depends on /// but std's. What the roots reach of them translates; any function of std's is its /// mapping, a binding, or refused. -#[derive(Default)] pub struct Crates(rustc_data_structures::fx::FxHashSet); impl Crates { @@ -415,7 +414,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { { self.err( tcx.def_span(def_id), - format!("module `{}` would share its files with the crate of its name", child.ident), + format!( + "module `{}` would share its files with the crate of its name", + child.ident + ), ); } } @@ -1462,7 +1464,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { let tcx = self.tcx; let def_id = instance.def_id(); // Without unwinding, std's MIR has none of the cleanups a panic runs. - if !def_id.is_local() + if !self.crates.translates(def_id) && tcx.required_panic_strategy(def_id.krate) == Some(rustc_target::spec::PanicStrategy::Abort) { self.err( From 3cdee52884e7d8a8636aba66d66ed0f82bba0f0f Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:47:38 -0700 Subject: [PATCH 5/7] fix(ts): give dependencies their flags through cargo's profile Flags the wrapper appends are not in cargo's fingerprint, so a dependency cached without MIR or rust2_ts stayed fresh. Set them as profile.dev.rustflags instead, which cargo fingerprints; only the package's -Cpanic=unwind stays in the wrapper, since cargo checks the package again every run. Co-Authored-By: Claude Opus 5.5 --- ts/src/cli.rs | 25 +++++++++++++++++++++++++ ts/src/driver.rs | 36 +++++------------------------------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/ts/src/cli.rs b/ts/src/cli.rs index 34f93b681..fdf6df729 100644 --- a/ts/src/cli.rs +++ b/ts/src/cli.rs @@ -45,6 +45,8 @@ fn try_run(args: &[String]) -> Result { ("CARGO_UNSTABLE_PROFILE_RUSTFLAGS", "true".as_ref()), ]; let mut cargo = std_from_source(); + cargo.push("--config".into()); + cargo.push(format!("profile.dev.rustflags={}", toml_list(FLAGS))); cargo.extend(args.cargo); Check { tool: "rust2-ts", @@ -57,6 +59,29 @@ fn try_run(args: &[String]) -> Result { .run() } +/// What every crate compiles with, through the profile, which cargo's fingerprint sees, +/// for a crate cached without them to compile again: the package and every crate it +/// depends on translate as one program. +/// +/// Every function's MIR, which a `cargo check` encodes for no dependency's functions +/// otherwise. Without the checks of undefined behavior that debug builds add around raw +/// pointers, such as a box's: JS has no pointers to check. With every call where the +/// source has it, as a release build's inlining would not keep it, since a call is what a +/// binding or a std function's translation stands in for. And with `rust2_ts` set, so a +/// crate can give translation a safe twin of its `unsafe`. std's crates have their own +/// below, and the rest of std's take these to no effect, since none translates. +const FLAGS: &[&str] = &[ + "-Zalways-encode-mir", + "-Zub-checks=no", + "-Zinline-mir=no", + "--cfg=rust2_ts", +]; + +fn toml_list(items: &[&str]) -> String { + let items: Vec = items.iter().map(|item| format!("{item:?}")).collect(); + format!("[{}]", items.join(", ")) +} + /// Cargo args that build std from source, unwinding and with every function's MIR, so /// the bodies of std's combinators lower as the crate's do, cleanups included: the /// target's prebuilt std aborts, so its MIR has none. Set through the profile, which diff --git a/ts/src/driver.rs b/ts/src/driver.rs index 551f98516..71d07403e 100644 --- a/ts/src/driver.rs +++ b/ts/src/driver.rs @@ -11,35 +11,17 @@ use rustc_middle::ty::TyCtxt; use crate::config::{self, Config}; use crate::{lower, program}; -/// What rustc compiles each crate that translates with: the package, and every crate it -/// depends on but std's. -/// -/// Every function's MIR, which a `cargo check` encodes for no dependency's functions -/// otherwise. Without the checks of undefined behavior that debug builds add around raw -/// pointers, such as a box's: JS has no pointers to check. With every call where the -/// source has it, as a release build's inlining would not keep it, since a call is what a -/// binding or a std function's translation stands in for. And with `rust2_ts` set, so a -/// crate can give translation a safe twin of its `unsafe`. -const FLAGS: &[&str] = &[ - "-Zalways-encode-mir", - "-Zub-checks=no", - "-Zinline-mir=no", - "--cfg=rust2_ts", -]; - /// Run as rustc, with `args` as cargo passed them: the wrapped rustc's path first. /// /// The package also compiles with the cleanups a panic unwinds through, which wasm32's -/// default of aborting leaves out: a JS caller can catch a panic and go on. A dependency -/// keeps the strategy cargo gives it, since unwinding sets its `cfg(panic)`, which -/// wasm-bindgen reads to require closures it can unwind through, as wasmtimer's are not. -/// So a panic drops nothing in a dependency's frames, as under wasm32's abort. +/// default of aborting leaves out: a JS caller can catch a panic and go on. Cargo +/// compiles it again each run, so its fingerprint needn't see that. A dependency keeps +/// the strategy cargo gives it, since unwinding sets its `cfg(panic)`, which wasm-bindgen +/// reads to require closures it can unwind through, as wasmtimer's are not. So a panic +/// drops nothing in a dependency's frames, as under wasm32's abort. pub fn run(args: Vec) -> ExitCode { rust2_driver::rustc::run("rust2-ts", args, |args| { let job = Translate::for_args(args)?; - if translates(args) { - args.extend(FLAGS.iter().map(|flag| flag.to_string())); - } if job.is_some() { args.push("-Cpanic=unwind".into()); } @@ -47,14 +29,6 @@ pub fn run(args: Vec) -> ExitCode { }) } -/// Whether rustc's `args` compile a crate that may translate: one for the target, not a -/// build script or proc macro, which compile for the host, and not one of std's, which -/// `-Zbuild-std` compiles with every item unstable. -fn translates(args: &[String]) -> bool { - args.iter().any(|arg| arg == "--target" || arg.starts_with("--target=")) - && !args.iter().any(|arg| arg.ends_with("force-unstable-if-unmarked")) -} - /// Compiles the configured crate, translating it after analysis. struct Translate { config: Config, From a3f13c886f3d7c7c1d7472b2de0c1821897ce312 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:00:48 -0700 Subject: [PATCH 6/7] fix(ts): refuse cargo profiles other than dev The flags translation needs, std's and every crate's, are the dev profile's, so --release or --profile would drop them silently. Co-Authored-By: Claude Opus 5.5 --- ts/src/cli.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ts/src/cli.rs b/ts/src/cli.rs index fdf6df729..83bd2b5bd 100644 --- a/ts/src/cli.rs +++ b/ts/src/cli.rs @@ -14,7 +14,7 @@ const USAGE: &str = "usage: rust2-ts --config (--out | --census Translates the library of the package that ts.toml names to TypeScript in . The package's Cargo.toml is beside ts.toml unless --manifest-path says where. Cargo args, -such as --features, are passed to `cargo check`. +such as --features, are passed to `cargo check`, which checks with the dev profile. --census writes a Markdown report of every construct translation refuses in the library, reached from each of its items as if each were a root, instead of translating it."; @@ -44,6 +44,16 @@ fn try_run(args: &[String]) -> Result { output, ("CARGO_UNSTABLE_PROFILE_RUSTFLAGS", "true".as_ref()), ]; + // What translation needs is the dev profile's, std's flags as well as `FLAGS`. + if let Some(arg) = args + .cargo + .iter() + .find(|arg| matches!(arg.as_str(), "--release" | "-r") || arg.starts_with("--profile")) + { + return Err(format!( + "`{arg}` is not supported: translation checks with the dev profile" + )); + } let mut cargo = std_from_source(); cargo.push("--config".into()); cargo.push(format!("profile.dev.rustflags={}", toml_list(FLAGS))); From 53c4d4a149488a8852ca240865a9992e488ebf70 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:18:11 -0700 Subject: [PATCH 7/7] fix(ts): count a bound type's methods as code the copy analysis doesn't read And check with the dev profile, which cargo refuses to combine with a profile the cargo args pick, clustered short options included. Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 3 +- ts/fixtures/crates/bind/slots.ts | 14 +++++ ts/fixtures/crates/dep/src/lib.rs | 16 ++++++ ts/fixtures/crates/expected/bind/slots.ts | 15 ++++++ ts/fixtures/crates/expected/harness.ts | 63 ++++++++++++----------- ts/fixtures/crates/expected/lib.ts | 5 ++ ts/fixtures/crates/refused.toml | 3 ++ ts/fixtures/crates/refused.txt | 6 +-- ts/fixtures/crates/src/harness.rs | 7 ++- ts/fixtures/crates/ts.toml | 1 + ts/src/cli.rs | 13 ++--- ts/src/lower/bind.rs | 5 +- 12 files changed, 106 insertions(+), 45 deletions(-) create mode 100644 ts/fixtures/crates/bind/slots.ts create mode 100644 ts/fixtures/crates/expected/bind/slots.ts diff --git a/ts/README.md b/ts/README.md index 5a19abc0e..68c568f6a 100644 --- a/ts/README.md +++ b/ts/README.md @@ -507,7 +507,8 @@ built natively for wasm32 and run beside its translation on the same inputs by - `crates` translates with `crates/dep`, which writes into a `Copy` type only the package copies, ranks the package's type in a generic function, drops, iterates, and reads through a raw pointer behind a safe twin under `rust2_ts` and in a function - `bind/last.ts` stands in for. Its differential test runs them against native Rust. + `bind/last.ts` stands in for. `bind/slots.ts` stands in for a type whose method alone + writes into an array the package copies. Its differential test runs them against native Rust. `refused.toml` drops the binding, its `rev` feature makes the dependency's iterator run from either end, and its `shared` feature adds a module of the dependency's name, with the errors in `refused.txt`, `rev.txt`, and `shared.txt`. diff --git a/ts/fixtures/crates/bind/slots.ts b/ts/fixtures/crates/bind/slots.ts new file mode 100644 index 000000000..b4c1b63c4 --- /dev/null +++ b/ts/fixtures/crates/bind/slots.ts @@ -0,0 +1,14 @@ +// `crates_dep::Slots`, which writes into the array it is given. + +import * as rt from "../rt.ts"; + +export type Slots = { base: rt.i16 }; + +export function withBase(base: rt.i16): Slots { + return { base }; +} + +export function fill(self: Slots, out: rt.i16[]): void { + out[0] = self.base; + out[1] = rt.i16Sub(self.base, 1 as rt.i16); +} diff --git a/ts/fixtures/crates/dep/src/lib.rs b/ts/fixtures/crates/dep/src/lib.rs index 306203858..23590e02e 100644 --- a/ts/fixtures/crates/dep/src/lib.rs +++ b/ts/fixtures/crates/dep/src/lib.rs @@ -88,6 +88,22 @@ impl DoubleEndedIterator for Countdown { } } +/// Writes into the arrays it is given, where a binding stands in for it. +pub struct Slots { + base: i16, +} + +impl Slots { + pub fn with_base(base: i16) -> Self { + Self { base } + } + + /// Fill `out` from the base: the only write into a `[i16; 2]`. + pub fn fill(&self, out: &mut [i16; 2]) { + *out = [self.base, self.base - 1]; + } +} + /// The first byte of `bytes`. pub fn first(bytes: &[u8]) -> u8 { assert!(!bytes.is_empty(), "no first byte"); diff --git a/ts/fixtures/crates/expected/bind/slots.ts b/ts/fixtures/crates/expected/bind/slots.ts new file mode 100644 index 000000000..5ff8c38e8 --- /dev/null +++ b/ts/fixtures/crates/expected/bind/slots.ts @@ -0,0 +1,15 @@ +// Generated by rust2-ts. Do not edit. +// `crates_dep::Slots`, which writes into the array it is given. + +import * as rt from "../rt.ts"; + +export type Slots = { base: rt.i16 }; + +export function withBase(base: rt.i16): Slots { + return { base }; +} + +export function fill(self: Slots, out: rt.i16[]): void { + out[0] = self.base; + out[1] = rt.i16Sub(self.base, 1 as rt.i16); +} diff --git a/ts/fixtures/crates/expected/harness.ts b/ts/fixtures/crates/expected/harness.ts index a966a0779..db5546190 100644 --- a/ts/fixtures/crates/expected/harness.ts +++ b/ts/fixtures/crates/expected/harness.ts @@ -1,8 +1,9 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { copy$Point } from "./lib.ts"; +import { copy$Point, copy$i16s } from "./lib.ts"; import * as last from "./bind/last.ts"; +import * as Slots from "./bind/slots.ts"; import { heaviest$Item, first } from "./crates_dep.ts"; import * as Countdown from "./crates_dep.Countdown.ts"; import * as Guard from "./crates_dep.Guard.ts"; @@ -14,12 +15,12 @@ export function run(input: rt.ByteSlice): rt.ByteVec { let guards!: Guard.Guard[]; let held!: Guard.Guard; let _late!: Guard.Guard; - let _83!: boolean; - let _84!: boolean; + let _113!: boolean; + let _114!: boolean; let unwind: number = 0; try { - _84 = false; - _83 = false; + _114 = false; + _113 = false; const out: rt.ByteVec = new rt.ByteVec(); if (input.length >= 2) { const p: Point.Point = { x: input.data[input.start] as rt.i32, y: input.data[input.start + 1] as rt.i32 }; @@ -34,31 +35,31 @@ export function run(input: rt.ByteSlice): rt.ByteVec { rt.byteVecPush(out, _29._0); } log = new rt.Arc(new rt.Mutex(new rt.ByteVec())); - unwind = 49; + unwind = 57; guards = rt.iterToArray(rt.iterMap(rt.iterTake(rt.iterSlice(rt.byteSliceView(input)), 3), (_26: rt.u8): Guard.Guard => { return Guard.new(_26, log); }), false, undefined, rt.drop); - unwind = 55; - _84 = true; + unwind = 63; + _114 = true; held = Guard.new(255, log); - unwind = 53; - _83 = true; - _84 = false; + unwind = 61; + _113 = true; + _114 = false; rt.dropAll(guards); _late = Guard.new(254, log); - unwind = 48; - _83 = false; + unwind = 56; + _113 = false; rt.drop(held); - unwind = 53; + unwind = 61; rt.drop(_late); - unwind = 49; - _83 = false; - _84 = false; + unwind = 57; + _113 = false; + _114 = false; const _60: rt.Mutex = log.value; - unwind = 47; + unwind = 55; const _56: rt.Iter = rt.iterSlice(rt.byteSliceView(rt.byteVecSlice(_60.value))); rt.iterExtendBytes(out, _56); - unwind = 49; + unwind = 57; const iter: Countdown.Countdown = Countdown.new(rt.min(input.length, 4) & 0xff); while (true) { const _70: rt.u8 | rt.Done = Countdown.next$Iterator(iter); @@ -67,10 +68,14 @@ export function run(input: rt.ByteSlice): rt.ByteVec { } rt.byteVecPush(out, _70); } - const _78: rt.u8 = first(input); - rt.byteVecPush(out, _78); - const _81: rt.u8 = last.last(input); - rt.byteVecPush(out, _81); + const kept: rt.i16[] = [(input.length << 16) >> 16, 7]; + const filled: rt.i16[] = copy$i16s(kept); + Slots.fill(Slots.withBase(kept[0]), filled); + rt.iterExtendBytes(out, rt.iterSlice(Uint8Array.of(kept[0] & 0xff, kept[1] & 0xff, filled[0] & 0xff, filled[1] & 0xff))); + const _108: rt.u8 = first(input); + rt.byteVecPush(out, _108); + const _111: rt.u8 = last.last(input); + rt.byteVecPush(out, _111); unwind = 0; rt.drop(log); return out; @@ -80,24 +85,24 @@ export function run(input: rt.ByteSlice): rt.ByteVec { if (unwind === 0) { throw e; } - if (unwind === 47) { + if (unwind === 55) { break b4; } - if (unwind === 48) { + if (unwind === 56) { rt.drop(_late); } else { - if (unwind === 49) { + if (unwind === 57) { break b4; } - if (unwind !== 53) { + if (unwind !== 61) { break b5; } } - if (_83) { + if (_113) { rt.drop(held); } } - if (_84) { + if (_114) { rt.dropAll(guards); } } diff --git a/ts/fixtures/crates/expected/lib.ts b/ts/fixtures/crates/expected/lib.ts index dbf13619b..7e74a1252 100644 --- a/ts/fixtures/crates/expected/lib.ts +++ b/ts/fixtures/crates/expected/lib.ts @@ -1,9 +1,14 @@ // Generated by rust2-ts. Do not edit. +import * as rt from "./rt.ts"; import type * as Point from "./crates_dep.Point.ts"; export function copy$Point(v: Point.Point): Point.Point { return { x: v.x, y: v.y }; } +export function copy$i16s(v: rt.i16[]): rt.i16[] { + return v.slice(); +} + //# sourceMappingURL=lib.ts.map diff --git a/ts/fixtures/crates/refused.toml b/ts/fixtures/crates/refused.toml index 469496565..0147e38ab 100644 --- a/ts/fixtures/crates/refused.toml +++ b/ts/fixtures/crates/refused.toml @@ -2,3 +2,6 @@ # the errors. package = "crates-fixture" roots = ["harness::run"] + +[bind] +"crates_dep::Slots" = "bind/slots.ts" diff --git a/ts/fixtures/crates/refused.txt b/ts/fixtures/crates/refused.txt index 17429cb17..43dfc317d 100644 --- a/ts/fixtures/crates/refused.txt +++ b/ts/fixtures/crates/refused.txt @@ -1,3 +1,3 @@ -unsupported call to `core::slice::::as_ptr` at fixtures/crates/dep/src/lib.rs:113:18 -unsupported call to `std::ptr::const_ptr::::add` at fixtures/crates/dep/src/lib.rs:113:27 -a raw pointer dereferenced at fixtures/crates/dep/src/lib.rs:113:11 +unsupported call to `core::slice::::as_ptr` at fixtures/crates/dep/src/lib.rs:129:18 +unsupported call to `std::ptr::const_ptr::::add` at fixtures/crates/dep/src/lib.rs:129:27 +a raw pointer dereferenced at fixtures/crates/dep/src/lib.rs:129:11 diff --git a/ts/fixtures/crates/src/harness.rs b/ts/fixtures/crates/src/harness.rs index fa5900ff7..b21358653 100644 --- a/ts/fixtures/crates/src/harness.rs +++ b/ts/fixtures/crates/src/harness.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex}; -use crates_dep::{Countdown, Guard, Point, Weigh}; +use crates_dep::{Countdown, Guard, Point, Slots, Weigh}; /// The package's own type, which the dependency's generic function ranks. struct Item(u8); @@ -42,6 +42,11 @@ pub fn run(input: &[u8]) -> Vec { for n in Countdown::new(input.len().min(4) as u8) { out.push(n); } + // Only the binding writes into a `[i16; 2]`, so the copy must not share it. + let kept: [i16; 2] = [input.len() as i16, 7]; + let mut filled = kept; + Slots::with_base(kept[0]).fill(&mut filled); + out.extend([kept[0] as u8, kept[1] as u8, filled[0] as u8, filled[1] as u8]); out.push(crates_dep::first(input)); out.push(crates_dep::last(input)); out diff --git a/ts/fixtures/crates/ts.toml b/ts/fixtures/crates/ts.toml index 158bbc24b..5228bb184 100644 --- a/ts/fixtures/crates/ts.toml +++ b/ts/fixtures/crates/ts.toml @@ -2,4 +2,5 @@ package = "crates-fixture" roots = ["harness::run"] [bind] +"crates_dep::Slots" = "bind/slots.ts" "crates_dep::last" = "bind/last.ts" diff --git a/ts/src/cli.rs b/ts/src/cli.rs index 83bd2b5bd..98b43eb13 100644 --- a/ts/src/cli.rs +++ b/ts/src/cli.rs @@ -44,19 +44,12 @@ fn try_run(args: &[String]) -> Result { output, ("CARGO_UNSTABLE_PROFILE_RUSTFLAGS", "true".as_ref()), ]; - // What translation needs is the dev profile's, std's flags as well as `FLAGS`. - if let Some(arg) = args - .cargo - .iter() - .find(|arg| matches!(arg.as_str(), "--release" | "-r") || arg.starts_with("--profile")) - { - return Err(format!( - "`{arg}` is not supported: translation checks with the dev profile" - )); - } let mut cargo = std_from_source(); cargo.push("--config".into()); cargo.push(format!("profile.dev.rustflags={}", toml_list(FLAGS))); + // The profile those flags and std's are set on, which cargo refuses to combine with + // one the cargo args pick. + cargo.extend(["--profile".into(), "dev".into()]); cargo.extend(args.cargo); Check { tool: "rust2-ts", diff --git a/ts/src/lower/bind.rs b/ts/src/lower/bind.rs index ddcd908fd..9c75e8dce 100644 --- a/ts/src/lower/bind.rs +++ b/ts/src/lower/bind.rs @@ -146,7 +146,10 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { .iter() .map(|def_id| def_id.to_def_id()) .chain(self.read.iter().copied()); - self.writes = super::copy::Writes::of_bodies(tcx, bodies, &|def_id| self.own(def_id)); + // A binding's code, a bound type's methods' included, is code it doesn't read. + self.writes = super::copy::Writes::of_bodies(tcx, bodies, &|def_id| { + self.crates.translates(def_id) && !self.bound(def_id) + }); } /// The item a binding's path names: from the crate root, or else from a crate the crate