import { Option, none, some } from 'fp-ts/lib/Option.js'; import Prando from 'prando'; // a shrinkable generator needs to maintain some state, // namely the sequence of generated values in order to be // able to backtrack and shrink. type GenMode = 'gen' | 'shrink'; export interface Gen { generate(): A; generateN(n: number): A[]; } export interface GenBytes { generate(n: number): number[]; } export class BytesGen implements GenBytes { prng: Prando; bytes: number[][]; // the internal sequence of generated values constructor(prng: Prando) { this.prng = prng; this.bytes = []; } generate(n: number): number[] { if (n < 0) { throw new Error('n must be greater than 0'); } return this.generateBytes(n); } generateBytes(n: number): number[] { const gens = []; for (let i = 0; i < n; i++) { gens.push(this.prng.nextInt(0, 255)); } this.bytes.push([...gens]); return gens; } reject() { if (this.bytes.length === 0) { throw new Error('No more bytes to shrink'); } this.bytes.pop(); } replay(): GenBytes { return new ReplayBytes(this.bytes); } } export class ReplayBytes implements GenBytes { bytes: number[][]; // the internal sequence of generated values index: number = 0; // the current index in the sequence of generated values constructor(bytes: number[][]) { this.bytes = bytes; this.index = 0; } generate(n: number): number[] { if (this.index >= this.bytes.length) { throw new Error('No more bytes to shrink'); } const chunk = [...this.bytes[this.index++]]; if (chunk.length < n) { const zeros = new Array(n - chunk.length).fill(0); chunk.unshift(...zeros); } return chunk; } } class GenInt implements Gen { private bytes: BytesGen; constructor(seed: number | undefined = undefined) { this.bytes = new BytesGen(new Prando(seed ? seed : Math.random() * 1000)); } generate(): number { const bytes = this.bytes.generate(1); return bytes[0]; } generateN(n: number): number[] { const result = []; for (let i = 0; i < n; i++) { result.push(this.bytes.generate(1)[0]); } return result; } } class GenIntWithin implements Gen { 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)); this.min = min; this.max = max; } generate(): number { const range = this.max - this.min; const numBytes = Math.ceil(Math.log2(range) / 8); const maxBytes = Math.pow(2, numBytes * 8); const upperBound = maxBytes - (maxBytes % range); let bytes = this.bytes.generate(numBytes); let rejection = 0; while (true) { let value = 0; for (let i = 0; i < bytes.length; i++) { value = (value << 8) | bytes[i]; } if (value >= upperBound) { this.bytes.reject(); bytes = this.bytes.generate(numBytes); continue; } if (rejection > 0) { console.log(`Rejection sampling: rejected ${rejection} values before accepting ${value}`); } return this.min + (value % range); } } generateN(n: number): number[] { const result = []; for (let i = 0; i < n; i++) { result.push(this.bytes.generate(1)[0]); } return result; } } class GenASCIIString implements Gen { private bytes: BytesGen; constructor(seed: number | undefined = undefined) { this.bytes = new BytesGen(new Prando(seed ? seed : Math.random() * 1000)); } generate(): string { const length = this.bytes.generate(1)[0]; // limit to 20 characters const bytes = this.bytes.generate(length); return String.fromCharCode(...bytes.map(b => b % 128)); // limit to ASCII } generateN(n: number): string[] { const result = []; for (let i = 0; i < n; i++) { result.push(this.generate()); } return result; } } export const genInt = (seed: number | undefined = undefined) => new GenInt(seed); export const genIntWithin = (min: number, max: number, seed: number | undefined = undefined) => new GenIntWithin(min, max, seed); export const genASCIIString = (seed: number | undefined = undefined) => new GenASCIIString(seed);