Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions go/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,8 @@ impl Rng {
}
}

/// The inputs rust2-ts's differential test runs: every op with every short tail, each
/// u64 boundary through the first `ops` ops with each of `selectors` second bytes, and
/// `count` random ones.
/// Every op with every short tail, each u64 boundary through the first `ops` ops with each
/// of `selectors` second bytes, and `count` random ones from one stream.
fn cases(ops: u8, selectors: u8, count: usize) -> Vec<Vec<u8>> {
let mut cases = vec![Vec::new()];
for op in 0..=255u8 {
Expand Down
2 changes: 0 additions & 2 deletions quest/m0/checks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,3 @@ Each child reports its CPU saved, cold and warm, against `origin/main`.
## Required

- [A warm gate](/quest/m0/checks/cache.md) - the land gate builds through mbx, as moq's does
- [Fixtures that merge](/quest/m0/checks/fixture-layout.md) - `lang` splits by module, and `reject`'s snapshots key by item, not line
- [A case's inputs stay as cases are added](/quest/m0/checks/case-inputs.md) - each differential case's generated inputs are seeded by its name
24 changes: 0 additions & 24 deletions quest/m0/checks/case-inputs.md

This file was deleted.

16 changes: 0 additions & 16 deletions quest/m0/checks/fixture-layout.md

This file was deleted.

4 changes: 3 additions & 1 deletion ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,9 @@ just ts census # census moq-net and kio at the same revision
Each fixture under `fixtures/` is translated and compared with its `expected/` snapshot,
then type-checked with `tsc`. A fixture with a `harness::run(&[u8]) -> Vec<u8>` is also
built natively for wasm32 and run beside its translation on the same inputs by
`tests/differential.ts`, which fails on any difference in output or panic messages.
`tests/differential.ts`, which fails on any difference in output or panic messages. A
harness that names its cases draws each case's inputs from a stream seeded by its name,
so adding, removing, or reordering a case changes no other case's inputs.

- `codec` vendors moq-net's sync codec, which a harness outside the crate could not reach,
and writes its errors' `Display` and `Debug`. It is also translated without
Expand Down
11 changes: 11 additions & 0 deletions ts/fixtures/codec/caller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// The harness, with its cases by name for `differential.ts`.

import { cases, run } from "./harness.ts";

export { run };

/** Each case's name and the byte that picks it, its place in `cases`. */
export const ops = new Map(cases().map(([name], i): [string, Uint8Array] => [name, Uint8Array.of(i)]));

/** Its cases read full-width values. */
export const wide = true;
26 changes: 13 additions & 13 deletions ts/fixtures/codec/expected/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,34 @@ import * as Probe from "./lite/probe.Probe.ts";
import type * as lite_version_Version from "./lite/version.Version.ts";

/**
* Each case, which the first byte picks modulo their count, so a case added at the end
* moves no other.
* Each case's name and what it runs. The first byte picks one modulo their count, so a
* case added at the end moves no other.
*/
export function cases(): Array<(a0: rt.ByteSlice, a1: rt.ByteVec) => undefined> {
return [(a0: rt.ByteSlice, a1: rt.ByteVec): undefined => {
export function cases(): [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined][] {
return [["decode_script", (a0: rt.ByteSlice, a1: rt.ByteVec): undefined => {
decodeScript(a0.data, a0.start, a0.length, a1);
}, (a02: rt.ByteSlice, a12: rt.ByteVec): undefined => {
}] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["encode_script", (a02: rt.ByteSlice, a12: rt.ByteVec): undefined => {
encodeScript(a02.data, a02.start, a02.length, a12);
}, zigzag as (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined, sizes as (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined, (a03: rt.ByteSlice, a13: rt.ByteVec): undefined => {
}] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["zigzag", zigzag as (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["sizes", sizes as (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["roundtrip", (a03: rt.ByteSlice, a13: rt.ByteVec): undefined => {
roundtrip$coding_version_Versions(a03.data, a03.start, a03.length, a13);
}, (a04: rt.ByteSlice, a14: rt.ByteVec): undefined => {
}] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["roundtrip_lite::<Group>", (a04: rt.ByteSlice, a14: rt.ByteVec): undefined => {
roundtripLite$lite_group_Group(a04.data, a04.start, a04.length, a14);
}, (a05: rt.ByteSlice, a15: rt.ByteVec): undefined => {
}] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["roundtrip_lite::<Probe>", (a05: rt.ByteSlice, a15: rt.ByteVec): undefined => {
roundtripLite$lite_probe_Probe(a05.data, a05.start, a05.length, a15);
}, (a06: rt.ByteSlice, a16: rt.ByteVec): undefined => {
}] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["encode_messages", (a06: rt.ByteSlice, a16: rt.ByteVec): undefined => {
encodeMessages(a06.data, a06.start, a06.length, a16);
}, (a07: rt.ByteSlice, a17: rt.ByteVec): undefined => {
}] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined], ["errors", (a07: rt.ByteSlice, a17: rt.ByteVec): undefined => {
errors(a07.data, a07.start, a07.length, a17);
}];
}] as [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined]];
}

/** Run one case. */
export function run(input: rt.ByteSlice): rt.ByteVec {
const out: rt.ByteVec = new rt.ByteVec();
if (input.length !== 0) {
const op: rt.u8 = input.data[input.start];
const cases_: Array<(a0: rt.ByteSlice, a1: rt.ByteVec) => undefined> = cases();
cases_[rt.idx(cases_, rt.usizeRem(op as rt.usize, cases_.length))](new rt.ByteSlice(input.data, input.start + 1, input.length - 1), out);
const cases_: [string, (a0: rt.ByteSlice, a1: rt.ByteVec) => undefined][] = cases();
cases_[rt.idx(cases_, rt.usizeRem(op as rt.usize, cases_.length))][1](new rt.ByteSlice(input.data, input.start + 1, input.length - 1), out);
return out;
}
return out;
Expand Down
27 changes: 14 additions & 13 deletions ts/fixtures/codec/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ use crate::{SessionError, lite};
/// What a case runs on the rest of the input.
type Case = fn(&[u8], &mut Vec<u8>);

/// Each case, which the first byte picks modulo their count, so a case added at the end
/// moves no other.
fn cases() -> Vec<Case> {
/// Each case's name and what it runs. The first byte picks one modulo their count, so a
/// case added at the end moves no other.
fn cases() -> Vec<(&'static str, Case)> {
vec![
decode_script,
encode_script,
zigzag,
sizes,
roundtrip::<Versions>,
roundtrip_lite::<lite::Group>,
roundtrip_lite::<lite::Probe>,
encode_messages,
errors,
("decode_script", decode_script),
("encode_script", encode_script),
("zigzag", zigzag),
("sizes", sizes),
("roundtrip", roundtrip::<Versions>),
("roundtrip_lite::<Group>", roundtrip_lite::<lite::Group>),
("roundtrip_lite::<Probe>", roundtrip_lite::<lite::Probe>),
("encode_messages", encode_messages),
("errors", errors),
]
}

Expand All @@ -34,7 +34,8 @@ pub fn run(input: &[u8]) -> Vec<u8> {
return out;
};
let cases = cases();
cases[usize::from(op) % cases.len()](data, &mut out);
let (_, case) = cases[usize::from(op) % cases.len()];
case(data, &mut out);
out
}

Expand Down
6 changes: 3 additions & 3 deletions ts/fixtures/lang/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ export function op(name: string): number[] {
return [at >> 8, at & 0xff];
}

/** The bytes that pick each case, through `op`, which panics for one past what two bytes pick. */
export const ops: Uint8Array[] = cases().map(([name]) => Uint8Array.from(op(name)));
/** Each case's name and the bytes that pick it, through `op`, which panics for one past what two bytes pick. */
export const ops = new Map(cases().map(([name]): [string, Uint8Array] => [name, Uint8Array.from(op(name))]));

/** Which case the first two bytes of `input` pick. */
export function picked(input: Uint8Array): number {
return ((input[0] << 8) | input[1]) % ops.length;
return ((input[0] << 8) | input[1]) % ops.size;
}

/** A double's bits, big-endian, as `floats` takes them. */
Expand Down
2 changes: 1 addition & 1 deletion ts/fixtures/lang/shapes/caller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { be, edges, op, ops as picks, run as harness } from "./lang.ts";
import * as rt from "./rt.ts";

/** The bytes that pick each case, and a `Fault`'s first byte. */
export const ops: Uint8Array[] = [...picks, Uint8Array.of(0xff)];
export const ops = new Map<string, Uint8Array>([...picks, ["fmt::Fault", Uint8Array.of(0xff)]]);

export function run(input: rt.ByteSlice): rt.ByteVec {
if (rt.byteSliceFirst(input) !== 0xff) return harness(input);
Expand Down
18 changes: 12 additions & 6 deletions ts/fixtures/stock/caller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
// The harness, with inputs it names for `differential.ts`: `buf::reserves`' edges of
// `rt.BytesMut`'s limit, which only these reach, as each allocates 512 MiB, each of
// `hashlink`'s iterators read through every pick of steps from either end, each
// `buf::chains` chain copied by each pick of a length, split at either end and between,
// and every pair of steps over `hashlink`'s maps of what drops.
// The harness, with its cases by name and the inputs it names for `differential.ts`:
// `buf::reserves`' edges of `rt.BytesMut`'s limit, which only these reach, as each
// allocates 512 MiB, each of `hashlink`'s iterators read through every pick of steps from
// either end, each `buf::chains` chain copied by each pick of a length, split at either
// end and between, and every pair of steps over `hashlink`'s maps of what drops.

import { op, run } from "./harness.ts";
import { cases as named, op, run } from "./harness.ts";

export { run };

/** Each case's name and the byte that picks it. */
export const ops = new Map(named().map(([name]): [string, Uint8Array] => [name, Uint8Array.of(op(name))]));

/** Its cases read full-width values. */
export const wide = true;

const reserves = op("buf::reserves");

/** Each edge but shared storage past the limit, which throws, read past by none and by some. */
Expand Down
6 changes: 3 additions & 3 deletions ts/runtime/rt/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// A function that may drop a key, value, or item std's would, rather than return or keep
// it, takes what drops each that does, as `k`, `v`, or `f`, and drops it where std's does.

import { ByteSlice, ByteVec, byteVecPush, byteVecReserve, type Capped, capOf, capReserve, capReserveExact, capSet, type Debug, Err, type FmtResult, type Formatter, fmtDebugTupleFields, fmtList, type None, Ok, type Place, panic, type RangeBound, type Result, rangeCheck, sliceSort, sliceView, vecExact, vecMax, vecReserve, vecWithCapacity } from "../rt.ts";
import { ByteSlice, ByteVec, byteVecPush, byteVecReserve, type Capped, capHold, capOf, capReserve, capReserveExact, capSet, type Debug, Err, type FmtResult, type Formatter, fmtDebugTupleFields, fmtList, type None, Ok, type Place, panic, type RangeBound, type Result, rangeCheck, sliceSort, sliceView, vecExact, vecMax, vecReserve, vecWithCapacity } from "../rt.ts";
import type { Hashing } from "./hash.ts";
import { type Limbs, lt, type u64, type usize } from "./int.ts";
import {
Expand Down Expand Up @@ -3515,11 +3515,11 @@ export function dequeSplitOff<T>(q: VecDeque<T>, i: number): VecDeque<T> {
return other;
}

/** `Vec::from` of a deque: its items in order, with its capacity, as std's makes its buffer contiguous and keeps it. */
/** `Vec::from` of a deque: its items in order, with its capacity, as std's makes its buffer contiguous and keeps it, however the length then drops. */
export function dequeToVec<T>(q: VecDeque<T>): T[] {
const v: T[] & Capped = [];
for (let i = 0; i < q.n; i++) v.push(q.b[at(q, i)] as T);
capSet(v, v.length, q.b.length, q.s);
capHold(v, q.b.length, q.s);
return v;
}

Expand Down
70 changes: 6 additions & 64 deletions ts/tests/differential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
// usage: node differential.ts <fixture.wasm> <harness.ts> [cases]
//
// A harness, or a hand-written caller wrapping one, exports `run(input: rt.ByteSlice):
// Vec<u8>`, and may export `cases`, inputs it names, which run with the generated ones,
// `ops`, the bytes that pick each of its cases, if not the first byte alone, and
// `within(input, native, translated)`, which accepts outputs that differ only where Rust
// leaves the result to the platform, as libm's are. [cases] random inputs run, or that
// many per op of a harness that exports `ops`.
// Vec<u8>`, what `inputs.ts` reads to pick its inputs, and may export `within(input,
// native, translated)`, which accepts outputs that differ only where Rust leaves the
// result to the platform, as libm's are. [cases] random inputs run, or that many per case
// of a harness that names its cases.
// The wasm build exports `input`, `run`, and `output` around the crate's entry, which
// `wasm.ts` calls. A panic traps natively and throws `Panic` in
// TypeScript, and the two agree only if their messages do. An abort traps natively
Expand All @@ -19,6 +18,7 @@ import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { pathToFileURL } from "node:url";

import { inputs } from "./inputs.ts";
import { type Outcome, Wasm } from "./wasm.ts";

const [wasmPath, harnessPath, countArg] = process.argv.slice(2);
Expand All @@ -41,33 +41,6 @@ function translated(input: Uint8Array): Outcome {
}
}

// xorshift64*: the same cases on every run.
let state = 0x9e3779b97f4a7c15n;
function next(): bigint {
state ^= state >> 12n;
state ^= (state << 25n) & 0xffffffffffffffffn;
state ^= state >> 27n;
return (state * 0x2545f4914f6cdd1dn) & 0xffffffffffffffffn;
}
function byte(): number {
return Number(next() & 0xffn);
}

/** Bytes biased toward the values codecs care about: zero, all ones, and boundaries. */
function interesting(): number {
const pick = Number(next() % 8n);
switch (pick) {
case 0:
return 0;
case 1:
return 0xff;
case 2:
return [0x3f, 0x40, 0x7f, 0x80, 0xbf, 0xc0, 0xdf, 0xe0, 0xef, 0xf0, 0xf7, 0xf8, 0xfb, 0xfc, 0xfd, 0xfe][Number(next() % 16n)];
default:
return byte();
}
}

function show(outcome: Outcome): string {
if (outcome === "abort") return outcome;
return outcome instanceof Uint8Array ? Buffer.from(outcome).toString("hex") : `panic ${JSON.stringify(outcome.panic)}`;
Expand All @@ -81,38 +54,7 @@ function display(outcome: Outcome): string {
return show(outcome);
}

const cases: Uint8Array[] = [new Uint8Array(0)];
const named: Uint8Array[] | undefined = harness.ops;
const ops: Uint8Array[] = named ?? Array.from({ length: 256 }, (_, op) => Uint8Array.of(op));
if (new Set(ops.map((op) => Buffer.from(op).toString("hex"))).size !== ops.length) throw new Error("two ops are the same bytes, so one of them never runs");
// Every op with every short tail, so each path's edges are covered exhaustively.
for (const op of ops) {
cases.push(op);
for (let b = 0; b < 256; b += 17) cases.push(Uint8Array.of(...op, b));
}
// Full-width values through the first ops: each u64 boundary 2^k - 1, 2^k, 2^k + 1. A
// harness that names its ops names these inputs itself, for the ops that take them.
for (const op of named ? [] : ops.slice(0, 8)) {
for (let k = 0n; k <= 64n; k++) {
for (const delta of [-1n, 0n, 1n]) {
const value = ((1n << k) + delta) & 0xffffffffffffffffn;
const bytes = new Uint8Array(8);
new DataView(bytes.buffer).setBigUint64(0, value);
for (const selector of [0, 1, 2, 3, 4, 5, 6]) {
cases.push(Uint8Array.of(...op, selector, ...bytes, ...bytes, ...bytes));
}
}
}
}
// A harness's own cases, which name what they cover.
if (Array.isArray(harness.cases)) for (const input of harness.cases) cases.push(input);
for (let i = 0; i < count * (named?.length ?? 1); i++) {
const len = Number(next() % 48n);
const input = new Uint8Array(len + 1);
input[0] = byte();
for (let j = 1; j <= len; j++) input[j] = interesting();
cases.push(input);
}
const cases = inputs(harness, count);

let failures = 0;
let panics = 0;
Expand Down
Loading