@nmnmcc/ability 2.0.6 → 2.0.7

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.
@@ -252,16 +252,8 @@ const patternIssues = (
252
252
  }) || before
253
253
  )
254
254
  })
255
- if (memo.work.steps > Limits.searchSteps && !memo.work.noted) {
256
- memo.work.noted = true
257
- return [
258
- {
259
- path: at,
260
- issue: `The field patterns of the policy take more than ${String(Limits.searchSteps)} steps to check. The most is ${String(Limits.searchSteps)}.`,
261
- },
262
- ]
263
- }
264
- return fits
255
+ // Past the limit of the work, `ruleIssues` gives one issue for it.
256
+ return fits || memo.work.steps > Limits.searchSteps
265
257
  ? []
266
258
  : [
267
259
  {
@@ -314,15 +306,45 @@ const principalField = (
314
306
  const isIssue = (value: Catalog.Field | Issue): value is Issue =>
315
307
  "issue" in value
316
308
 
317
- /** The issues of one operand of a test of a field. */
309
+ /**
310
+ * The fields that a test reads, with one field for each distinct kind of
311
+ * value. A rule for very many kinds may have one field type in all of them,
312
+ * so each value of the test is checked one time for each distinct kind, not
313
+ * one time for each resource. The kinds are found by their text in a native
314
+ * `Map`, which belongs to this call.
315
+ */
316
+ const distinctFields = (
317
+ context: Context,
318
+ field: string,
319
+ ): ReadonlyArray<Catalog.Field> =>
320
+ Array.from(
321
+ new Map(
322
+ Arr.map(
323
+ fieldsAt(context, field),
324
+ (found) => [JSON.stringify(found.kinds), found] as const,
325
+ ),
326
+ ).values(),
327
+ )
328
+
329
+ /**
330
+ * Counts the work of the checks of the values of a test, as the work of the
331
+ * field patterns counts. Past the limit, the values are not checked, and
332
+ * `ruleIssues` gives one issue for the work.
333
+ */
334
+ const spendChecks = (memo: Memo, values: number, fields: number): boolean => {
335
+ memo.work.steps += values * Math.max(fields, 1)
336
+ return memo.work.steps <= Limits.searchSteps
337
+ }
338
+
339
+ /** The issues of one operand of a test of a field, with its distinct fields. */
318
340
  const operandIssues = (
319
341
  context: Context,
320
342
  field: string,
343
+ fields: ReadonlyArray<Catalog.Field>,
321
344
  ordered: boolean,
322
345
  operand: Operand,
323
346
  at: ReadonlyArray<PropertyKey>,
324
347
  ): ReadonlyArray<Issue> => {
325
- const fields = fieldsAt(context, field)
326
348
  if (operand._tag === "Literal") {
327
349
  const value = operand.value
328
350
  if (ordered && typeof value !== "string" && typeof value !== "number") {
@@ -520,8 +542,17 @@ const conditionIssues = (
520
542
  }
521
543
  seen.add(node)
522
544
  const here = (issues: ReadonlyArray<Issue>) => under(context.memo, at, issues)
545
+ // A test looks its field up in each resource of the rule. A rule for very
546
+ // many kinds with very many tests is very much work, so it counts. Past
547
+ // the limit, `ruleIssues` gives one issue for it, and the policy does not
548
+ // load.
549
+ const room = (values: number) =>
550
+ spendChecks(context.memo, 1 + values, context.named.length)
523
551
  switch (node._tag) {
524
552
  case "Compare": {
553
+ if (!room(1)) {
554
+ return []
555
+ }
525
556
  const exists = readIssues(context, node.field, ["field"])
526
557
  return here(
527
558
  exists.length > 0
@@ -529,6 +560,7 @@ const conditionIssues = (
529
560
  : operandIssues(
530
561
  context,
531
562
  node.field,
563
+ distinctFields(context, node.field),
532
564
  node.op !== "eq" && node.op !== "ne",
533
565
  node.value,
534
566
  ["value"],
@@ -536,20 +568,28 @@ const conditionIssues = (
536
568
  )
537
569
  }
538
570
  case "In": {
571
+ if (!room(0)) {
572
+ return []
573
+ }
539
574
  const exists = readIssues(context, node.field, ["field"])
575
+ if (exists.length > 0) {
576
+ return here(exists)
577
+ }
578
+ const fields = distinctFields(context, node.field)
579
+ if (!spendChecks(context.memo, node.values.length, fields.length)) {
580
+ return []
581
+ }
540
582
  return here(
541
- exists.length > 0
542
- ? exists
543
- : Arr.flatMap(node.values, (operand, index) =>
544
- operandIssues(context, node.field, false, operand, [
545
- "values",
546
- index,
547
- ]),
548
- ),
583
+ Arr.flatMap(node.values, (operand, index) =>
584
+ operandIssues(context, node.field, fields, false, operand, [
585
+ "values",
586
+ index,
587
+ ]),
588
+ ),
549
589
  )
550
590
  }
551
591
  case "Exists":
552
- return here(readIssues(context, node.field, ["field"]))
592
+ return room(0) ? here(readIssues(context, node.field, ["field"])) : []
553
593
  case "Under":
554
594
  return here(underIssues(context, node.prefix, []))
555
595
  case "And":
@@ -582,15 +622,18 @@ const hasAction = (
582
622
  Arr.some(actions, (action) => descriptor.actions.includes(action))
583
623
 
584
624
  /**
585
- * Checks one rule against a domain. It gives one issue for each problem. `at`
586
- * is the path of the rule, for the paths of the issues.
625
+ * Checks one rule against a domain. It gives one issue for each problem, up
626
+ * to the most issues of the binding. `at` gives the path of the rule, and it
627
+ * is made only for an issue that the binding keeps: a rule with very many
628
+ * problems deep in a tree does not make a full path for each one.
587
629
  */
588
630
  export const ruleIssues = (
589
631
  domain: Domain.Any,
590
632
  rule: RuleData,
591
- at: ReadonlyArray<PropertyKey>,
633
+ at: LazyPath,
592
634
  memo: Memo = makeMemo(),
593
635
  ): ReadonlyArray<Issue> => {
636
+ const here = (issues: ReadonlyArray<Issue>) => under(memo, at, issues)
594
637
  const all = rule.resources.includes("*")
595
638
  // Each kind is checked one time, also when the list names it again. The
596
639
  // names come from a policy, so a native `Set` finds the copies:
@@ -623,46 +666,77 @@ export const ruleIssues = (
623
666
  const actions = Array.from(
624
667
  new Set(Arr.flatMap(known, (descriptor) => descriptor.actions)),
625
668
  )
669
+ const fieldIssues = Arr.flatMap(rule.fields ?? [], (field, index) =>
670
+ here(
671
+ field.includes("*")
672
+ ? patternIssues(memo, known, field, ["fields", index])
673
+ : fieldIssue(context, field, ["fields", index]),
674
+ ),
675
+ )
676
+ const whenIssues =
677
+ rule.when === undefined
678
+ ? []
679
+ : Nodes.isNode(rule.when)
680
+ ? conditionIssues(
681
+ context,
682
+ rule.when,
683
+ () => [...at(), "when"],
684
+ new Set(),
685
+ )
686
+ : here([notWhole(["when"])])
687
+ // The work is known only after every check of the rule. Past the limit, a
688
+ // check gives no issue, so this issue must come, or the policy would load
689
+ // with checks left out.
690
+ const work = memo.work.steps > Limits.searchSteps && !memo.work.noted
691
+ memo.work.noted ||= work
626
692
  return [
627
- ...Arr.map(
693
+ ...Arr.flatMap(
628
694
  Arr.filter(names, (kind) => !HashMap.has(domain.resources, kind)),
629
- (kind): Issue => ({
630
- path: [...at, "resources"],
631
- issue: suggestIn(
632
- memo,
633
- `There is no resource with the kind "${kind}"`,
634
- kind,
635
- "~kinds",
636
- () => kinds,
637
- ),
638
- }),
695
+ (kind: string) =>
696
+ here([
697
+ {
698
+ path: ["resources"],
699
+ issue: suggestIn(
700
+ memo,
701
+ `There is no resource with the kind "${kind}"`,
702
+ kind,
703
+ "~kinds",
704
+ () => kinds,
705
+ ),
706
+ },
707
+ ]),
639
708
  ),
640
- ...Arr.map(
709
+ ...Arr.flatMap(
641
710
  Arr.filter(
642
711
  rule.actions,
643
712
  (action) => action !== "*" && !actions.includes(action),
644
713
  ),
645
- (action): Issue => ({
646
- path: [...at, "actions"],
647
- issue: suggestIn(
648
- memo,
649
- `The action "${action}" is not an action of these resources`,
650
- action,
651
- `~actions\u0000${names.join(",")}`,
652
- () => actions,
653
- ),
654
- }),
655
- ),
656
- ...Arr.flatMap(rule.fields ?? [], (field, index) =>
657
- field.includes("*")
658
- ? patternIssues(memo, known, field, [...at, "fields", index])
659
- : fieldIssue(context, field, [...at, "fields", index]),
714
+ (action: string) =>
715
+ here([
716
+ {
717
+ path: ["actions"],
718
+ issue: suggestIn(
719
+ memo,
720
+ `The action "${action}" is not an action of these resources`,
721
+ action,
722
+ `~actions\u0000${names.join(",")}`,
723
+ () => actions,
724
+ ),
725
+ },
726
+ ]),
660
727
  ),
661
- ...(rule.when === undefined
662
- ? []
663
- : Nodes.isNode(rule.when)
664
- ? conditionIssues(context, rule.when, () => [...at, "when"], new Set())
665
- : [notWhole([...at, "when"])]),
728
+ ...fieldIssues,
729
+ ...whenIssues,
730
+ // The issue of the work is not one of the most issues of the binding,
731
+ // so a policy past the limit always says why.
732
+ ...(work
733
+ ? [
734
+ {
735
+ path: at(),
736
+ issue: `The policy takes more than ${String(Limits.searchSteps)} steps to check: its field patterns, and the fields and values of its tests on each resource. The most is ${String(Limits.searchSteps)}.`,
737
+ },
738
+ ]
739
+ : []),
666
740
  ]
667
741
  }
668
742
 
@@ -725,7 +799,7 @@ export const treeIssues = (
725
799
  const memo = makeMemo()
726
800
  return Arr.flatMap(rulesOf(root, undefined, 1), (found) =>
727
801
  Array.isArray(found)
728
- ? ruleIssues(domain, found[0], Place.pathOf(found[1]), memo)
802
+ ? ruleIssues(domain, found[0], () => Place.pathOf(found[1]), memo)
729
803
  : [found as Issue],
730
804
  )
731
805
  }
@@ -418,15 +418,6 @@ const namesOf = (
418
418
  */
419
419
  const branchOf = (place: Place, count: { size: number }): Branch => {
420
420
  const structs = Arr.filter(Arr.map(place.schemas, structOf), isStruct)
421
- // The structs of classes. A value of a class is not copied into a plain
422
- // object, so it keeps its getters and methods.
423
- const classes = Arr.filter(
424
- Arr.map(
425
- Arr.filter(place.schemas, (schema) => schema._tag === "Declaration"),
426
- structOf,
427
- ),
428
- isStruct,
429
- )
430
421
  const values = Arr.filter(
431
422
  place.schemas,
432
423
  (schema) => !isStruct(structOf(schema)),
@@ -488,12 +479,18 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
488
479
  }
489
480
  }
490
481
  const structProperties = Arr.map(structs, propertiesOf)
491
- const classProperties = Arr.map(classes, propertiesOf)
492
482
  const children = Arr.map(namesOf(structs), (name) => {
493
483
  const properties = Arr.flatMap(structProperties, (named) => {
494
484
  const property = named.get(name)
495
485
  return property === undefined ? [] : [property]
496
486
  })
487
+ // A struct without the name may still have it as an extra field, with
488
+ // any value: only a decode or a `make` takes extra fields away, and a
489
+ // decision may get a value made by hand. A value that may be any
490
+ // object, as one of `Schema.Unknown` may, may have the name with any
491
+ // value too.
492
+ const top =
493
+ place.top || valueKinds.object || properties.length < structs.length
497
494
  const below: Place = {
498
495
  path: [...place.path, name],
499
496
  schemas: Arr.flatMap(properties, (property) =>
@@ -505,15 +502,10 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
505
502
  place.missing ||
506
503
  values.length > 0 ||
507
504
  Arr.some(properties, (property) => SchemaAST.isOptional(property.type)),
508
- // A struct without the name may still have it as an extra field,
509
- // with any value: only a decode takes extra fields away. A value
510
- // that may be any object, as one of `Schema.Unknown` may, may have
511
- // the name with any value too.
512
- top: place.top || valueKinds.object || properties.length < structs.length,
513
- plain:
514
- place.plain &&
515
- !valueKinds.object &&
516
- Arr.every(classProperties, (named) => named.has(name)),
505
+ top,
506
+ // A value that may be anything may be a list, or have a getter, so
507
+ // a read below it may not find plain data.
508
+ plain: place.plain && !top,
517
509
  }
518
510
  return [name, branchOf(below, count)] as const
519
511
  })
@@ -522,6 +514,8 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
522
514
  if (signatures.length === 0) {
523
515
  return { field, children, names, key: undefined, keyBranch: undefined }
524
516
  }
517
+ const keyTop =
518
+ place.top || valueKinds.object || records.length < structs.length
525
519
  const key: Key = {
526
520
  _tag: "Key",
527
521
  parameters: Arr.map(signatures, (signature) => signature.parameter),
@@ -534,11 +528,8 @@ const branchOf = (place: Place, count: { size: number }): Branch => {
534
528
  ),
535
529
  // A record may not have a given key.
536
530
  missing: true,
537
- top: place.top || valueKinds.object || records.length < structs.length,
538
- plain:
539
- place.plain &&
540
- !valueKinds.object &&
541
- Arr.every(classes, (struct) => struct.indexSignatures.length > 0),
531
+ top: keyTop,
532
+ plain: place.plain && !keyTop,
542
533
  },
543
534
  count,
544
535
  )
@@ -124,24 +124,39 @@ const checkOperands = (values: unknown): ReadonlyArray<Operand> => {
124
124
 
125
125
  interface Facts {
126
126
  readonly size: number
127
+ readonly weight: number
127
128
  readonly depth: number
128
129
  readonly opaque: boolean
129
130
  }
130
131
 
131
- const leaf: Facts = { size: 1, depth: 1, opaque: false }
132
+ const leaf: Facts = { size: 1, weight: 1, depth: 1, opaque: false }
132
133
 
133
- /** The facts of a node with children. It stops a node past the limits. */
134
- const factsOf = (children: ReadonlyArray<Node>): Facts => {
135
- const size = Arr.reduce(children, 1, (total, child) => total + child.size)
136
- if (size > Limits.conditionNodes) {
134
+ /**
135
+ * The facts of a test with values, as `In` and `Under`. Each value counts in
136
+ * the weight, since a walk that reads the test reads its values too. It stops
137
+ * a test past the limits.
138
+ */
139
+ const valuesFacts = (values: ReadonlyArray<Operand>): Facts =>
140
+ checkWeight({ ...leaf, weight: 1 + values.length })
141
+
142
+ const checkWeight = (facts: Facts): Facts => {
143
+ if (facts.weight > Limits.conditionNodes) {
137
144
  throw Limits.exceeded(
138
145
  "A condition",
139
146
  "conditionNodes",
140
- "nodes",
141
- size,
147
+ "nodes and values",
148
+ facts.weight,
142
149
  Limits.conditionNodes,
143
150
  )
144
151
  }
152
+ return facts
153
+ }
154
+
155
+ /** The facts of a node with children. It stops a node past the limits. */
156
+ const factsOf = (children: ReadonlyArray<Node>): Facts => {
157
+ const size = Arr.reduce(children, 1, (total, child) => total + child.size)
158
+ const weight = Arr.reduce(children, 1, (total, child) => total + child.weight)
159
+ checkWeight({ ...leaf, weight })
145
160
  const depth =
146
161
  1 + Arr.reduce(children, 0, (top, child) => Math.max(top, child.depth))
147
162
  if (depth > Limits.conditionDepth) {
@@ -155,6 +170,7 @@ const factsOf = (children: ReadonlyArray<Node>): Facts => {
155
170
  }
156
171
  return {
157
172
  size,
173
+ weight,
158
174
  depth,
159
175
  opaque: Arr.some(children, (child) => child.opaque),
160
176
  }
@@ -183,6 +199,15 @@ abstract class Base extends Data.Class {
183
199
  return this.#facts.size
184
200
  }
185
201
 
202
+ /**
203
+ * The number of nodes and of the values of their `In` and `Under` tests,
204
+ * with a shared child counted for each place. A walk that reads the tests
205
+ * does this much work, so the limits of the nodes count it.
206
+ */
207
+ get weight(): number {
208
+ return this.#facts.weight
209
+ }
210
+
186
211
  /** The number of levels. */
187
212
  get depth(): number {
188
213
  return this.#facts.depth
@@ -244,10 +269,8 @@ export class In extends Base {
244
269
  readonly field: string
245
270
  readonly values: ReadonlyArray<Operand>
246
271
  }) {
247
- super(
248
- { field: checkPath(props.field), values: checkOperands(props.values) },
249
- leaf,
250
- )
272
+ const values = checkOperands(props.values)
273
+ super({ field: checkPath(props.field), values }, valuesFacts(values))
251
274
  }
252
275
  }
253
276
 
@@ -265,7 +288,8 @@ export class Under extends Base {
265
288
  readonly _tag = "Under"
266
289
  declare readonly prefix: ReadonlyArray<Operand>
267
290
  constructor(props: { readonly prefix: ReadonlyArray<Operand> }) {
268
- super({ prefix: checkOperands(props.prefix) }, leaf)
291
+ const prefix = checkOperands(props.prefix)
292
+ super({ prefix }, valuesFacts(prefix))
269
293
  }
270
294
  }
271
295
 
@@ -333,7 +357,7 @@ export class Predicate extends Base {
333
357
  }
334
358
  super(
335
359
  { name: props.name, run: props.run },
336
- { size: 1, depth: 1, opaque: true },
360
+ { size: 1, weight: 1, depth: 1, opaque: true },
337
361
  )
338
362
  }
339
363
  }
@@ -130,7 +130,8 @@ export class Rules extends Base {
130
130
  size: Arr.reduce(
131
131
  conditions,
132
132
  1,
133
- (total, when) => total + 1 + (when?.size ?? 0),
133
+ // A condition counts its nodes and the values of its tests.
134
+ (total, when) => total + 1 + (when?.weight ?? 0),
134
135
  ),
135
136
  depth: 1,
136
137
  opaque: Arr.some(conditions, (when) => when?.opaque ?? false),
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Walks that do not fill the stack.
3
+ *
4
+ * A walk is written one time, with the steps of a {@link Runner}, and a
5
+ * runner runs it:
6
+ * - {@link direct} runs each step at once, as a plain call does. It is fast,
7
+ * and it is for an input with few levels.
8
+ * - {@link loop} puts each step into the run loop of an Effect. A step gives
9
+ * the next step in place of calling it, and the loop runs one step after
10
+ * the other, so the stack does not grow with the levels of the input. This
11
+ * is a trampoline.
12
+ *
13
+ * {@link run} takes `direct` for an input with at most `directDepth` levels,
14
+ * and `loop` for a deeper one. A walk makes each call for a child through
15
+ * `suspend`, so the loop makes the step of the child only when it runs it.
16
+ */
17
+ import * as Arr from "effect/Array"
18
+ import * as Cause from "effect/Cause"
19
+ import * as Effect from "effect/Effect"
20
+ import * as Exit from "effect/Exit"
21
+ import * as Option from "effect/Option"
22
+
23
+ /**
24
+ * A step of a walk that gives an `A`. A runner of steps knows what a step
25
+ * is: the value itself for `direct`, and an Effect for `loop`. The type is
26
+ * only in the types.
27
+ */
28
+ export interface Step<A> {
29
+ readonly "~Step": (_: never) => A
30
+ }
31
+
32
+ /** The steps that a walk may take. */
33
+ export interface Runner {
34
+ readonly succeed: <A>(value: A) => Step<A>
35
+ /** The step that `step` makes, when the runner runs it. */
36
+ readonly suspend: <A>(step: () => Step<A>) => Step<A>
37
+ readonly map: <A, B>(step: Step<A>, f: (value: A) => B) => Step<B>
38
+ readonly flatMap: <A, B>(step: Step<A>, f: (value: A) => Step<B>) => Step<B>
39
+ /** Runs a step for each item, in order, and gives their values. */
40
+ readonly forEach: <A, B>(
41
+ items: ReadonlyArray<A>,
42
+ f: (item: A, index: number) => Step<B>,
43
+ ) => Step<ReadonlyArray<B>>
44
+ /** Runs a step for each item, in order, up to the first `true`. */
45
+ readonly some: <A>(
46
+ items: ReadonlyArray<A>,
47
+ f: (item: A) => Step<boolean>,
48
+ ) => Step<boolean>
49
+ /** Runs a step for each item, in order, up to the first `false`. */
50
+ readonly every: <A>(
51
+ items: ReadonlyArray<A>,
52
+ f: (item: A) => Step<boolean>,
53
+ ) => Step<boolean>
54
+ }
55
+
56
+ // A step of `direct` is its value. The casts below only change the type.
57
+ const value = <A>(step: Step<A>): A => step as unknown as A
58
+ const step = <A>(value: A): Step<A> => value as unknown as Step<A>
59
+
60
+ /** Runs each step at once, on the stack. */
61
+ export const direct: Runner = {
62
+ succeed: step,
63
+ suspend: (make) => make(),
64
+ map: (from, f) => step(f(value(from))),
65
+ flatMap: (from, f) => f(value(from)),
66
+ forEach: (items, f) =>
67
+ step(Arr.map(items, (item, index) => value(f(item, index)))),
68
+ some: (items, f) => step(Arr.some(items, (item) => value(f(item)))),
69
+ every: (items, f) => step(Arr.every(items, (item) => value(f(item)))),
70
+ }
71
+
72
+ // A step of `loop` is an Effect with no error and no services. A failure of a
73
+ // walk is a thrown error, which `run` gives back as it was.
74
+ type Loop<A> = Effect.Effect<A>
75
+ const effect = <A>(step: Step<A>): Loop<A> => step as unknown as Loop<A>
76
+ const fromEffect = <A>(effect: Loop<A>): Step<A> => effect as unknown as Step<A>
77
+
78
+ /** Runs each step in the run loop of an Effect, so a deep walk does not fill the stack. */
79
+ export const loop: Runner = {
80
+ succeed: (value) => fromEffect(Effect.succeed(value)),
81
+ suspend: (make) => fromEffect(Effect.suspend(() => effect(make()))),
82
+ map: (from, f) => fromEffect(Effect.map(effect(from), f)),
83
+ flatMap: (from, f) =>
84
+ fromEffect(Effect.flatMap(effect(from), (value) => effect(f(value)))),
85
+ forEach: (items, f) =>
86
+ fromEffect(Effect.forEach(items, (item, index) => effect(f(item, index)))),
87
+ some: (items, f) =>
88
+ fromEffect(
89
+ Effect.map(
90
+ Effect.findFirst(items, (item) => effect(f(item))),
91
+ Option.isSome,
92
+ ),
93
+ ),
94
+ every: (items, f) =>
95
+ fromEffect(
96
+ Effect.map(
97
+ Effect.findFirst(items, (item) =>
98
+ Effect.map(effect(f(item)), (value) => !value),
99
+ ),
100
+ Option.isNone,
101
+ ),
102
+ ),
103
+ }
104
+
105
+ /**
106
+ * The most levels of an input that `run` walks with `direct`. A level takes
107
+ * a few calls of the walk and of the runner on the stack, so this keeps the
108
+ * stack far from full, and most inputs take the fast way.
109
+ */
110
+ export const directDepth = 200
111
+
112
+ /**
113
+ * Gives the runner for an input with `depth` levels. The walks of a condition
114
+ * or a policy tree know the depth of the input from its node. A walk of an
115
+ * input with an unknown depth, as a decoder, takes `loop`.
116
+ */
117
+ export const runnerFor = (depth: number): Runner =>
118
+ depth <= directDepth ? direct : loop
119
+
120
+ /**
121
+ * Runs a walk at once, with the runner for `depth` levels, and gives its
122
+ * value. An error that a step throws comes out as it was, also from `loop`.
123
+ */
124
+ export const run = <A>(depth: number, walk: (runner: Runner) => Step<A>): A =>
125
+ runWith(runnerFor(depth), walk)
126
+
127
+ /** Runs a walk at once with a runner, as {@link run} does. */
128
+ export const runWith = <A>(
129
+ runner: Runner,
130
+ walk: (runner: Runner) => Step<A>,
131
+ ): A => {
132
+ if (runner === direct) {
133
+ return value(walk(direct))
134
+ }
135
+ return Exit.match(Effect.runSyncExit(effect(walk(loop))), {
136
+ onSuccess: (result) => result,
137
+ onFailure: (cause) => {
138
+ throw Cause.squash(cause)
139
+ },
140
+ })
141
+ }
142
+
143
+ /**
144
+ * Gives the Effect of a step of `loop`, so a walk can go on in an Effect
145
+ * that runs a predicate.
146
+ */
147
+ export const toEffect = <A>(step: Step<A>): Effect.Effect<A> => effect(step)