Skip to content
Draft
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
120 changes: 120 additions & 0 deletions .changeset/calm-trees-generate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
"@effect/vitest": patch
"effect": patch
---

Add the experimental Schema-first `effect/unstable/arbitrary/Arbitrary` module for native generation without
fast-check. `Arbitrary.schema` derives an opaque arbitrary from the decoded Schema `Type`, `Arbitrary.sampleEffect`
provides interruptible sampling with typed exhaustion, and `Arbitrary.checkEffect` returns structured property results.
The initial implementation supports bounded discards, shrinking, replay, and recursive and mutually recursive Schemas.
`SampleError` and `Exhausted` include the effective seed so discarded runs remain reproducible even when the caller did
not provide one. `Arbitrary.isArbitrary` identifies values through the module's nominal protocol. Numeric constraints
retain `NaN` when it is accepted by their supported `Order.Number` bounds. Union derivation validates `oneOf`
exclusivity and isolates lazy cross-member shrinking from unrelated random generation. Object derivation keeps
optional-property selection constructive when candidate fields have different recursive costs.
Struct, Record, JSON-object, and record-shaped `Arbitrary.all` outputs periodically use a null prototype as an edge
case, preserving that prototype throughout shrinking and replay without perturbing structural PRNG choices. The change
adds 0.01–0.03 KB gzip to representative Arbitrary fixtures and leaves production-only bundle sentinels unchanged.

Add `Arbitrary.map`, `Arbitrary.flatMap`, `Arbitrary.filter`, `Arbitrary.filterMap`, and `Arbitrary.all` for composing
derived Arbitraries without exposing a second catalog of primitive constructors. Filtering remains bounded and
promotes valid shrink descendants through rejected nodes. `maxShrinks` bounds every inspected shrink candidate,
including candidates rejected before property evaluation, while retaining the best shrunk input found when the
budget is exhausted. `flatMap` provides deterministic dependent generation, source-first shrinking, post-source PRNG
checkpoints, and one shared residual recursion budget. `all` combines tuples, iterables, and records with a shared
budget, randomized internal generation order, stable output shape, and independent member shrinking. Arbitrary values
implement `Pipeable` for composition with data-last combinators.

Add the experimental Schema `arbitraryConstraint` and `toCodecArbitrary` annotations and their
`Schema.Annotations.ToArbitrary` types. Declarations can provide a Schema Link optimized for generation, while filters
can contribute native semantic constraints. The callback receives decoded type parameters and normalized constraints.
The compiler owns efficient representations for common built-ins, including JSON, RegExp, URL, Date, byte arrays,
ReadonlyMap, and ReadonlySet. Effect-specific HashMap, HashSet, Chunk, Graph, BigDecimal, and date-time declarations keep
local generation Links, while declarations with productive canonical codecs require no arbitrary-specific annotation.
`Schema.isUniqueKey` provides key-based Map uniqueness for explicit array representations.

The same ownership policy applies to formatter and equivalence derivation: implementations for common declarations
live in their compiler, while domain-specific and dynamically constructed declarations retain local annotations.
Declarations whose intrinsic `Equal` implementation already matches their Schema equivalence need no annotation or
compiler special case. This keeps unused common callbacks out of production Schema bundles.

Against the previous layout, `schema-toArbitrary` decreases from 36.68 KB to 33.24 KB gzip and
`arbitrary-combinators` decreases from 37.16 KB to 33.70 KB. `schema-toFormatter` increases from 18.92 KB to 19.49 KB
and `schema-toEquivalence` increases from 19.05 KB to 19.39 KB because callers that explicitly derive these capabilities
now retain the common declaration handlers. Generic production fixtures remain unchanged; an equivalence-specific
production fixture using common declarations decreases from 20.75 KB to 20.48 KB, while declarations whose intrinsic
equality is sufficient decrease from 23.42 KB to 23.34 KB. An Arbitrary-specific production fixture using common
declarations decreases from 20.35 KB to 19.61 KB, while one using the locally annotated BigDecimal and date-time
declarations increases from 18.34 KB to 23.01 KB.
The complete 31-scenario native Arbitrary comparison reports no statistically classified runtime regression; the five
moved BigDecimal and date-time scenarios remain within measurement noise.

