diff --git a/quest/m2/ts-async.md b/quest/m2/ts-async.md index dbcb7bc2c..4605a0f66 100644 --- a/quest/m2/ts-async.md +++ b/quest/m2/ts-async.md @@ -24,8 +24,8 @@ moq-net still needs, by its census from MIR at the pinned revision: - Async closures, which only moq-net's tests write today. - 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 + `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 edge: `rt.spawn`'s shape for callers, and cancelling a spawned task. diff --git a/quest/m2/ts-model/README.md b/quest/m2/ts-model/README.md index cbc163c64..c8a697cb9 100644 --- a/quest/m2/ts-model/README.md +++ b/quest/m2/ts-model/README.md @@ -47,7 +47,6 @@ Public API: the translator's output for moq-net's model. ## Required - [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 - [Float formatting](/quest/m2/ts-model/floats.md) - `f64` formats and parses as std does - [The rest of std the model reaches](/quest/m2/ts-model/std-rest.md) - the std calls the census still refuses - [Well-known crates without config](/quest/m2/ts-model/stock.md) - rust2-ts maps `bytes`, `rand`, and `tracing` itself, so `model.toml` stops binding and ignoring them 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/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 523149f39..58074806b 100644 --- a/ts/README.md +++ b/ts/README.md @@ -10,7 +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. 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 @@ -51,7 +58,8 @@ 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. +(`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, atomics, and async in `rt/task.ts`, `f64`'s methods in `rt/float.ts`, and `Duration` in `rt/time.ts`. What translates from std's own MIR, `Option`'s, `Result`'s, and `Poll`'s methods, 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 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 | | --- | --- | @@ -549,6 +560,14 @@ 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, iterates, and reads + through a raw pointer behind a safe twin under `rust2_ts` and in a function + `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`. - `reject` holds constructs outside the subset; its expected errors are `expected.txt`, and its expected census is `census.md`. diff --git a/ts/fixtures/codec/expected/coding/decode.DecodeError.ts b/ts/fixtures/codec/expected/coding/decode.DecodeError.ts index 141fd28da..d501715fb 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,8 +36,8 @@ export function fmt$Display(self: DecodeError, __formatter: rt.Formatter): rt.Re return rt.fmtWriteStr(__formatter, "expected end"); } if (self.tag === "MessageTooLarge") { - const _26: rt.usize = self.size; - const _28: rt.usize = self.max; + 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"]); } diff --git a/ts/fixtures/codec/expected/lib.SessionError.ts b/ts/fixtures/codec/expected/lib.SessionError.ts index 06ff628f7..d7f1c5651 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 arg = asDisplay$AsDisplay_u16(self._0); + const args2: rt.Fmt[] = [(f: rt.Formatter) => rt.u16Display(arg, 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 new file mode 100644 index 000000000..fdb503ee7 --- /dev/null +++ b/ts/fixtures/crates/Cargo.toml @@ -0,0 +1,23 @@ +[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 = [] +# The dependency's iterator runs from either end, which translation refuses. +rev = ["crates-dep/rev"] + +[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/bind/slots.ts b/ts/fixtures/crates/bind/slots.ts new file mode 100644 index 000000000..968acec82 --- /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[]): undefined { + out[0] = self.base; + out[1] = rt.i16Sub(self.base, 1 as rt.i16); +} diff --git a/ts/fixtures/crates/dep/Cargo.toml b/ts/fixtures/crates/dep/Cargo.toml new file mode 100644 index 000000000..fcab35cbf --- /dev/null +++ b/ts/fixtures/crates/dep/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "crates-dep" +description = "The dependency crates-fixture translates with" +version = "0.0.0" +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 new file mode 100644 index 000000000..23590e02e --- /dev/null +++ b/ts/fixtures/crates/dep/src/lib.rs @@ -0,0 +1,130 @@ +//! 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, an +//! iterator, 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); + } +} + +/// 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) + } +} + +/// 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"); + 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/bind/slots.ts b/ts/fixtures/crates/expected/bind/slots.ts new file mode 100644 index 000000000..8a7bfc9f1 --- /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[]): undefined { + out[0] = self.base; + out[1] = rt.i16Sub(self.base, 1 as rt.i16); +} 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/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..5257cf9ba --- /dev/null +++ b/ts/fixtures/crates/expected/harness.ts @@ -0,0 +1,114 @@ +// Generated by rust2-ts. Do not edit. + +import * as rt from "./rt.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"; +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 _113!: boolean; + let _114!: boolean; + let unwind: number = 0; + try { + _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 }; + 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 = 57; + guards = rt.iterToArray(rt.iterMap(rt.iterTake(rt.iterSlice(rt.byteSliceView(input)), 3), (_26: rt.u8): Guard.Guard => { + return Guard.new(_26, log); + }), undefined, rt.drop); + unwind = 63; + _114 = true; + held = Guard.new(255, log); + unwind = 61; + _113 = true; + _114 = false; + rt.dropAll(guards); + _late = Guard.new(254, log); + unwind = 56; + _113 = false; + rt.drop(held); + unwind = 61; + rt.drop(_late); + unwind = 57; + _113 = false; + _114 = false; + const _60: rt.Mutex = log.value; + unwind = 55; + const _56: rt.Iter = rt.iterSlice(rt.byteSliceView(rt.byteVecSlice(_60.value))); + rt.iterExtendBytes(out, _56); + 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); + if (_70 === rt.DONE) { + break; + } + rt.byteVecPush(out, _70); + } + 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; + } catch (e) { + b4: { + b5: { + if (unwind === 0) { + throw e; + } + if (unwind === 55) { + break b4; + } + if (unwind === 56) { + rt.drop(_late); + } else { + if (unwind === 57) { + break b4; + } + if (unwind !== 61) { + break b5; + } + } + if (_113) { + rt.drop(held); + } + } + if (_114) { + 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..7e74a1252 --- /dev/null +++ b/ts/fixtures/crates/expected/lib.ts @@ -0,0 +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 new file mode 100644 index 000000000..0147e38ab --- /dev/null +++ b/ts/fixtures/crates/refused.toml @@ -0,0 +1,7 @@ +# The dependency's `unsafe` without the binding that stands in for it: `refused.txt` holds +# 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 new file mode 100644 index 000000000..43dfc317d --- /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: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/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/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..b21358653 --- /dev/null +++ b/ts/fixtures/crates/src/harness.rs @@ -0,0 +1,53 @@ +//! The entry point both sides of the differential test run. + +use std::sync::{Arc, Mutex}; + +use crates_dep::{Countdown, Guard, Point, Slots, 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()); + 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/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..5228bb184 --- /dev/null +++ b/ts/fixtures/crates/ts.toml @@ -0,0 +1,6 @@ +package = "crates-fixture" +roots = ["harness::run"] + +[bind] +"crates_dep::Slots" = "bind/slots.ts" +"crates_dep::last" = "bind/last.ts" diff --git a/ts/fixtures/reject/expected.txt b/ts/fixtures/reject/expected.txt index 0cd37e239..eed8a9d79 100644 --- a/ts/fixtures/reject/expected.txt +++ b/ts/fixtures/reject/expected.txt @@ -1,6 +1,6 @@ -a root must not be generic at fixtures/reject/src/lib.rs:94:1 no item `lopsided` in the crate no item `missing` in the crate +a root must not be generic at fixtures/reject/src/lib.rs:94:1 a printable type must implement `Display` or `Debug` at fixtures/reject/src/lib.rs:92:1 unsupported type `f32` at fixtures/reject/src/lib.rs:7:14 a scalar constant of type `f32` at fixtures/reject/src/lib.rs:8:2 diff --git a/ts/src/cli.rs b/ts/src/cli.rs index 34f93b681..98b43eb13 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."; @@ -45,6 +45,11 @@ 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))); + // 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", @@ -57,6 +62,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 bf146bd9f..71d07403e 100644 --- a/ts/src/driver.rs +++ b/ts/src/driver.rs @@ -12,19 +12,18 @@ use crate::config::{self, Config}; use crate::{lower, program}; /// 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. 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)?; - // 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()); } 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 4e2224544..8ea5e9f36 100644 --- a/ts/src/lower/bind.rs +++ b/ts/src/lower/bind.rs @@ -141,7 +141,15 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { }, ); } - self.writes = super::copy::Writes::of_crate(tcx, &|def_id| self.bound(def_id)); + let bodies = tcx + .mir_keys(()) + .iter() + .map(|def_id| def_id.to_def_id()) + .chain(self.read.iter().copied()); + // 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 @@ -196,9 +204,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. @@ -365,9 +373,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 b02f9bb52..14c1f5b15 100644 --- a/ts/src/lower/copy.rs +++ b/ts/src/lower/copy.rs @@ -223,7 +223,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, @@ -242,22 +242,36 @@ 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, 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 tcx.mir_keys(()) { - let def_id = def_id.to_def_id(); - if !tcx.def_kind(def_id).is_fn_like() || bound(def_id) { + let mut queue: Vec = bodies.into_iter().collect(); + let mut seen = FxHashSet::default(); + while let Some(def_id) = queue.pop() { + // 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); + // 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, def_id, writes: &mut writes, - bound, + own, } .visit_body(body); } @@ -381,7 +395,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> { @@ -411,13 +425,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 fa4a1eced..927af7828 100644 --- a/ts/src/lower/drop.rs +++ b/ts/src/lower/drop.rs @@ -60,7 +60,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)), @@ -185,7 +185,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| { @@ -317,7 +317,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) { @@ -330,7 +330,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 320cbd350..303fa5902 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 6222ecf21..7cc55f4d3 100644 --- a/ts/src/lower/library.rs +++ b/ts/src/lower/library.rs @@ -440,7 +440,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; @@ -769,14 +769,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/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 00fa2492b..139f6ceda 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()) ) } @@ -714,7 +718,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() { @@ -725,7 +729,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 8c7a848b1..98c011eb1 100644 --- a/ts/src/lower/mir/mod.rs +++ b/ts/src/lower/mir/mod.rs @@ -77,16 +77,31 @@ 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 !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) } @@ -313,6 +328,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, @@ -368,7 +384,7 @@ impl<'m, 'b, 'a, 'tcx> Fx<'m, 'b, 'a, 'tcx> { steady: FxHashSet::default(), spilled: FxHashMap::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 3be6068a6..b609aa985 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 412594216..ada9e1754 100644 --- a/ts/src/lower/mod.rs +++ b/ts/src/lower/mod.rs @@ -25,7 +25,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, }; @@ -38,15 +38,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; } @@ -59,9 +63,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 { @@ -73,16 +75,25 @@ 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(); 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, @@ -160,6 +171,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)>, @@ -170,7 +183,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>, @@ -185,6 +201,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 trait of each trait object, by its principal trait. objects: rustc_data_structures::fx::FxHashMap, /// The class wrapping each type as each trait object. @@ -202,7 +220,35 @@ 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 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. +pub struct Crates(rustc_data_structures::fx::FxHashSet); + +impl Crates { + fn new(tcx: TyCtxt<'_>) -> Self { + Self( + tcx.crates(()) + .iter() + .copied() + .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) + } +} + +/// 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. #[derive(Clone)] pub struct Adt { pub sym: SymId, @@ -256,7 +302,42 @@ impl 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", @@ -266,6 +347,7 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { Some(Self { tcx, config, + crates: Crates::new(tcx), program: Program::default(), instances: Default::default(), queue: VecDeque::new(), @@ -273,7 +355,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, @@ -285,6 +368,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, @@ -321,12 +405,50 @@ 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; @@ -547,7 +669,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), @@ -573,7 +695,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); } @@ -589,13 +711,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()); @@ -630,7 +752,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)), } } @@ -742,7 +866,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, } @@ -767,7 +892,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(); @@ -1323,12 +1448,12 @@ impl<'a, 'tcx> Translator<'a, 'tcx> { return self.ty(inner, span); } // 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())), @@ -1461,12 +1586,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)); @@ -1477,7 +1603,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( @@ -1501,10 +1627,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/tests/fixtures.rs b/ts/tests/fixtures.rs index 23ee864a2..196ca04ca 100644 --- a/ts/tests/fixtures.rs +++ b/ts/tests/fixtures.rs @@ -514,6 +514,30 @@ fn bind() { ); } +/// 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"); + snapshot("crates", &out); + typecheck(&out); + differential("crates-fixture", &[], &out, "harness.ts", 1_000); + let cases: [(&str, &str, &[&str]); 3] = [ + ("refused", "refused.toml", &[]), + ("rev", "ts.toml", &["--", "--features", "rev"]), + ("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());