summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArnaud Bailly <arnaud@pankzsoft.com>2026-09-02 22:27:04 +0200
committerArnaud Bailly <arnaud@pankzsoft.com>2026-09-02 22:27:04 +0200
commitd68fdea59aaf05877438bfba669c96d716523765 (patch)
tree17913de3ed05cbc494b44b4ffabece46608c89f0
parent7f69074b259f479b05777ba915a6ef3481a749af (diff)
downloadlambda-nantes-main.tar.gz
Refactor generators to use a factory for passing bytesHEADmain
-rw-r--r--pbt/ts/src/gen.test.ts33
-rw-r--r--pbt/ts/src/gen.ts114
-rw-r--r--pbt/ts/src/stats.ts10
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);
}