Add `SchemaGetter.forbiddenEncoding`, a reusable getter for the encode side of decode-only Schema transformations.

Remove the fast-check bridge from the `effect` package, including `Schema.toArbitrary` and
`effect/testing/FastCheck`. Replace the legacy `Schema.Annotations.ToArbitrary` callback contract with the native
Schema-first types. The `effect` package no longer depends on fast-check.

Migrate `TestSchema.Asserts.verifyLosslessTransformation` and `TestSchema.Asserts.arbitrary().verifyGeneration` to the
native runner. Both methods now accept native check options directly, bound unsuccessful generation, and include the
shrunk input and replay token in property failures.

Use the Arbitrary runner for all `@effect/vitest` property tests. Property inputs may combine Schemas and Arbitraries,
and are composed directly with `Arbitrary.all`; check options are available through `arbitrary`. Raw fast-check
arbitraries and the `fastCheck` options object are no longer supported. As with the previous fast-check adapter, thrown
exceptions, defects, and typed failures from a property are shrinkable falsifications; Effect interruption remains an
interruption.

Optimize constructive regular-expression generation by caching feasible lengths on the compiled pattern, computing
sequence-suffix feasibility once, and precomputing character-class metadata. Seeded generation, shrinking, and replay
remain unchanged.

Optimize `BigDecimal.Order` and `BigDecimal.Equivalence` with a shared hybrid comparator. Ordinary scale differences
use cached, bounded coefficient alignment, while large differences are compared without materializing their decimal
zeroes. `BigDecimal.make` now rejects scales that are not safe integers.

Before its removal, the materialized fast-check bridge fixture
`schema-toArbitrary-materialized-fast-check.ts` measured 79.00 KB minified and gzipped.

Representative runtime measurements against corresponding hand-written fast-check 4.9.0 arbitraries are shown below.
Values are median latency on Node 24.12.0 and Apple M3; lower is better. Both implementations validate the
same output domains, although their generation distributions are not identical. Native speedup is fast-check latency
divided by Native latency, so higher is better.

| Scenario | fast-check | Native | Native speedup |
| ----------------------------------- | ---------: | ------: | -------------: |
| 32 recursive samples | 150 µs | 103 µs | 1.45x |
| 128 optional Struct samples | 244 µs | 86.0 µs | 2.84x |
| 128 constrained strings | 742 µs | 49.7 µs | 14.86x |
| RegExp derivation and first sample | 13.4 ms | 30.8 µs | 429.02x |
| 64 RegExp strings | 595 µs | 919 µs | 0.64x |
| RegExp failure and shrinking | 168 µs | 88.2 µs | 1.91x |
| 128 bounded numbers | 68.9 µs | 21.8 µs | 3.18x |
| 128 `Uint8Array` samples | 98.3 µs | 74.4 µs | 1.32x |
| 128 `BigDecimal` samples | 66.6 µs | 56.3 µs | 1.18x |
| 128 `DateTime.Utc` samples | 71.2 µs | 50.5 µs | 1.42x |
| 128 named time zones | 52.2 µs | 27.9 µs | 1.85x |
| 128 time zones | 63.7 µs | 33.8 µs | 1.89x |
| 128 zoned date-times | 130 µs | 112 µs | 1.16x |
| 32 samples through Schema filter | 65.9 µs | 49.4 µs | 1.33x |
| 32 unique arrays | 156 µs | 132 µs | 1.18x |
| 128 literal samples | 40.0 µs | 3.70 µs | 10.78x |
| 128 mapped samples | 59.0 µs | 14.1 µs | 4.21x |
| 128 samples through passing filter | 58.9 µs | 13.9 µs | 4.23x |
| 32 samples through selective filter | 66.1 µs | 42.9 µs | 1.54x |
| 128 `filterMap` samples | 75.7 µs | 31.5 µs | 2.40x |
| Filtered failure and shrinking | 12.7 µs | 7.71 µs | 1.66x |
| 128 `all` tuple samples | 43.5 µs | 18.5 µs | 2.35x |
| 128 `all` record samples | 81.0 µs | 30.4 µs | 2.66x |
| 128 dependent `flatMap` samples | 125 µs | 67.2 µs | 1.86x |
| `flatMap` failure and shrinking | 20.1 µs | 6.71 µs | 2.99x |
| Replay `flatMap` shrink path | 14.3 µs | 6.57 µs | 2.17x |
| Passing property, 100 runs | 42.3 µs | 27.1 µs | 1.56x |
| `TestSchema`, 100 generations | 44.5 µs | 35.9 µs | 1.24x |
| First failure plus one shrink | 8.77 µs | 1.30 µs | 6.75x |
| Replay recorded failure | 6.35 µs | 1.19 µs | 5.36x |

