diff options
| author | Arnaud Bailly <arnaud@pankzsoft.com> | 2026-09-02 22:27:04 +0200 |
|---|---|---|
| committer | Arnaud Bailly <arnaud@pankzsoft.com> | 2026-09-02 22:27:04 +0200 |
| commit | d68fdea59aaf05877438bfba669c96d716523765 (patch) | |
| tree | 17913de3ed05cbc494b44b4ffabece46608c89f0 /pbt | |
| parent | 7f69074b259f479b05777ba915a6ef3481a749af (diff) | |
| download | lambda-nantes-main.tar.gz | |
Diffstat (limited to 'pbt')
| -rw-r--r-- | pbt/ts/src/gen.test.ts | 33 | ||||
| -rw-r--r-- | pbt/ts/src/gen.ts | 114 | ||||
| -rw-r--r-- | pbt/ts/src/stats.ts | 10 |
3 files changed, 111 insertions, 46 deletions
diff --git a/pbt/ts/src/gen.test.ts b/pbt/ts/src/gen.test.ts index 6e891e4..8d73beb 100644 --- a/pbt/ts/src/gen.test.ts +++ b/pbt/ts/src/gen.test.ts @@ -1,18 +1,18 @@ import Prando from 'prando'; import { none } from 'fp-ts/lib/Option.js'; -import * as G from './gen.js'; +import G from './gen.js'; import { test, expect } from 'vitest'; import { chiSquare, computeChiSquaredPValue, computeCramersV } from './stats.js'; test('can generate and shrink bytes', async () => { - const prng = new Prando(Math.random() * 1000); - const gen = new G.BytesGen(prng); + const prng = G(); + const gen = prng.genBytes(); const bytes: number[][] = []; for (let i = 1; i < 10; i++) { bytes.push(gen.generate(i)); } console.log('Generated bytes:', bytes); - const replay = gen.replay(); + const replay = prng.replay().genBytes(); for (let i = 1; i < 10; i++) { const replayed = replay.generate(i); expect(replayed, `Replaying at ${i}`).toEqual(bytes[i - 1]); @@ -20,22 +20,19 @@ test('can generate and shrink bytes', async () => { }); test('can generate an arbitrary integer', () => { - const prng = new Prando(Math.random() * 1000); - const gen = G.genInt(); + const gen = G().genInt(); const value = gen.generate(); expect(typeof value).toBe('number'); }); test('can generate N arbitrary integers', () => { - const prng = new Prando(Math.random() * 1000); - const gen = G.genInt(); + const gen = G().genInt(); const value = gen.generateN(5); expect(value.length).toBe(5); }); test('can generate arbitrary integer within bounds', () => { - const seed = Math.random() * 1000; - const gen = G.genIntWithin(1000, 200000, seed); + const gen = G().genIntWithin(1000, 200000); for (let i = 0; i < 100; i++) { const value = gen.generate(); expect(value).toBeGreaterThanOrEqual(1000); @@ -44,10 +41,9 @@ test('can generate arbitrary integer within bounds', () => { }); test('generated integers are uniformly distributed', () => { - const seed = Math.random() * 1000; const lb = 0; const ub = 100000; - const gen = G.genIntWithin(lb, ub, seed); + const gen = G().genIntWithin(lb, ub); const range = ub - lb; const buckets = 100; const numSamples = 100000; @@ -75,14 +71,12 @@ test('generated integers are uniformly distributed', () => { // compute p-value and Cramér's V. const cramerv = computeCramersV(chiSquared, numSamples, buckets); const pValue = computeChiSquaredPValue(chiSquared, buckets); - console.log(`Chi-squared: ${chiSquared}, p-value: ${pValue}, Cramer V: ${cramerv}`); - expect(cramerv).toBeLessThan(0.015); - expect(pValue).toBeGreaterThan(0.05); + expect(cramerv, `Chi-squared: ${chiSquared}, Cramer V: ${cramerv}`).toBeLessThan(0.015); + expect(pValue, `Chi-squared: ${chiSquared}, p-value: ${pValue}`).toBeGreaterThan(0.05); }); test('can generate uniformly distributed arbitrary strings shorter than 256 characters', () => { - const seed = Math.random() * (1 << 31); - const gen = G.genASCIIString(seed); + const gen = G().genString(); const buckets = 16; const numSamples = (1 << 16); const lengths = new Array(buckets).fill(0); @@ -92,10 +86,9 @@ test('can generate uniformly distributed arbitrary strings shorter than 256 char expect(value.length).toBeLessThan(256); } - console.log(lengths); const chiSquared = chiSquare(lengths, new Array(buckets).fill(numSamples / buckets)); const cramerv = computeCramersV(chiSquared, numSamples, buckets); const pValue = computeChiSquaredPValue(chiSquared, buckets); - console.log(`Chi-squared: ${chiSquared}, p-value: ${pValue}, Cramer V: ${cramerv}`); - expect(cramerv).toBeLessThan(0.015); + expect(cramerv, `Chi-squared: ${chiSquared}, Cramer V: ${cramerv}`).toBeLessThan(0.015); + expect(pValue, `Chi-squared: ${chiSquared}, p-value: ${pValue}`).toBeGreaterThan(0.05); }); diff --git a/pbt/ts/src/gen.ts b/pbt/ts/src/gen.ts index 7d8c6f1..587bca6 100644 --- a/pbt/ts/src/gen.ts +++ b/pbt/ts/src/gen.ts @@ -5,7 +5,20 @@ import Prando from 'prando'; // namely the sequence of generated values in order to be // able to backtrack and shrink. -type GenMode = 'gen' | 'shrink'; +export interface Generators { + + genInt(): Gen<number>; + + genIntWithin(min: number, max: number): Gen<number>; + + genBytes(): GenBytes; + + genString(): Gen<string>; + + genList<A>(generator: Gen<A>): Gen<A[]>; + + replay(): void; +} export interface Gen<A> { generate(): A; @@ -13,11 +26,12 @@ export interface Gen<A> { generateN(n: number): A[]; } -export interface GenBytes { +interface GenBytes { + reject(): void; generate(n: number): number[]; } -export class BytesGen implements GenBytes { +class BytesGen implements GenBytes { prng: Prando; bytes: number[][]; // the internal sequence of generated values @@ -54,7 +68,7 @@ export class BytesGen implements GenBytes { } } -export class ReplayBytes implements GenBytes { +class ReplayBytes implements GenBytes { bytes: number[][]; // the internal sequence of generated values index: number = 0; // the current index in the sequence of generated values @@ -77,13 +91,18 @@ export class ReplayBytes implements GenBytes { return chunk; } + + reject() { + if (this.index >= this.bytes.length) { + throw new Error('No more bytes to shrink'); + } + this.index++; + } } class GenInt implements Gen<number> { - private bytes: BytesGen; - - constructor(seed: number | undefined = undefined) { - this.bytes = new BytesGen(new Prando(seed ? seed : Math.random() * 1000)); + constructor(private bytes: GenBytes) { + this.bytes = bytes; } generate(): number { @@ -102,10 +121,8 @@ class GenInt implements Gen<number> { } class GenIntWithin implements Gen<number> { - private bytes: BytesGen; - - constructor(private min: number, private max: number, seed: number | undefined = undefined) { - this.bytes = new BytesGen(new Prando(seed ? seed : Math.random() * 1000000000)); + constructor(private bytes: GenBytes, private min: number, private max: number) { + this.bytes = bytes; this.min = min; this.max = max; } @@ -148,10 +165,8 @@ class GenIntWithin implements Gen<number> { } class GenASCIIString implements Gen<string> { - private bytes: BytesGen; - - constructor(seed: number | undefined = undefined) { - this.bytes = new BytesGen(new Prando(seed ? seed : Math.random() * 1000)); + constructor(private bytes: GenBytes) { + this.bytes = bytes } generate(): string { @@ -169,8 +184,69 @@ class GenASCIIString implements Gen<string> { } } -export const genInt = (seed: number | undefined = undefined) => new GenInt(seed); +class Seeded implements Generators { + + constructor(private bytes: BytesGen) { + this.bytes = bytes; + } + + replay(): Generators { + return new Replayed(this.bytes); + } + + genInt(): Gen<number> { + return new GenInt(this.bytes); + } + genIntWithin(min: number, max: number): Gen<number> { + return new GenIntWithin(this.bytes, min, max); + } + genBytes(): GenBytes { + return this.bytes; + } + genString(): Gen<string> { + return new GenASCIIString(this.bytes); + } + genList<A>(generator: Gen<A>): Gen<A[]> { + throw new Error('Method not implemented.'); + } + +} + +class Replayed implements Generators { + bytes : GenBytes; + + constructor (bytes: BytesGen) { + this.bytes = bytes.replay(); + } + + replay(): Generators { + throw new Error('Cannot replay a replayed generator'); + } + + genInt(): Gen<number> { + return new GenInt(this.bytes); + } + genIntWithin(min: number, max: number): Gen<number> { + return new GenIntWithin(this.bytes, min, max); + } + genBytes(): GenBytes { + return this.bytes; + } + genString(): Gen<string> { + return new GenASCIIString(this.bytes); + } + genList<A>(generator: Gen<A>): Gen<A[]> { + throw new Error('Method not implemented.'); + } + +} + + +const generators = (seed: number | undefined = undefined) => { + const prng = new Prando(seed ? seed : Math.random() * 10000000); + const bytes = new BytesGen(prng); -export const genIntWithin = (min: number, max: number, seed: number | undefined = undefined) => new GenIntWithin(min, max, seed); + return new Seeded(bytes); +}; -export const genASCIIString = (seed: number | undefined = undefined) => new GenASCIIString(seed); +export default generators; diff --git a/pbt/ts/src/stats.ts b/pbt/ts/src/stats.ts index 8c50856..8f9832c 100644 --- a/pbt/ts/src/stats.ts +++ b/pbt/ts/src/stats.ts @@ -86,15 +86,11 @@ export function chiSquare (samples : number [], expected : number []) : number { throw new Error("Samples and expected arrays must have the same length."); } - let chiSquareValue = 0; - for (let i = 0; i < samples.length; i++) { - const observed = samples[i]; + return samples.reduce((acc, observed, i) => { const exp = expected[i]; if (exp === 0) { throw new Error("Expected frequency cannot be zero."); } - chiSquareValue += Math.pow(observed - exp, 2) / exp; - } - - return chiSquareValue; + return acc + Math.pow(observed - exp, 2) / exp; + }, 0); } |
