Skip to content

Arbitrary.schema: no generation override for a struct's cross-field filter, and lookahead patterns are skipped quietly #10479

Open

Description

@usirin

Version: effect 4.1.1 (effect/Arbitrary; @effect/vitest 4.1.1 it.prop calls Arbitrary.schema on Schema inputs, so it fails the same way).

Two gaps in Arbitrary.schema. Both end in SampleError after 2001 discards.

1. A cross-field filter on a struct has no generation override

import {Arbitrary, Effect, Schema} from "effect";

const Dead = Schema.Struct({
	tries: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
	errors: Schema.Array(Schema.String).check(Schema.isMaxLength(32)),
}).check(Schema.makeFilter((v) => v.errors.length === v.tries || "one error per try"));

await Effect.runPromise(Arbitrary.sampleEffect(Arbitrary.schema(Dead), {count: 200, seed: 119120}));
// SampleError {generated: 29, discards: 2001}

arbitraryConstraint cannot say "these two fields agree", and toCodecArbitrary is read only on declarations. In v3, an arbitrary annotation on any schema could replace generation. Could a filter, or any node, take a generation override (an Arbitrary, or a toCodecArbitrary-style link)? Today the workaround is to keep a hand-built Arbitrary next to the schema and pass that to it.prop, so the schema and its generator can drift apart.

2. isPattern with a lookahead is skipped quietly

const TraceId = Schema.String.check(Schema.isPattern(/^(?!0{32})[0-9a-f]{32}$/));
await Effect.runPromise(Arbitrary.sampleEffect(Arbitrary.schema(TraceId), {count: 200, seed: 119120}));
// SampleError {generated: 0, discards: 2001}

The pattern is not compiled for generation, so the generator falls back to random strings and the filter discards all of them. Two asks, either helps: support lookahead (or at least a leading negative lookahead), or throw at derivation with an error that names the pattern it could not compile, instead of the discard error at sample time.

Workaround in use: the same set without a lookahead, isPattern(/^[0-9a-f]{32}$/) plus a filter t !== "0".repeat(32), generates 500 of 500 valid values.

Activity

  1. self-assigned this
    on Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions