@mark1russell7/layout-contract 0.2.12 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@
2
2
  import type { ClaimBreak } from "./corpus.ts";
3
3
  import { canonicalJson, sha256Hex } from "./hash.ts";
4
4
  import { stepRandom } from "./random.ts";
5
+ import { isShapeKind, SHAPE_KIND_IDS, type ShapeKind } from "./shapes.ts";
5
6
 
6
7
  /** The schema of a strategy space. */
7
8
  export const STRATEGY_SPACE_SCHEMA = "strategy-space/1";
@@ -45,6 +46,18 @@ export interface MemberRule {
45
46
  readonly min: number;
46
47
  readonly max: number;
47
48
  readonly ordered: boolean;
49
+ /**
50
+ * The member lists that the option permits. Each entry is a list of whole configs of the space of the members.
51
+ * Without `pool`, each list of base configs with a good count is a member list.
52
+ * With `pool`, a config of the option has the members of one entry, and `spaceSize` counts each entry one time.
53
+ */
54
+ readonly pool?: readonly (readonly StrategyConfig[])[];
55
+ }
56
+
57
+ /** The inputs that an option supports. A missing field supports each value. */
58
+ export interface OptionSupports {
59
+ /** The shape kinds of the boxes (`docs/SHAPES.md`). Without it, the option supports each kind. */
60
+ readonly shape?: readonly ShapeKind[];
48
61
  }
49
62
 
50
63
  /** One implementation of a stage. An option with `members` is a meta option: it chooses between whole configs, or wraps them. */
@@ -52,6 +65,14 @@ export interface StageOption {
52
65
  readonly id: string;
53
66
  readonly params: readonly SpaceParam[];
54
67
  readonly members?: MemberRule;
68
+ /** The inputs that the option supports. It describes the option, so it is not part of a config or of its id. */
69
+ readonly supports?: OptionSupports;
70
+ }
71
+
72
+ /** The shape kinds of the inputs of a check, a count or an enumeration. Without `shapes`, each option counts. */
73
+ export interface ShapeFilter {
74
+ /** The shape kinds of the input. Only the options that support each of these kinds count. */
75
+ readonly shapes?: readonly ShapeKind[];
55
76
  }
56
77
 
57
78
  /** One stage of the pipeline of an engine. A config of an `optional` stage can have no option. */
@@ -113,7 +134,7 @@ export interface SpaceSize {
113
134
  }
114
135
 
115
136
  /** The options of `spaceSize`. */
116
- export interface SpaceSizeOptions {
137
+ export interface SpaceSizeOptions extends ShapeFilter {
117
138
  /** The count of the levels of each numeric param, a whole number of 1 or more. */
118
139
  readonly levels: number;
119
140
  /** The largest meta count that `spaceSize` gives. The default is `10n ** 18n`. */
@@ -121,7 +142,7 @@ export interface SpaceSizeOptions {
121
142
  }
122
143
 
123
144
  /** The options of a grid. The grid gives only the configs without a meta option. */
124
- export interface GridOptions {
145
+ export interface GridOptions extends ShapeFilter {
125
146
  readonly mode: "grid";
126
147
  readonly levels: number;
127
148
  /** The largest count of configs. A larger grid causes a `RangeError`. The default is 100000. */
@@ -129,12 +150,12 @@ export interface GridOptions {
129
150
  }
130
151
 
131
152
  /** The options of a sample. */
132
- export interface SampleOptions {
153
+ export interface SampleOptions extends ShapeFilter {
133
154
  readonly mode: "sample";
134
155
  /** The count of the configs, a whole number of 0 or more. */
135
156
  readonly n: number;
136
157
  readonly seed: number;
137
- /** With `skip` (the default), the sample has no meta option. With `sample`, a meta option draws its members from base samples. */
158
+ /** With `skip` (the default), the sample has no meta option. With `sample`, a meta option draws its members from base samples, or one entry of its `pool`. */
138
159
  readonly meta?: "skip" | "sample";
139
160
  /** The largest count of option draws. The default is the larger value of 1000 and `100 * n`. */
140
161
  readonly maxTries?: number;
@@ -189,6 +210,21 @@ function isActive(p: SpaceParam, values: Values): boolean {
189
210
  return p.when.in.some((x) => x === v);
190
211
  }
191
212
 
213
+ /** This function gives the problem of a value of a param, or `undefined` for a value in its range. The corpus dimensions of `families.ts` use it. */
214
+ export function paramProblem(param: SpaceParam, value: unknown): string | undefined {
215
+ return valueProblem(param, value);
216
+ }
217
+
218
+ /** This function tells if a param is active for the values of the earlier params. A param without `when` is always active. */
219
+ export function paramActive(param: SpaceParam, values: Readonly<Record<string, ParamValue>>): boolean {
220
+ return isActive(param, values);
221
+ }
222
+
223
+ /** This function gives the stable form of a value of a param: a real number gets 12 significant digits, and `-0` becomes `0`. */
224
+ export function paramValue(param: SpaceParam, value: ParamValue): ParamValue {
225
+ return normalize(param, value);
226
+ }
227
+
192
228
  /** This function gives the part of a space that has a stage, and the prefix of the names of its stages. */
193
229
  function partOf(space: StrategySpace, stage: string): { readonly space: StrategySpace; readonly prefix: string } {
194
230
  for (const part of space.parts ?? []) {
@@ -271,14 +307,36 @@ function optionProblems(stage: SpaceStage, o: StageOption): Problem[] {
271
307
  if (problem !== undefined) out.push(["when", `${pat} has a when value that is not a value of ${p.when.param}: ${problem}`]);
272
308
  }
273
309
  });
310
+ if (o.supports !== undefined) out.push(...supportsProblems(o.supports, at));
274
311
  if (o.members !== undefined) {
275
312
  const { min, max, ordered } = o.members;
276
313
  if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || min > max || typeof ordered !== "boolean")
277
314
  out.push(["members", `${at} has a member rule without whole counts 1 <= min <= max and a boolean ordered`]);
315
+ const pool: unknown = o.members.pool;
316
+ if (pool !== undefined && (!Array.isArray(pool) || pool.length === 0 || !pool.every((entry: unknown) => Array.isArray(entry))))
317
+ out.push(["pool", `${at} has a pool that is not a list of one or more member lists`]);
278
318
  }
279
319
  return out;
280
320
  }
281
321
 
322
+ /** This function gives the problems of the `supports` of an option. */
323
+ function supportsProblems(supports: unknown, at: string): Problem[] {
324
+ if (!isObject(supports)) return [["shape", `the supports of ${at} are not an object`]];
325
+ const shape = supports["shape"];
326
+ if (shape === undefined) return [];
327
+ if (!Array.isArray(shape) || shape.length === 0) return [["shape", `${at} supports no shape kind. Remove shape to support each kind`]];
328
+ const out: Problem[] = [];
329
+ shape.forEach((k: unknown, i) => {
330
+ if (!isShapeKind(k)) out.push(["shape", `${at} supports the shape kind ${show(k)}, which is not one of ${SHAPE_KIND_IDS.join(", ")}`]);
331
+ else if (shape.indexOf(k) !== i) out.push(["duplicate", `${at} has the shape kind ${k} two times`]);
332
+ });
333
+ return out;
334
+ }
335
+
336
+ /** This function tells if an option supports each shape kind of the list. */
337
+ const supportsShapes = (o: StageOption, shapes: readonly ShapeKind[] | undefined): boolean =>
338
+ shapes === undefined || o.supports?.shape === undefined || shapes.every((k) => o.supports!.shape!.includes(k));
339
+
282
340
  /** This function gives the problems of the params of one stage choice. */
283
341
  function choiceParamProblems(o: StageOption, params: unknown, at: string): Problem[] {
284
342
  if (params !== undefined && !isObject(params)) return [["config", `the params of ${at} are not an object`]];
@@ -301,7 +359,7 @@ function choiceParamProblems(o: StageOption, params: unknown, at: string): Probl
301
359
  }
302
360
 
303
361
  /** This function gives the problems of a config of a space. A member of a meta option has `member: true`. */
304
- function configProblems(space: StrategySpace, config: unknown, member: boolean): Problem[] {
362
+ function configProblems(space: StrategySpace, config: unknown, member: boolean, shapes?: readonly ShapeKind[]): Problem[] {
305
363
  if (!isObject(config) || !isObject(config["stages"])) return [["config", "the config is not an object with stages"]];
306
364
  const out: Problem[] = [];
307
365
  const stages = config["stages"];
@@ -339,6 +397,10 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean):
339
397
  }
340
398
  chosen.set(s.name, o.id);
341
399
  const at = `the option ${s.name}.${o.id}`;
400
+ if (!supportsShapes(o, shapes)) {
401
+ const missing = shapes!.filter((k) => !o.supports!.shape!.includes(k));
402
+ out.push(["shape", `${at} supports the shape kinds ${o.supports!.shape!.join(", ")}, not ${missing.join(", ")}`]);
403
+ }
342
404
  if (meta !== undefined && meta !== s.name) out.push(["meta", `the stage ${s.name} is set, but the meta option of ${meta} replaces the other stages of its part`]);
343
405
  out.push(...choiceParamProblems(o, c["params"], at));
344
406
  if (o.members === undefined) {
@@ -349,42 +411,77 @@ function configProblems(space: StrategySpace, config: unknown, member: boolean):
349
411
  out.push(["depth", `a member has the meta option ${s.name}.${o.id}. The depth of a meta config is 1`]);
350
412
  continue;
351
413
  }
352
- out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at));
414
+ out.push(...memberProblems(partOf(space, s.name).space, o.members, c["members"], at, shapes));
353
415
  }
354
416
  for (const c of space.constraints ?? []) if (!holds(c, chosen)) out.push(["constraint", `the config breaks the constraint ${describe(c)}`]);
355
417
  return out;
356
418
  }
357
419
 
358
420
  /** This function gives the problems of the members of a meta option. `part` is the space of the members. */
359
- function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown, at: string): Problem[] {
421
+ function memberProblems(part: StrategySpace, rule: MemberRule, members: unknown, at: string, shapes?: readonly ShapeKind[]): Problem[] {
360
422
  if (!Array.isArray(members)) return [["members", `${at} is a meta option without a list of members`]];
361
423
  const out: Problem[] = [];
362
424
  if (members.length < rule.min || members.length > rule.max) out.push(["members", `${at} has ${members.length} members, not ${rule.min} to ${rule.max}`]);
363
425
  const ids = new Map<string, number>();
364
426
  members.forEach((m: unknown, k) => {
365
- const inner = configProblems(part, m, true);
427
+ const inner = configProblems(part, m, true, shapes);
366
428
  for (const [claim, detail] of inner) out.push([claim, `the member ${k} of ${at}: ${detail}`]);
367
429
  if (inner.length > 0) return;
368
430
  const id = idOf(canonicalOf(part, m as StrategyConfig, true));
369
431
  if (ids.has(id)) out.push(["duplicate", `the members ${ids.get(id)} and ${k} of ${at} are the same config ${id}`]);
370
432
  else ids.set(id, k);
371
433
  });
434
+ if (rule.pool !== undefined && out.length === 0) {
435
+ const key = memberKey(part, rule, members as readonly StrategyConfig[]);
436
+ if (!rule.pool.some((entry) => memberKey(part, rule, entry) === key))
437
+ out.push(["pool", `${at} has a member list that is not in its pool of ${rule.pool.length} ${rule.pool.length === 1 ? "list" : "lists"}`]);
438
+ }
439
+ return out;
440
+ }
441
+
442
+ /** This function gives the key of a good member list: the `configId` of each member, sorted when the rule is not ordered. */
443
+ function memberKey(part: StrategySpace, rule: MemberRule, members: readonly StrategyConfig[]): string {
444
+ const ids = members.map((m) => idOf(canonicalOf(part, m, true)));
445
+ if (!rule.ordered) ids.sort();
446
+ return ids.join(",");
447
+ }
448
+
449
+ /** This function gives the problems of the pool of a meta option of a good space. `part` is the space of the members. */
450
+ function poolProblems(part: StrategySpace, rule: MemberRule, at: string): Problem[] {
451
+ const out: Problem[] = [];
452
+ const keys = new Map<string, number>();
453
+ const plain: MemberRule = { min: rule.min, max: rule.max, ordered: rule.ordered };
454
+ (rule.pool ?? []).forEach((entry, j) => {
455
+ const inner = memberProblems(part, plain, entry, `the pool entry ${j} of ${at}`);
456
+ out.push(...inner);
457
+ if (inner.length > 0) return;
458
+ const key = memberKey(part, rule, entry);
459
+ if (keys.has(key)) out.push(["duplicate", `the pool entries ${keys.get(key)} and ${j} of ${at} have the same members`]);
460
+ else keys.set(key, j);
461
+ });
372
462
  return out;
373
463
  }
374
464
 
465
+ /** This function tells if a member list fits a space: each member is a good config of the space. A shape filter removes options from the space. */
466
+ const fits = (part: StrategySpace, entry: readonly StrategyConfig[]): boolean => entry.every((m) => configProblems(part, m, true).length === 0);
467
+
375
468
  const toBreaks = (problems: readonly Problem[], inputs: readonly string[] = []): ClaimBreak[] => problems.map(([claim, detail]) => ({ claim, detail, inputs }));
376
469
 
377
470
  /**
378
471
  * This function checks one config of a space. It gives one break for each problem. An empty list tells that the config is good.
379
472
  * The `claim` of a break is the kind of the problem, for example `missing`, `unknown`, `inactive`, `param`, `constraint`, `members` or `depth`.
473
+ *
474
+ * A member list that is not an entry of the `pool` of its option gives the claim `pool`.
475
+ * With `shapes`, an option that does not support a kind of the list gives the claim `shape`. A member of a meta option gets the same check.
380
476
  */
381
- export function checkConfig(space: StrategySpace, config: StrategyConfig): ClaimBreak[] {
382
- return toBreaks(configProblems(space, config, false));
477
+ export function checkConfig(space: StrategySpace, config: StrategyConfig, options: ShapeFilter = {}): ClaimBreak[] {
478
+ return toBreaks(configProblems(space, config, false, options.shapes));
383
479
  }
384
480
 
385
481
  /**
386
482
  * This function checks a space. It gives one break for each problem. An empty list tells that the space is good.
387
483
  * It finds duplicate names, empty ranges, defaults out of range, enums without values, bad log scales, unknown names and bad named configs.
484
+ * It also checks each entry of a `pool`: the count of its members, each member and a repeated entry.
388
485
  * The `inputs` of a break of a named config have the name of the config.
389
486
  */
390
487
  export function checkSpace(space: StrategySpace): ClaimBreak[] {
@@ -423,6 +520,9 @@ export function checkSpace(space: StrategySpace): ClaimBreak[] {
423
520
  for (const s of space.stages) if (partOf(space, s.name).prefix === "") out.push(["parts", `the stage ${s.name} is not a stage of a part`]);
424
521
  const breaks = toBreaks(out);
425
522
  if (breaks.length > 0) return breaks;
523
+ for (const s of space.stages)
524
+ for (const o of s.options) if (o.members?.pool !== undefined) breaks.push(...toBreaks(poolProblems(partOf(space, s.name).space, o.members, `the option ${s.name}.${o.id}`)));
525
+ if (breaks.length > 0) return breaks;
426
526
  for (const [name, config] of Object.entries(space.named ?? {}))
427
527
  breaks.push(...toBreaks(configProblems(space, config, false).map(([c, d]) => [c, `the named config ${name}: ${d}`] as const), [name]));
428
528
  return breaks;
@@ -569,7 +669,7 @@ interface Combo {
569
669
  const optionIds = (choices: Choices): Map<string, string | null> => new Map([...choices].map(([k, o]) => [k, o?.id ?? null]));
570
670
  const keeps = (space: StrategySpace, choices: Choices): boolean => (space.constraints ?? []).every((c) => holds(c, optionIds(choices)));
571
671
 
572
- /** This function gives the count of the member lists of a meta option from `base` base configs, or `null` above `limit`. */
672
+ /** This function gives the count of the member lists of a meta option without a pool from `base` base configs, or `null` above `limit`. */
573
673
  function memberLists(base: bigint, rule: MemberRule, limit: bigint): bigint | null {
574
674
  let sum = 0n;
575
675
  for (let m = rule.min; m <= rule.max; m++) {
@@ -638,7 +738,8 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
638
738
  for (const o of s.options) {
639
739
  if (o.members === undefined) continue;
640
740
  const choices = new Map<string, StageOption | null>(stages.map((x) => [x.name, x === s ? o : null]));
641
- if (keeps(space, choices)) out.push({ choices, base: false, weight: times(countOf(o), memberLists(base, o.members, limit), limit) });
741
+ const lists = o.members.pool === undefined ? memberLists(base, o.members, limit) : BigInt(o.members.pool.length);
742
+ if (keeps(space, choices)) out.push({ choices, base: false, weight: times(countOf(o), lists, limit) });
642
743
  }
643
744
  return out;
644
745
  }
@@ -646,14 +747,49 @@ function combos(space: StrategySpace, levels: number, meta: boolean, limit: bigi
646
747
  /**
647
748
  * This function gives the count of the grid points of a space, with `levels` levels for each numeric param.
648
749
  * It counts only the active params and the combinations that keep the constraints. It gives the meta configs apart.
750
+ * A meta option with a `pool` counts each entry of the pool one time.
751
+ * With `shapes`, it counts only the options that support each kind of the list.
649
752
  */
650
753
  export function spaceSize(space: StrategySpace, options: SpaceSizeOptions): SpaceSize {
651
754
  assertSpace(space);
652
755
  assertWhole(options.levels, "levels", 1);
653
- const limit = options.maxMeta ?? 10n ** 18n;
756
+ assertShapes(options.shapes);
757
+ return sizeOf(restrict(space, options.shapes), options.levels, options.maxMeta ?? 10n ** 18n);
758
+ }
759
+
760
+ /**
761
+ * This function gives the space with only the options that support each shape kind of `shapes`. Without `shapes`, it gives the same space.
762
+ * A stage can lose all of its options. Then the space has no config with that stage.
763
+ * A pool keeps only the entries whose members fit the smaller space. A meta option without such an entry goes out.
764
+ */
765
+ function restrict(space: StrategySpace, shapes: readonly ShapeKind[] | undefined): StrategySpace {
766
+ if (shapes === undefined) return space;
767
+ const kept: StrategySpace = {
768
+ ...space,
769
+ stages: space.stages.map((s) => ({ ...s, options: s.options.filter((o) => supportsShapes(o, shapes)) })),
770
+ ...(space.parts === undefined ? {} : { parts: space.parts.map((p) => restrict(p, shapes)) }),
771
+ };
772
+ const withPool = (s: SpaceStage, o: StageOption): StageOption[] => {
773
+ if (o.members?.pool === undefined) return [o];
774
+ const part = partOf(kept, s.name).space;
775
+ const pool = o.members.pool.filter((entry) => fits(part, entry));
776
+ return pool.length === 0 ? [] : [{ ...o, members: { ...o.members, pool } }];
777
+ };
778
+ return { ...kept, stages: kept.stages.map((s) => ({ ...s, options: s.options.flatMap((o) => withPool(s, o)) })) };
779
+ }
780
+
781
+ /** This function throws a `RangeError` for a list of shape kinds with a kind that is not declared. */
782
+ function assertShapes(shapes: readonly ShapeKind[] | undefined): void {
783
+ if (shapes === undefined) return;
784
+ if (!Array.isArray(shapes)) throw new RangeError("The option shapes is not a list of shape kinds.");
785
+ for (const k of shapes) if (!isShapeKind(k)) throw new RangeError(`The shape kind ${show(k)} is not one of ${SHAPE_KIND_IDS.join(", ")}.`);
786
+ }
787
+
788
+ /** This function gives the count of the grid points of a space that is known to be good. */
789
+ function sizeOf(space: StrategySpace, levels: number, limit: bigint): SpaceSize {
654
790
  let base = 0n;
655
791
  let meta: bigint | null = 0n;
656
- for (const c of combos(space, options.levels, true, limit)) {
792
+ for (const c of combos(space, levels, true, limit)) {
657
793
  if (c.base) base += c.weight!;
658
794
  else meta = meta === null || c.weight === null || meta + c.weight > limit ? null : meta + c.weight;
659
795
  }
@@ -674,11 +810,12 @@ function* product<T>(lists: readonly (readonly T[])[], i = 0, acc: T[] = []): Ge
674
810
  }
675
811
 
676
812
  /** This function gives the configs of the grid of a space, without meta options. */
677
- function gridConfigs(space: StrategySpace, levels: number, limit: number): StrategyConfig[] {
678
- const size = spaceSize(space, { levels, maxMeta: 0n }).base;
813
+ function gridConfigs(space: StrategySpace, levels: number, limit: number, shapes: readonly ShapeKind[] | undefined): StrategyConfig[] {
814
+ const kept = restrict(space, shapes);
815
+ const size = sizeOf(kept, levels, 0n).base;
679
816
  if (size > BigInt(limit)) throw new RangeError(`The grid of the space ${space.engine} has ${size} configs, more than the limit ${limit}.`);
680
817
  const out: StrategyConfig[] = [];
681
- for (const c of combos(space, levels, false, 0n)) {
818
+ for (const c of combos(kept, levels, false, 0n)) {
682
819
  const names = [...c.choices.keys()];
683
820
  const lists = names.map((k) => {
684
821
  const o = c.choices.get(k)!;
@@ -724,6 +861,7 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
724
861
  const lists = stages.map((s) => {
725
862
  const list: (StageOption | null)[] = s.options.filter((o) => meta || o.members === undefined);
726
863
  if (s.optional) list.push(null);
864
+ if (list.length === 0) throw new Error(`The stage ${s.name} of the space ${space.engine} has no option for a sample. A shape filter can remove all of its options.`);
727
865
  return list;
728
866
  });
729
867
  const points: Map<string, StageOption | null>[] = [];
@@ -761,6 +899,14 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
761
899
  });
762
900
  }
763
901
  if (o.members === undefined) continue;
902
+ const fixed = o.members.pool;
903
+ if (fixed !== undefined) {
904
+ const r = stepRandom(seed, `pool-entry:${s.name}:${o.id}`);
905
+ permutation(users.length, r).forEach((stratum, j) => {
906
+ members[users[j]!]![k] = [...fixed[Math.min(fixed.length - 1, Math.floor(((stratum + r()) / users.length) * fixed.length))]!];
907
+ });
908
+ continue;
909
+ }
764
910
  const part = partOf(space, s.name).space;
765
911
  const poolSize = Math.max(n, 4 * o.members.max, 8);
766
912
  const seen = new Set<string>();
@@ -794,12 +940,15 @@ function sampleConfigs(space: StrategySpace, n: number, seed: number, meta: bool
794
940
  * - The mode `grid` gives each grid point without a meta option, in a stable order. Stages, options and values go in the order of the space.
795
941
  * - The mode `sample` gives `n` configs of a seeded Latin hypercube. The options of a stage get equal strata, and each active param gets its own strata.
796
942
  * - A sample draws again when a point breaks a constraint. After `maxTries` draws, it throws an `Error`. Two sampled configs can be equal.
943
+ * - With `shapes`, only the options that support each kind of the list count. Without `shapes`, the result does not change.
944
+ * - A meta option with a `pool` gives only the entries of its pool. With `shapes`, only the entries whose members support each kind count.
797
945
  */
798
946
  export function enumerateSpace(space: StrategySpace, options: EnumerateOptions): StrategyConfig[] {
799
947
  assertSpace(space);
948
+ assertShapes(options.shapes);
800
949
  if (options.mode === "grid") {
801
950
  assertWhole(options.levels, "levels", 1);
802
- return gridConfigs(space, options.levels, options.limit ?? 100_000);
951
+ return gridConfigs(space, options.levels, options.limit ?? 100_000, options.shapes);
803
952
  }
804
953
  if (options.mode !== "sample") throw new RangeError(`The mode ${String((options as { mode?: unknown }).mode)} is not grid or sample.`);
805
954
  assertWhole(options.n, "n", 0);
@@ -807,7 +956,16 @@ export function enumerateSpace(space: StrategySpace, options: EnumerateOptions):
807
956
  const meta = options.meta ?? "skip";
808
957
  if (meta !== "skip" && meta !== "sample") throw new RangeError(`The option meta is ${String(meta)}, not skip or sample.`);
809
958
  if (options.n === 0) return [];
810
- return sampleConfigs(space, options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
959
+ return sampleConfigs(restrict(space, options.shapes), options.n, options.seed, meta === "sample", options.maxTries ?? Math.max(1000, 100 * options.n));
960
+ }
961
+
962
+ /**
963
+ * This function gives the shape kinds that a space accepts, in the order of `SHAPE_KINDS`. The value comes from the `supports` of the options.
964
+ * A space accepts a kind when it has at least one config without a meta option on that kind.
965
+ */
966
+ export function spaceShapeKinds(space: StrategySpace): ShapeKind[] {
967
+ assertSpace(space);
968
+ return SHAPE_KIND_IDS.filter((k) => sizeOf(restrict(space, [k]), 1, 0n).base > 0n);
811
969
  }
812
970
 
813
971
  /**