@nmnmcc/ability 2.0.7 → 2.1.2
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 +52 -29
- package/dist/Authorizer.d.ts +21 -12
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +121 -19
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +32 -21
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +9 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +5 -0
- package/dist/Domain.js.map +1 -1
- package/dist/Limits.d.ts +44 -13
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +52 -22
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +102 -18
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +7 -6
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +2 -2
- package/dist/internal/analysis.js +81 -75
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +11 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +86 -64
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +33 -10
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +170 -77
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +27 -31
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/domain.js +19 -5
- package/dist/internal/domain.js.map +1 -1
- package/dist/internal/json.js +120 -76
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/limits.d.ts +28 -0
- package/dist/internal/limits.d.ts.map +1 -1
- package/dist/internal/limits.js +75 -16
- package/dist/internal/limits.js.map +1 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +53 -44
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +50 -15
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +169 -127
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +16 -13
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +38 -22
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/resource.js +1 -1
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +6 -5
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +106 -92
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +19 -5
- package/dist/internal/trampoline.js.map +1 -1
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +317 -86
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +143 -21
- package/src/internal/analysis.ts +154 -124
- package/src/internal/authorizer.ts +29 -14
- package/src/internal/bind.ts +193 -136
- package/src/internal/catalog.ts +320 -118
- package/src/internal/decide.ts +61 -50
- package/src/internal/domain.ts +39 -3
- package/src/internal/json.ts +212 -117
- package/src/internal/limits.ts +101 -31
- package/src/internal/nodes.ts +65 -47
- package/src/internal/patterns.ts +79 -25
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +31 -23
- package/src/internal/policy.ts +63 -23
- package/src/internal/rule.ts +16 -3
- package/src/internal/search.ts +186 -136
- package/src/internal/trampoline.ts +51 -4
package/src/internal/policy.ts
CHANGED
|
@@ -15,10 +15,12 @@ import * as Condition from "../Condition.ts"
|
|
|
15
15
|
import type * as Domain from "../Domain.ts"
|
|
16
16
|
import type * as Policy from "../Policy.ts"
|
|
17
17
|
import * as Bind from "./bind.ts"
|
|
18
|
+
import * as Limits from "./limits.ts"
|
|
18
19
|
import * as Nodes from "./nodes.ts"
|
|
19
20
|
import * as PolicyJson from "./policy-json.ts"
|
|
20
21
|
import * as PolicyNodes from "./policy-nodes.ts"
|
|
21
22
|
import * as RuleData from "./rule.ts"
|
|
23
|
+
import * as Trampoline from "./trampoline.ts"
|
|
22
24
|
|
|
23
25
|
type Any = Policy.Any
|
|
24
26
|
type Node = Policy.Node
|
|
@@ -60,8 +62,24 @@ export class PolicyData<
|
|
|
60
62
|
/** The node of a policy, with no types, for the code below. */
|
|
61
63
|
export const nodeOf = (policy: Any): PolicyNodes.Node => policy.node as never
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Makes a policy of a domain from its tree. The constructors of the nodes
|
|
67
|
+
* check the ceiling of `policyNodes`, since a node does not know its domain,
|
|
68
|
+
* so this checks the limit of the domain.
|
|
69
|
+
*/
|
|
70
|
+
export const fromNode = (domain: Domain.Any, node: PolicyNodes.Node): any => {
|
|
71
|
+
const most = domain.limits.policyNodes
|
|
72
|
+
if (node.size > most) {
|
|
73
|
+
throw Limits.exceeded(
|
|
74
|
+
"A policy tree",
|
|
75
|
+
"policyNodes",
|
|
76
|
+
"nodes",
|
|
77
|
+
node.size,
|
|
78
|
+
most,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
return new PolicyData({ domain, node })
|
|
82
|
+
}
|
|
65
83
|
|
|
66
84
|
/**
|
|
67
85
|
* The error when a rule does not fit its domain. It has one issue for each
|
|
@@ -191,28 +209,50 @@ const provideNode = (
|
|
|
191
209
|
}),
|
|
192
210
|
]),
|
|
193
211
|
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
// The walk of the tree takes the steps of a trampoline, so a deep tree
|
|
213
|
+
// does not fill the stack.
|
|
214
|
+
return Trampoline.run(root.depth, (R) => {
|
|
215
|
+
const provide = (
|
|
216
|
+
node: PolicyNodes.Node,
|
|
217
|
+
): Trampoline.Step<PolicyNodes.Node> => {
|
|
218
|
+
if (!node.opaque) {
|
|
219
|
+
return R.succeed(node)
|
|
220
|
+
}
|
|
221
|
+
switch (node._tag) {
|
|
222
|
+
case "Rules":
|
|
223
|
+
return R.succeed(
|
|
224
|
+
new PolicyNodes.Rules({
|
|
225
|
+
algorithm: node.algorithm,
|
|
226
|
+
rules: Arr.map(
|
|
227
|
+
node.rules,
|
|
228
|
+
(value) => provided.get(value) ?? value,
|
|
229
|
+
),
|
|
230
|
+
}),
|
|
231
|
+
)
|
|
232
|
+
case "All":
|
|
233
|
+
case "Any":
|
|
234
|
+
case "FirstOf": {
|
|
235
|
+
const tag = node._tag
|
|
236
|
+
return R.map(
|
|
237
|
+
R.forEach(node.policies, (child) =>
|
|
238
|
+
R.suspend(() => provide(child)),
|
|
239
|
+
),
|
|
240
|
+
(policies) => new PolicyNodes[tag](policies),
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
case "Ceiling":
|
|
244
|
+
return R.flatMap(
|
|
245
|
+
R.suspend(() => provide(node.cap)),
|
|
246
|
+
(cap) =>
|
|
247
|
+
R.map(
|
|
248
|
+
R.suspend(() => provide(node.policy)),
|
|
249
|
+
(policy) => new PolicyNodes.Ceiling({ cap, policy }),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
213
253
|
}
|
|
214
|
-
|
|
215
|
-
|
|
254
|
+
return provide(root)
|
|
255
|
+
})
|
|
216
256
|
}
|
|
217
257
|
|
|
218
258
|
/** Gives the services in `context` to every predicate of a policy. */
|
package/src/internal/rule.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type * as Domain from "../Domain.ts"
|
|
|
12
12
|
import type * as Policy from "../Policy.ts"
|
|
13
13
|
import type * as Resource from "../Resource.ts"
|
|
14
14
|
import * as Catalog from "./catalog.ts"
|
|
15
|
+
import * as Limits from "./limits.ts"
|
|
15
16
|
import * as Nodes from "./nodes.ts"
|
|
16
17
|
import * as ResourceData from "./resource.ts"
|
|
17
18
|
import * as Patterns from "./patterns.ts"
|
|
@@ -167,6 +168,7 @@ export const matchesInside = (pattern: string, field: string): boolean =>
|
|
|
167
168
|
*/
|
|
168
169
|
export const fitsOf = (
|
|
169
170
|
catalog: Catalog.Catalog,
|
|
171
|
+
parts: number,
|
|
170
172
|
): ((pattern: string) => boolean) => {
|
|
171
173
|
// A native `Map`: the hash of a string in Effect has no random seed, and a
|
|
172
174
|
// policy could hold very many patterns with one hash. The engine hashes a
|
|
@@ -177,7 +179,11 @@ export const fitsOf = (
|
|
|
177
179
|
if (known !== undefined) {
|
|
178
180
|
return known
|
|
179
181
|
}
|
|
180
|
-
const found = Patterns.fitsCatalog(
|
|
182
|
+
const found = Patterns.fitsCatalog(
|
|
183
|
+
Patterns.parseBound(pattern),
|
|
184
|
+
catalog,
|
|
185
|
+
parts,
|
|
186
|
+
)
|
|
181
187
|
memo.set(pattern, found)
|
|
182
188
|
return found
|
|
183
189
|
}
|
|
@@ -205,6 +211,8 @@ export const matchesTarget = (
|
|
|
205
211
|
readonly descriptor?: Resource.Descriptor | undefined
|
|
206
212
|
/** The test of {@link fitsOf} for the catalog of the descriptor. */
|
|
207
213
|
readonly fits?: ((pattern: string) => boolean) | undefined
|
|
214
|
+
/** The most parts of a field path in the domain. */
|
|
215
|
+
readonly parts?: number | undefined
|
|
208
216
|
},
|
|
209
217
|
): boolean => {
|
|
210
218
|
if (!rule.actions.includes("*") && !rule.actions.includes(target.action)) {
|
|
@@ -226,7 +234,11 @@ export const matchesTarget = (
|
|
|
226
234
|
return (
|
|
227
235
|
rule._tag === "Allow" &&
|
|
228
236
|
(descriptor === undefined ||
|
|
229
|
-
Arr.some(
|
|
237
|
+
Arr.some(
|
|
238
|
+
rule.fields,
|
|
239
|
+
target.fits ??
|
|
240
|
+
fitsOf(descriptor.catalog, target.parts ?? Limits.fieldPathParts),
|
|
241
|
+
))
|
|
230
242
|
)
|
|
231
243
|
}
|
|
232
244
|
const field = target.field
|
|
@@ -236,7 +248,8 @@ export const matchesTarget = (
|
|
|
236
248
|
const open =
|
|
237
249
|
rule._tag === "Deny" &&
|
|
238
250
|
target.descriptor !== undefined &&
|
|
239
|
-
(Catalog.lookup(target.descriptor.catalog, field)?.open ??
|
|
251
|
+
(Catalog.lookup(target.descriptor.catalog, field, target.parts)?.open ??
|
|
252
|
+
false)
|
|
240
253
|
return rule.fields.some(
|
|
241
254
|
(pattern) =>
|
|
242
255
|
matchesField(pattern, field) || (open && matchesInside(pattern, field)),
|
package/src/internal/search.ts
CHANGED
|
@@ -17,6 +17,7 @@ import * as Algebra from "./algebra.ts"
|
|
|
17
17
|
import * as Catalog from "./catalog.ts"
|
|
18
18
|
import * as Limits from "./limits.ts"
|
|
19
19
|
import * as Nodes from "./nodes.ts"
|
|
20
|
+
import * as Trampoline from "./trampoline.ts"
|
|
20
21
|
|
|
21
22
|
type Node = Nodes.Node
|
|
22
23
|
|
|
@@ -279,51 +280,65 @@ const testFixed = (
|
|
|
279
280
|
}
|
|
280
281
|
|
|
281
282
|
/** Puts one value into a field, and into the fields below it. */
|
|
282
|
-
const fix = (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
283
|
+
const fix = (root: Node, field: string, value: unknown, search: Search): Node =>
|
|
284
|
+
// The walks of a condition take the steps of a trampoline, so a deep
|
|
285
|
+
// condition does not fill the stack.
|
|
286
|
+
Trampoline.run(root.depth, (R) => {
|
|
287
|
+
const go = (node: Node): Trampoline.Step<Node> => {
|
|
288
|
+
const children = (nodes: ReadonlyArray<Node>) =>
|
|
289
|
+
R.forEach(nodes, (child) => R.suspend(() => go(child)))
|
|
290
|
+
switch (node._tag) {
|
|
291
|
+
case "Compare":
|
|
292
|
+
case "In":
|
|
293
|
+
case "Exists": {
|
|
294
|
+
const known = fixedValue(node.field, field, value)
|
|
295
|
+
return R.succeed(
|
|
296
|
+
known === free
|
|
297
|
+
? node
|
|
298
|
+
: testFixed(node, known, search)
|
|
299
|
+
? Nodes.always
|
|
300
|
+
: Nodes.never,
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
case "And":
|
|
304
|
+
return R.map(children(node.conditions), Algebra.and)
|
|
305
|
+
case "Or":
|
|
306
|
+
return R.map(children(node.conditions), Algebra.or)
|
|
307
|
+
case "Not":
|
|
308
|
+
return R.map(
|
|
309
|
+
R.suspend(() => go(node.condition)),
|
|
310
|
+
Algebra.not,
|
|
311
|
+
)
|
|
312
|
+
default:
|
|
313
|
+
return R.succeed(node)
|
|
314
|
+
}
|
|
299
315
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
case "Or":
|
|
303
|
-
return Algebra.or(Arr.map(node.conditions, fixChild))
|
|
304
|
-
case "Not":
|
|
305
|
-
return node.condition.pipe(fixChild, Algebra.not)
|
|
306
|
-
default:
|
|
307
|
-
return node
|
|
308
|
-
}
|
|
309
|
-
}
|
|
316
|
+
return go(root)
|
|
317
|
+
})
|
|
310
318
|
|
|
311
319
|
/** The fields of the tests of a condition, once for each test. */
|
|
312
|
-
const testedFields = (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
320
|
+
const testedFields = (root: Node): ReadonlyArray<string> =>
|
|
321
|
+
Trampoline.run(root.depth, (R) => {
|
|
322
|
+
const go = (node: Node): Trampoline.Step<ReadonlyArray<string>> => {
|
|
323
|
+
switch (node._tag) {
|
|
324
|
+
case "Compare":
|
|
325
|
+
case "In":
|
|
326
|
+
case "Exists":
|
|
327
|
+
return R.succeed([node.field])
|
|
328
|
+
case "And":
|
|
329
|
+
case "Or":
|
|
330
|
+
return R.map(
|
|
331
|
+
R.forEach(node.conditions, (child) => R.suspend(() => go(child))),
|
|
332
|
+
Arr.flatten,
|
|
333
|
+
)
|
|
334
|
+
case "Not":
|
|
335
|
+
return R.suspend(() => go(node.condition))
|
|
336
|
+
default:
|
|
337
|
+
return R.succeed([])
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return go(root)
|
|
341
|
+
})
|
|
327
342
|
|
|
328
343
|
/**
|
|
329
344
|
* Picks the next field to split on. A field comes before the fields below it,
|
|
@@ -353,59 +368,66 @@ const nextField = (node: Node): string => {
|
|
|
353
368
|
* one field at a time, and tries one value from each cell of the field. Each
|
|
354
369
|
* step reads the whole condition, so the budget counts its size.
|
|
355
370
|
*/
|
|
356
|
-
const searchNode = (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
): Found => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
371
|
+
const searchNode = (search: Search) => (R: Trampoline.Runner) => {
|
|
372
|
+
const go = (
|
|
373
|
+
node: Node,
|
|
374
|
+
state: SearchState,
|
|
375
|
+
assignment: ReadonlyArray<readonly [string, unknown]>,
|
|
376
|
+
): Trampoline.Step<Found> => {
|
|
377
|
+
if (node._tag === "True") {
|
|
378
|
+
return R.succeed({ assignment: Option.some(assignment), state })
|
|
379
|
+
}
|
|
380
|
+
if (node._tag === "False") {
|
|
381
|
+
return R.succeed({ assignment: Option.none(), state })
|
|
382
|
+
}
|
|
383
|
+
// The key reads at most a small part of the node: a node with very
|
|
384
|
+
// many nodes, or with very many values in its `In` tests, is its own
|
|
385
|
+
// key. A step reads the nodes but not the values, which `membersOf`
|
|
386
|
+
// keeps, so the budget counts the size.
|
|
387
|
+
const key = Algebra.keyOf(node)
|
|
388
|
+
if (search.failed.has(key)) {
|
|
389
|
+
return R.succeed({ assignment: Option.none(), state })
|
|
390
|
+
}
|
|
391
|
+
if (state.steps + node.size > search.budget) {
|
|
392
|
+
throw new OutOfBudget()
|
|
393
|
+
}
|
|
394
|
+
const field = nextField(node)
|
|
395
|
+
// The cells are tried in order, until one gives values. A fold over
|
|
396
|
+
// the cells does no work after a found answer, and it does not grow
|
|
397
|
+
// the stack with the number of cells. Each call for the next field
|
|
398
|
+
// is a step of the runner, so very many fields do not fill the stack.
|
|
399
|
+
return R.map(
|
|
400
|
+
R.reduce(
|
|
401
|
+
search.cells(field),
|
|
402
|
+
{
|
|
403
|
+
assignment: Option.none(),
|
|
404
|
+
state: { ...state, steps: state.steps + node.size },
|
|
405
|
+
} as Found,
|
|
406
|
+
(before, cell): Trampoline.Step<Found> => {
|
|
407
|
+
if (Option.isSome(before.assignment)) {
|
|
408
|
+
return R.succeed(before)
|
|
409
|
+
}
|
|
410
|
+
const steps = before.state.steps + node.size
|
|
411
|
+
if (steps > search.budget) {
|
|
412
|
+
throw new OutOfBudget()
|
|
413
|
+
}
|
|
414
|
+
return R.suspend(() =>
|
|
415
|
+
go(fix(node, field, cell, search), { ...before.state, steps }, [
|
|
416
|
+
...assignment,
|
|
417
|
+
[field, cell],
|
|
418
|
+
]),
|
|
419
|
+
)
|
|
420
|
+
},
|
|
421
|
+
),
|
|
422
|
+
(result) => {
|
|
423
|
+
if (Option.isNone(result.assignment)) {
|
|
424
|
+
search.failed.add(key)
|
|
425
|
+
}
|
|
426
|
+
return result
|
|
427
|
+
},
|
|
428
|
+
)
|
|
407
429
|
}
|
|
408
|
-
return
|
|
430
|
+
return go
|
|
409
431
|
}
|
|
410
432
|
|
|
411
433
|
/**
|
|
@@ -413,28 +435,40 @@ const searchNode = (
|
|
|
413
435
|
* parts (`opaque`) becomes an empty object, and it keeps an object that is
|
|
414
436
|
* there.
|
|
415
437
|
*/
|
|
438
|
+
type Fields = Readonly<Record<string, unknown>>
|
|
439
|
+
|
|
416
440
|
const setPath = (
|
|
417
|
-
root:
|
|
441
|
+
root: Fields,
|
|
418
442
|
names: ReadonlyArray<string>,
|
|
419
443
|
value: unknown,
|
|
420
|
-
):
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
444
|
+
): Fields => {
|
|
445
|
+
const last = names[names.length - 1]
|
|
446
|
+
if (last === undefined) {
|
|
423
447
|
return root
|
|
424
448
|
}
|
|
425
449
|
// Only an own field counts, as a condition reads it. An inherited one,
|
|
426
450
|
// such as `__proto__`, is not a field of the example.
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
isObject(inner) && inner !== opaque
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
451
|
+
const own = (base: Fields, name: string): unknown =>
|
|
452
|
+
Object.hasOwn(base, name) ? base[name] : undefined
|
|
453
|
+
// The object at each part of the path, from the root down to the object
|
|
454
|
+
// that gets the value. A part that is not an object becomes empty. The
|
|
455
|
+
// path is walked in a loop, so a long path does not fill the stack.
|
|
456
|
+
const bases = Arr.scan(names.slice(0, -1), root, (base, name): Fields => {
|
|
457
|
+
const inner = own(base, name)
|
|
458
|
+
return isObject(inner) && inner !== opaque ? (inner as Fields) : {}
|
|
459
|
+
})
|
|
460
|
+
const target = bases[bases.length - 1]!
|
|
461
|
+
const inner = own(target, last)
|
|
462
|
+
const placed: Fields =
|
|
463
|
+
value === opaque && isObject(inner)
|
|
464
|
+
? target
|
|
465
|
+
: { ...target, [last]: value === opaque ? {} : value }
|
|
466
|
+
// Each object above gets the new object below it, from the bottom up.
|
|
467
|
+
return Arr.reduceRight(
|
|
468
|
+
names.slice(0, -1),
|
|
469
|
+
placed,
|
|
470
|
+
(below, name, index): Fields => ({ ...bases[index]!, [name]: below }),
|
|
471
|
+
)
|
|
438
472
|
}
|
|
439
473
|
|
|
440
474
|
const exampleOf = (
|
|
@@ -452,31 +486,42 @@ const exampleOf = (
|
|
|
452
486
|
* belongs to one search.
|
|
453
487
|
*/
|
|
454
488
|
const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
489
|
+
type Pair = readonly [string, ReadonlyArray<Nodes.Scalar>]
|
|
490
|
+
// The walk takes the steps of a trampoline, so a deep condition does not
|
|
491
|
+
// fill the stack.
|
|
492
|
+
const pairs = Trampoline.run(node.depth, (R) => {
|
|
493
|
+
const go = (current: Node): Trampoline.Step<ReadonlyArray<Pair>> => {
|
|
494
|
+
switch (current._tag) {
|
|
495
|
+
case "Compare":
|
|
496
|
+
return R.succeed([[current.field, [literalOf(current.value)]]])
|
|
497
|
+
case "In":
|
|
498
|
+
return R.succeed([
|
|
499
|
+
[current.field, Arr.map(current.values, literalOf)],
|
|
500
|
+
])
|
|
501
|
+
case "Exists":
|
|
502
|
+
return R.succeed([[current.field, []]])
|
|
503
|
+
case "Under":
|
|
504
|
+
throw new Error(
|
|
505
|
+
"Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.",
|
|
506
|
+
)
|
|
507
|
+
case "And":
|
|
508
|
+
case "Or":
|
|
509
|
+
return R.map(
|
|
510
|
+
R.forEach(current.conditions, (child) =>
|
|
511
|
+
R.suspend(() => go(child)),
|
|
512
|
+
),
|
|
513
|
+
Arr.flatten,
|
|
514
|
+
)
|
|
515
|
+
case "Not":
|
|
516
|
+
return R.suspend(() => go(current.condition))
|
|
517
|
+
default:
|
|
518
|
+
return R.succeed([])
|
|
519
|
+
}
|
|
476
520
|
}
|
|
477
|
-
|
|
521
|
+
return go(node)
|
|
522
|
+
})
|
|
478
523
|
const all = new Map<string, Set<Nodes.Scalar>>()
|
|
479
|
-
Arr.forEach(pairs
|
|
524
|
+
Arr.forEach(pairs, ([field, values]) => {
|
|
480
525
|
const known = all.get(field) ?? new Set<Nodes.Scalar>()
|
|
481
526
|
Arr.forEach(values, (value) => known.add(value))
|
|
482
527
|
all.set(field, known)
|
|
@@ -486,9 +531,9 @@ const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
|
|
|
486
531
|
|
|
487
532
|
/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */
|
|
488
533
|
export const kindsIn =
|
|
489
|
-
(catalog: Catalog.Catalog | undefined) =>
|
|
534
|
+
(catalog: Catalog.Catalog | undefined, parts?: number) =>
|
|
490
535
|
(field: string): Catalog.Kinds =>
|
|
491
|
-
(catalog === undefined ? undefined : Catalog.lookup(catalog, field))
|
|
536
|
+
(catalog === undefined ? undefined : Catalog.lookup(catalog, field, parts))
|
|
492
537
|
?.kinds ?? Catalog.anyKinds
|
|
493
538
|
|
|
494
539
|
/**
|
|
@@ -500,13 +545,14 @@ export const satisfiable = (
|
|
|
500
545
|
node: Node,
|
|
501
546
|
kindsOf: (field: string) => Catalog.Kinds,
|
|
502
547
|
budget: number = Limits.searchSteps,
|
|
548
|
+
fields: number = Limits.searchFields,
|
|
503
549
|
): Satisfiability => {
|
|
504
550
|
if (node.opaque) {
|
|
505
551
|
return satisfiability.Unknown({ reason: "predicate" })
|
|
506
552
|
}
|
|
507
553
|
const constants = constantsOf(node)
|
|
508
554
|
// The search calls itself one time for each field that it fixes.
|
|
509
|
-
if (constants.size >
|
|
555
|
+
if (constants.size > fields) {
|
|
510
556
|
return satisfiability.Unknown({ reason: "budget" })
|
|
511
557
|
}
|
|
512
558
|
// The cells of each field, in a native `Map` as the constants are.
|
|
@@ -532,7 +578,11 @@ export const satisfiable = (
|
|
|
532
578
|
budget,
|
|
533
579
|
}
|
|
534
580
|
try {
|
|
535
|
-
|
|
581
|
+
// The search goes one call deeper for each field, so the runner is the
|
|
582
|
+
// one for that many levels.
|
|
583
|
+
const found = Trampoline.run(constants.size, (R) =>
|
|
584
|
+
searchNode(search)(R)(Algebra.simplify(node), { steps: 0 }, []),
|
|
585
|
+
)
|
|
536
586
|
return Option.match(found.assignment, {
|
|
537
587
|
onNone: () => satisfiability.Unsatisfiable(),
|
|
538
588
|
onSome: (assignment) =>
|
|
@@ -41,6 +41,15 @@ export interface Runner {
|
|
|
41
41
|
items: ReadonlyArray<A>,
|
|
42
42
|
f: (item: A, index: number) => Step<B>,
|
|
43
43
|
) => Step<ReadonlyArray<B>>
|
|
44
|
+
/**
|
|
45
|
+
* Runs a step for each item, in order, with the value of the step before.
|
|
46
|
+
* It does not grow the stack with the number of items.
|
|
47
|
+
*/
|
|
48
|
+
readonly reduce: <A, B>(
|
|
49
|
+
items: ReadonlyArray<A>,
|
|
50
|
+
initial: B,
|
|
51
|
+
f: (before: B, item: A) => Step<B>,
|
|
52
|
+
) => Step<B>
|
|
44
53
|
/** Runs a step for each item, in order, up to the first `true`. */
|
|
45
54
|
readonly some: <A>(
|
|
46
55
|
items: ReadonlyArray<A>,
|
|
@@ -58,15 +67,18 @@ const value = <A>(step: Step<A>): A => step as unknown as A
|
|
|
58
67
|
const step = <A>(value: A): Step<A> => value as unknown as Step<A>
|
|
59
68
|
|
|
60
69
|
/** Runs each step at once, on the stack. */
|
|
70
|
+
// A step of `direct` is its value, so a function that gives a step gives a
|
|
71
|
+
// value too. The runner passes such a function on with no wrap: a wrap for
|
|
72
|
+
// each item would be a call more for each node of a walk.
|
|
61
73
|
export const direct: Runner = {
|
|
62
74
|
succeed: step,
|
|
63
75
|
suspend: (make) => make(),
|
|
64
76
|
map: (from, f) => step(f(value(from))),
|
|
65
77
|
flatMap: (from, f) => f(value(from)),
|
|
66
|
-
forEach: (items, f) =>
|
|
67
|
-
|
|
68
|
-
some: (items, f) => step(Arr.some(items,
|
|
69
|
-
every: (items, f) => step(Arr.every(items,
|
|
78
|
+
forEach: (items, f) => step(Arr.map(items, f as never)),
|
|
79
|
+
reduce: (items, initial, f) => step(Arr.reduce(items, initial, f as never)),
|
|
80
|
+
some: (items, f) => step(Arr.some(items, f as never)),
|
|
81
|
+
every: (items, f) => step(Arr.every(items, f as never)),
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
// A step of `loop` is an Effect with no error and no services. A failure of a
|
|
@@ -84,6 +96,14 @@ export const loop: Runner = {
|
|
|
84
96
|
fromEffect(Effect.flatMap(effect(from), (value) => effect(f(value)))),
|
|
85
97
|
forEach: (items, f) =>
|
|
86
98
|
fromEffect(Effect.forEach(items, (item, index) => effect(f(item, index)))),
|
|
99
|
+
reduce: (items, initial, f) =>
|
|
100
|
+
fromEffect(
|
|
101
|
+
Effect.reduce(
|
|
102
|
+
items,
|
|
103
|
+
() => initial,
|
|
104
|
+
(before, item) => effect(f(before, item)),
|
|
105
|
+
),
|
|
106
|
+
),
|
|
87
107
|
some: (items, f) =>
|
|
88
108
|
fromEffect(
|
|
89
109
|
Effect.map(
|
|
@@ -140,6 +160,33 @@ export const runWith = <A>(
|
|
|
140
160
|
})
|
|
141
161
|
}
|
|
142
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Makes a walk of an input with an unknown depth, as the input of a decoder.
|
|
165
|
+
* It runs with `direct` while the input is low. A part more than
|
|
166
|
+
* `directDepth` levels down runs with `loop`, and the walk stays in the loop
|
|
167
|
+
* below it, so it goes into the loop one time on each path. `walk` gets the
|
|
168
|
+
* runner, and the call for a child at the next level: `child(item, depth)`.
|
|
169
|
+
*/
|
|
170
|
+
export const adaptive = <I, A>(
|
|
171
|
+
walk: (
|
|
172
|
+
R: Runner,
|
|
173
|
+
child: (item: I, depth: number) => Step<A>,
|
|
174
|
+
) => (item: I, depth: number) => Step<A>,
|
|
175
|
+
): ((item: I) => A) => {
|
|
176
|
+
const inLoop: (item: I, depth: number) => Step<A> = walk(
|
|
177
|
+
loop,
|
|
178
|
+
(item, depth) => loop.suspend(() => inLoop(item, depth)),
|
|
179
|
+
)
|
|
180
|
+
const onStack: (item: I, depth: number) => Step<A> = walk(
|
|
181
|
+
direct,
|
|
182
|
+
(item, depth) =>
|
|
183
|
+
depth > directDepth
|
|
184
|
+
? step(runWith(loop, () => inLoop(item, depth)))
|
|
185
|
+
: onStack(item, depth),
|
|
186
|
+
)
|
|
187
|
+
return (item) => value(onStack(item, 1))
|
|
188
|
+
}
|
|
189
|
+
|
|
143
190
|
/**
|
|
144
191
|
* Gives the Effect of a step of `loop`, so a walk can go on in an Effect
|
|
145
192
|
* that runs a predicate.
|