@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.
- package/README.md +6 -5
- package/dist/Condition.d.ts +2 -2
- package/dist/internal/algebra.js +100 -73
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/analysis.js +1 -1
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.js +6 -5
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +52 -35
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.js +6 -6
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/nodes.d.ts +7 -0
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +34 -4
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +1 -1
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/trampoline.js +75 -0
- package/dist/internal/trampoline.js.map +1 -0
- package/package.json +1 -1
- package/src/internal/algebra.ts +218 -135
- package/src/internal/analysis.ts +3 -1
- package/src/internal/authorizer.ts +8 -5
- package/src/internal/bind.ts +131 -57
- package/src/internal/catalog.ts +15 -24
- package/src/internal/nodes.ts +37 -13
- package/src/internal/policy-nodes.ts +2 -1
- package/src/internal/trampoline.ts +147 -0
package/src/internal/bind.ts
CHANGED
|
@@ -252,16 +252,8 @@ const patternIssues = (
|
|
|
252
252
|
}) || before
|
|
253
253
|
)
|
|
254
254
|
})
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
586
|
-
*
|
|
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:
|
|
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.
|
|
693
|
+
...Arr.flatMap(
|
|
628
694
|
Arr.filter(names, (kind) => !HashMap.has(domain.resources, kind)),
|
|
629
|
-
(kind
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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.
|
|
709
|
+
...Arr.flatMap(
|
|
641
710
|
Arr.filter(
|
|
642
711
|
rule.actions,
|
|
643
712
|
(action) => action !== "*" && !actions.includes(action),
|
|
644
713
|
),
|
|
645
|
-
(action
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
...
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
}
|
package/src/internal/catalog.ts
CHANGED
|
@@ -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
|
-
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
|
|
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:
|
|
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
|
)
|
package/src/internal/nodes.ts
CHANGED
|
@@ -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
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|