Cold recursive derivation is not included because the native fixture constructs and compiles a Schema, while the
fast-check fixture constructs a hand-written arbitrary; it is not a like-for-like warm-generator comparison.

Add a guide for the native module and a migration guide from the fast-check bridge published in `effect@4.0.0-rc.109`.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"ast-types": "^0.14.2",
"babel-plugin-annotate-pure-calls": "^0.5.0",
"dprint": "^0.55.2",
"fast-check": "^4.9.0",
"glob": "^13.0.6",
"happy-dom": "^20.11.2",
"jscodeshift": "^17.4.0",
Expand Down
62 changes: 62 additions & 0 deletions packages/effect/ARBITRARY-FOLLOW-UPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Native Arbitrary Follow-ups

This file tracks unfinished work for the native Schema-first Arbitrary implementation. Settled behavior and technical
decisions belong in [ARBITRARY.md](ARBITRARY.md); migration guidance belongs in
[ARBITRARY-MIGRATION.md](ARBITRARY-MIGRATION.md). Remove an item from this file when it is resolved rather than keeping
completed implementation history here.

Every production change must preserve the existing guarantees:

- Schema remains the only catalog of primitive and structural generator constructors;
- discarded roots are bounded by `maxDiscards`;
- inspected shrink candidates, including rejected nodes, are bounded by `maxShrinks`;
- shrinking and replay remain deterministic for supported pure callbacks;
- recursive and mutually recursive Schemas retain their productivity guarantees;
- `Sample`, the shrink carrier, the PRNG, generation budgets, and compiler metadata remain private;
- runtime performance and bundle cost are measured before and after the change.

## Conditional research

These items are intentionally dormant until their trigger is observed.

### Finite-domain metadata

Evaluate private finite-domain metadata only if constructive unique generation demonstrates a real exhaustion or
productivity problem. Do not add public cardinality vocabulary preemptively.

### Decoded collection and Declaration profiling

Profile collection generation Links and Declaration decoding only when a new runtime baseline identifies a regression.
ReadonlyMap and ReadonlySet are compiler-owned; Effect-specific HashMap, HashSet, and Chunk keep declaration-local
generation Links.

### Schema-scoped distribution customization

Revisit application-owned distribution overrides, including deterministic Faker integration, only after a concrete
use case establishes the required scope and bundle boundary. A future design must work for checked and nested Schemas,
must not overload filter or declaration annotations with a second contract, and must not require synthetic Schemas in
test integrations. Prefer a derivation-time override mechanism over executable metadata captured by production Schema
modules.

### Trace-informed shrinking

Compare the current `Sample` tree with private structural spans or trace-informed shrinking only when a reproducible
case shows poor shrunk output or excessive candidate traversal.

### Concrete failing-input persistence

Evaluate persistence and reuse of concrete failing inputs last. After `map` or `flatMap`, an Arbitrary may no longer
have a Schema or codec capable of serializing its output, while the existing opaque replay token remains persistable.

## Verification policy

For every activated item, use the narrowest representative validation and record exact commands and artifacts:

- focused Arbitrary runtime tests and typetests when the public types change;
- package type checking and linting;
- seeded generation, shrinking, and replay characterization relevant to the slice;
- focused warm and cold runtime scenarios;
- focused Arbitrary bundle fixtures plus production Schema bundle sentinels.

Do not update public JSDoc, [ARBITRARY.md](ARBITRARY.md), the migration guide, or the changeset until the corresponding
behavior has passed its semantic, runtime, and bundle gates.
Loading
Loading