@nmnmcc/ability 2.0.2 → 2.0.5
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 +20 -14
- package/dist/Condition.d.ts +18 -8
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +2 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Limits.d.ts +3 -1
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +3 -1
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +2 -6
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js.map +1 -1
- package/dist/internal/algebra.js +84 -21
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/analysis.js +4 -2
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/bind.js +36 -23
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +9 -0
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +38 -23
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +2 -1
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/place.js +19 -0
- package/dist/internal/place.js.map +1 -0
- package/dist/internal/policy-json.js +39 -39
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/rule.d.ts +20 -22
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +32 -4
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +51 -34
- package/dist/internal/search.js.map +1 -1
- package/package.json +1 -1
- package/src/Condition.ts +46 -25
- package/src/Limits.ts +3 -1
- package/src/Policy.ts +1 -7
- package/src/internal/algebra.ts +110 -32
- package/src/internal/analysis.ts +5 -0
- package/src/internal/bind.ts +88 -32
- package/src/internal/catalog.ts +78 -30
- package/src/internal/decide.ts +3 -0
- package/src/internal/place.ts +32 -0
- package/src/internal/policy-json.ts +49 -29
- package/src/internal/rule.ts +59 -11
- package/src/internal/search.ts +78 -75
package/src/internal/decide.ts
CHANGED
|
@@ -37,6 +37,8 @@ interface DecideContext {
|
|
|
37
37
|
readonly input: Condition.Input
|
|
38
38
|
/** The resource of the kind, for the fields that a rule may grant. */
|
|
39
39
|
readonly descriptor: Resource.Descriptor
|
|
40
|
+
/** The test of `RuleData.fitsOf` for the catalog, for this decision. */
|
|
41
|
+
readonly fits: (pattern: string) => boolean
|
|
40
42
|
/** The rules that were tested, for {@link explain}. */
|
|
41
43
|
readonly trace: Ref.Ref<Chunk.Chunk<Step>> | undefined
|
|
42
44
|
}
|
|
@@ -282,6 +284,7 @@ const makeContext = (
|
|
|
282
284
|
path: descriptor.path,
|
|
283
285
|
},
|
|
284
286
|
descriptor,
|
|
287
|
+
fits: RuleData.fitsOf(descriptor.catalog),
|
|
285
288
|
trace,
|
|
286
289
|
}
|
|
287
290
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The place of a part of an input, as a path that shares the path of its
|
|
3
|
+
* parent. A deep tree with very many rules would need very much memory for a
|
|
4
|
+
* full copy of the path of each rule, so a place keeps only its own parts and
|
|
5
|
+
* its parent. The full path is made only for a problem, in one walk up.
|
|
6
|
+
*/
|
|
7
|
+
import * as Arr from "effect/Array"
|
|
8
|
+
import * as Option from "effect/Option"
|
|
9
|
+
|
|
10
|
+
/** A place: its own parts of the path, after the path of its parent. */
|
|
11
|
+
export interface Place {
|
|
12
|
+
readonly parent: Place | undefined
|
|
13
|
+
readonly parts: ReadonlyArray<PropertyKey>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The place below `parent` with more parts. */
|
|
17
|
+
export const at = (
|
|
18
|
+
parent: Place | undefined,
|
|
19
|
+
...parts: ReadonlyArray<PropertyKey>
|
|
20
|
+
): Place => ({ parent, parts })
|
|
21
|
+
|
|
22
|
+
/** The full path of a place. The root, with no place, has the empty path. */
|
|
23
|
+
export const pathOf = (place: Place | undefined): ReadonlyArray<PropertyKey> =>
|
|
24
|
+
Arr.flatten(
|
|
25
|
+
Arr.reverse(
|
|
26
|
+
Arr.unfold(place, (current) =>
|
|
27
|
+
current === undefined
|
|
28
|
+
? Option.none()
|
|
29
|
+
: Option.some([current.parts, current.parent] as const),
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
)
|
|
@@ -12,6 +12,7 @@ import type * as Domain from "../Domain.ts"
|
|
|
12
12
|
import * as Rule from "./rule.ts"
|
|
13
13
|
import * as Json from "./json.ts"
|
|
14
14
|
import * as Limits from "./limits.ts"
|
|
15
|
+
import * as Place from "./place.ts"
|
|
15
16
|
import * as PolicyNodes from "./policy-nodes.ts"
|
|
16
17
|
|
|
17
18
|
type Node = PolicyNodes.Node
|
|
@@ -213,10 +214,13 @@ const spendValue = (
|
|
|
213
214
|
)
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* A rule in the input, with its place, to decode after the tree. The place
|
|
219
|
+
* shares the path of its parent: see `Place`.
|
|
220
|
+
*/
|
|
217
221
|
interface RuleInput {
|
|
218
222
|
readonly input: unknown
|
|
219
|
-
readonly
|
|
223
|
+
readonly place: Place.Place
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
/**
|
|
@@ -227,20 +231,20 @@ interface RuleInput {
|
|
|
227
231
|
type Plan =
|
|
228
232
|
| {
|
|
229
233
|
readonly _tag: "Rules"
|
|
230
|
-
readonly
|
|
234
|
+
readonly place: Place.Place | undefined
|
|
231
235
|
readonly size: number
|
|
232
236
|
readonly algorithm: PolicyNodes.Algorithm
|
|
233
237
|
readonly rules: ReadonlyArray<number>
|
|
234
238
|
}
|
|
235
239
|
| {
|
|
236
240
|
readonly _tag: "All" | "Any" | "FirstOf"
|
|
237
|
-
readonly
|
|
241
|
+
readonly place: Place.Place | undefined
|
|
238
242
|
readonly size: number
|
|
239
243
|
readonly policies: ReadonlyArray<Plan>
|
|
240
244
|
}
|
|
241
245
|
| {
|
|
242
246
|
readonly _tag: "Ceiling"
|
|
243
|
-
readonly
|
|
247
|
+
readonly place: Place.Place | undefined
|
|
244
248
|
readonly size: number
|
|
245
249
|
readonly cap: Plan
|
|
246
250
|
readonly policy: Plan
|
|
@@ -252,11 +256,13 @@ type Plan =
|
|
|
252
256
|
* the value, so its size counts in the budget.
|
|
253
257
|
*/
|
|
254
258
|
const decodeMeta = (
|
|
255
|
-
|
|
259
|
+
rule: RuleInput,
|
|
256
260
|
domain: Domain.Any,
|
|
257
261
|
budget: Json.Budget,
|
|
258
|
-
): Effect.Effect<unknown, Json.Problem> =>
|
|
259
|
-
|
|
262
|
+
): Effect.Effect<unknown, Json.Problem> => {
|
|
263
|
+
const { input } = rule
|
|
264
|
+
const path = Place.pathOf(rule.place)
|
|
265
|
+
return Effect.flatMap(
|
|
260
266
|
attempt(() =>
|
|
261
267
|
typeof input !== "object" || input === null
|
|
262
268
|
? undefined
|
|
@@ -285,13 +291,16 @@ const decodeMeta = (
|
|
|
285
291
|
}),
|
|
286
292
|
),
|
|
287
293
|
)
|
|
294
|
+
}
|
|
288
295
|
|
|
289
296
|
const decodeRule = (
|
|
290
|
-
|
|
297
|
+
rule: RuleInput,
|
|
291
298
|
meta: unknown,
|
|
292
299
|
budget: Json.Budget,
|
|
293
300
|
domain: Domain.Any,
|
|
294
301
|
): Rule.Rule => {
|
|
302
|
+
const { input } = rule
|
|
303
|
+
const path = Place.pathOf(rule.place)
|
|
295
304
|
if (typeof input !== "object" || input === null) {
|
|
296
305
|
throw Json.problem(path, "A rule must be an object")
|
|
297
306
|
}
|
|
@@ -330,11 +339,13 @@ const decodeRule = (
|
|
|
330
339
|
/**
|
|
331
340
|
* Reads the shape of a tree, and puts each rule in `rules`. The rules are
|
|
332
341
|
* decoded after the walk: a condition has levels of its own, and a decode
|
|
333
|
-
* inside the walk would go down the levels of both.
|
|
342
|
+
* inside the walk would go down the levels of both. `path` is the path of the
|
|
343
|
+
* node for a problem here, and `place` keeps it for later.
|
|
334
344
|
*/
|
|
335
345
|
const readTree = (
|
|
336
346
|
input: unknown,
|
|
337
347
|
path: ReadonlyArray<PropertyKey>,
|
|
348
|
+
place: Place.Place | undefined,
|
|
338
349
|
depth: number,
|
|
339
350
|
budget: Json.Budget,
|
|
340
351
|
// The rules of one decode. The list belongs to that decode.
|
|
@@ -350,8 +361,15 @@ const readTree = (
|
|
|
350
361
|
throw Json.problem(path, "A node of a policy must be an object")
|
|
351
362
|
}
|
|
352
363
|
Json.spend(budget, 1, path)
|
|
353
|
-
const child = (value: unknown,
|
|
354
|
-
readTree(
|
|
364
|
+
const child = (value: unknown, ...parts: ReadonlyArray<PropertyKey>) =>
|
|
365
|
+
readTree(
|
|
366
|
+
value,
|
|
367
|
+
[...path, ...parts],
|
|
368
|
+
Place.at(place, ...parts),
|
|
369
|
+
depth + 1,
|
|
370
|
+
budget,
|
|
371
|
+
rules,
|
|
372
|
+
)
|
|
355
373
|
const tag = Json.read(input, "_tag", path)
|
|
356
374
|
switch (tag) {
|
|
357
375
|
case "Rules": {
|
|
@@ -365,11 +383,14 @@ const readTree = (
|
|
|
365
383
|
const found = Arr.map(
|
|
366
384
|
Json.readList(input, "rules", path, budget),
|
|
367
385
|
(rule, index) =>
|
|
368
|
-
rules.push({
|
|
386
|
+
rules.push({
|
|
387
|
+
input: rule,
|
|
388
|
+
place: Place.at(place, "rules", index),
|
|
389
|
+
}) - 1,
|
|
369
390
|
)
|
|
370
|
-
return sized({
|
|
391
|
+
return sized(path, {
|
|
371
392
|
_tag: "Rules",
|
|
372
|
-
|
|
393
|
+
place,
|
|
373
394
|
size: 1 + found.length,
|
|
374
395
|
algorithm: algorithm as PolicyNodes.Algorithm,
|
|
375
396
|
rules: found,
|
|
@@ -380,24 +401,21 @@ const readTree = (
|
|
|
380
401
|
case "FirstOf": {
|
|
381
402
|
const policies = Arr.map(
|
|
382
403
|
Json.readList(input, "policies", path, budget),
|
|
383
|
-
(value, index) => child(value,
|
|
404
|
+
(value, index) => child(value, "policies", index),
|
|
384
405
|
)
|
|
385
|
-
return sized({
|
|
406
|
+
return sized(path, {
|
|
386
407
|
_tag: tag,
|
|
387
|
-
|
|
408
|
+
place,
|
|
388
409
|
size: Arr.reduce(policies, 1, (total, plan) => total + plan.size),
|
|
389
410
|
policies,
|
|
390
411
|
})
|
|
391
412
|
}
|
|
392
413
|
case "Ceiling": {
|
|
393
|
-
const cap = child(Json.read(input, "cap", path),
|
|
394
|
-
const policy = child(Json.read(input, "policy", path),
|
|
395
|
-
|
|
396
|
-
"policy",
|
|
397
|
-
])
|
|
398
|
-
return sized({
|
|
414
|
+
const cap = child(Json.read(input, "cap", path), "cap")
|
|
415
|
+
const policy = child(Json.read(input, "policy", path), "policy")
|
|
416
|
+
return sized(path, {
|
|
399
417
|
_tag: "Ceiling",
|
|
400
|
-
|
|
418
|
+
place,
|
|
401
419
|
size: 1 + cap.size + policy.size,
|
|
402
420
|
cap,
|
|
403
421
|
policy,
|
|
@@ -409,10 +427,10 @@ const readTree = (
|
|
|
409
427
|
}
|
|
410
428
|
|
|
411
429
|
/** Stops a plan past `policyNodes`, with the message of the nodes. */
|
|
412
|
-
const sized = (plan: Plan): Plan => {
|
|
430
|
+
const sized = (path: ReadonlyArray<PropertyKey>, plan: Plan): Plan => {
|
|
413
431
|
if (plan.size > Limits.policyNodes) {
|
|
414
432
|
throw Json.problem(
|
|
415
|
-
|
|
433
|
+
path,
|
|
416
434
|
`A policy tree has ${String(plan.size)} nodes. The most is ${String(Limits.policyNodes)}.`,
|
|
417
435
|
)
|
|
418
436
|
}
|
|
@@ -443,7 +461,7 @@ const build = (plan: Plan, rules: ReadonlyArray<Rule.Rule>): Node => {
|
|
|
443
461
|
} catch (error) {
|
|
444
462
|
// A node past a limit is a problem at its place.
|
|
445
463
|
if (error instanceof Limits.LimitExceeded) {
|
|
446
|
-
throw Json.problem(plan.
|
|
464
|
+
throw Json.problem(Place.pathOf(plan.place), error.message)
|
|
447
465
|
}
|
|
448
466
|
throw error
|
|
449
467
|
}
|
|
@@ -474,7 +492,9 @@ export const decode = (
|
|
|
474
492
|
const budget: Json.Budget = { used: 0 }
|
|
475
493
|
// The rules of this decode. The list belongs to this decode.
|
|
476
494
|
const inputs: Array<RuleInput> = []
|
|
477
|
-
const plan = yield* attempt(() =>
|
|
495
|
+
const plan = yield* attempt(() =>
|
|
496
|
+
readTree(input, [], undefined, 1, budget, inputs),
|
|
497
|
+
)
|
|
478
498
|
const metas = yield* Effect.forEach(inputs, (rule) =>
|
|
479
499
|
decodeMeta(rule, domain, budget),
|
|
480
500
|
)
|
package/src/internal/rule.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as Arr from "effect/Array"
|
|
|
9
9
|
import * as Data from "effect/Data"
|
|
10
10
|
import * as Condition from "../Condition.ts"
|
|
11
11
|
import type * as Domain from "../Domain.ts"
|
|
12
|
+
import type * as Policy from "../Policy.ts"
|
|
12
13
|
import type * as Resource from "../Resource.ts"
|
|
13
14
|
import * as Catalog from "./catalog.ts"
|
|
14
15
|
import * as Nodes from "./nodes.ts"
|
|
@@ -33,17 +34,42 @@ export interface Shape {
|
|
|
33
34
|
readonly domain?: Domain.Any
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The class of every rule value. The types are those of `Policy.Rule`: `D` is
|
|
39
|
+
* the domain, `I` is what the tests of the condition read, and `X` is the
|
|
40
|
+
* mark of the condition. With no types, as in the nodes of a policy, a rule
|
|
41
|
+
* fits no domain, its condition fits no other rule, and it may have a
|
|
42
|
+
* predicate.
|
|
43
|
+
*/
|
|
44
|
+
export class Rule<D = never, I = never, X = unknown> extends Data.Class<
|
|
45
|
+
Policy.RuleShape<I, X>
|
|
46
|
+
> {
|
|
47
|
+
/** Only in the types. */
|
|
48
|
+
declare readonly "~Opaque": () => X
|
|
49
|
+
/** Only in the types. */
|
|
50
|
+
declare readonly "~Domain": (domain: D) => D
|
|
51
|
+
// A private field makes the type of a rule its own, as for a policy: a
|
|
52
|
+
// spread of a rule, or an object made by hand, is not a rule. `Equal` does
|
|
53
|
+
// not read it. It comes last, so its `;` stays: the generator method below
|
|
54
|
+
// needs one after a field. Node.js takes the `declare` fields out of the
|
|
55
|
+
// code, and a `;` after one of them would go with it.
|
|
56
|
+
readonly #rule = true;
|
|
57
|
+
|
|
58
|
+
*[Symbol.iterator](): Generator<Rule<D, I, X>, Rule<D, I, X>, unknown> {
|
|
39
59
|
yield this
|
|
40
60
|
return this
|
|
41
61
|
}
|
|
62
|
+
|
|
63
|
+
/** Tests if a value is a rule, by its private field. */
|
|
64
|
+
static is(u: unknown): u is Rule {
|
|
65
|
+
return typeof u === "object" && u !== null && #rule in u
|
|
66
|
+
}
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
/** Makes a rule from its data. It does not check the data against a domain. */
|
|
45
70
|
export const make = (data: Shape): Rule =>
|
|
46
71
|
// A missing option is left out, so two rules with the same data are equal.
|
|
72
|
+
// The data has no types here, as the code below works with no types.
|
|
47
73
|
new Rule({
|
|
48
74
|
_tag: data._tag,
|
|
49
75
|
actions: [...data.actions],
|
|
@@ -54,7 +80,7 @@ export const make = (data: Shape): Rule =>
|
|
|
54
80
|
...(data.id === undefined ? {} : { id: data.id }),
|
|
55
81
|
...(data.meta === undefined ? {} : { meta: data.meta }),
|
|
56
82
|
...(data.domain === undefined ? {} : { domain: data.domain }),
|
|
57
|
-
})
|
|
83
|
+
} as never)
|
|
58
84
|
|
|
59
85
|
/**
|
|
60
86
|
* Makes a rule from its data, as `Policy.rule` does. A condition with a
|
|
@@ -71,7 +97,7 @@ export const makePlain = (data: Shape): Rule => {
|
|
|
71
97
|
}
|
|
72
98
|
|
|
73
99
|
/** Tests if a value is a rule. */
|
|
74
|
-
export const isRule = (u: unknown): u is Rule => u
|
|
100
|
+
export const isRule = (u: unknown): u is Rule => Rule.is(u)
|
|
75
101
|
|
|
76
102
|
const toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>
|
|
77
103
|
Array.isArray(value) ? value : [value as A]
|
|
@@ -132,6 +158,31 @@ export const matchesField = (pattern: string, field: string): boolean =>
|
|
|
132
158
|
export const matchesInside = (pattern: string, field: string): boolean =>
|
|
133
159
|
Patterns.matchesBelow(Patterns.parseBound(pattern), field.split("."))
|
|
134
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Makes the test "a field pattern matches some field of the catalog" for one
|
|
163
|
+
* walk, such as one decision. A policy may name one pattern very many times,
|
|
164
|
+
* so the test keeps each answer by the text of the pattern, and the memo goes
|
|
165
|
+
* away when the walk ends. The binding puts a limit on the work of the
|
|
166
|
+
* patterns of a policy on each kind, so one walk does at most that work.
|
|
167
|
+
*/
|
|
168
|
+
export const fitsOf = (
|
|
169
|
+
catalog: Catalog.Catalog,
|
|
170
|
+
): ((pattern: string) => boolean) => {
|
|
171
|
+
// A native `Map`: the hash of a string in Effect has no random seed, and a
|
|
172
|
+
// policy could hold very many patterns with one hash. The engine hashes a
|
|
173
|
+
// string with a random seed.
|
|
174
|
+
const memo = new Map<string, boolean>()
|
|
175
|
+
return (pattern) => {
|
|
176
|
+
const known = memo.get(pattern)
|
|
177
|
+
if (known !== undefined) {
|
|
178
|
+
return known
|
|
179
|
+
}
|
|
180
|
+
const found = Patterns.fitsCatalog(Patterns.parseBound(pattern), catalog)
|
|
181
|
+
memo.set(pattern, found)
|
|
182
|
+
return found
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
135
186
|
/**
|
|
136
187
|
* Tests the parts of a rule that do not need the resource value: the action,
|
|
137
188
|
* the kind and the field.
|
|
@@ -152,6 +203,8 @@ export const matchesTarget = (
|
|
|
152
203
|
readonly kind: string
|
|
153
204
|
readonly field?: string | undefined
|
|
154
205
|
readonly descriptor?: Resource.Descriptor | undefined
|
|
206
|
+
/** The test of {@link fitsOf} for the catalog of the descriptor. */
|
|
207
|
+
readonly fits?: ((pattern: string) => boolean) | undefined
|
|
155
208
|
},
|
|
156
209
|
): boolean => {
|
|
157
210
|
if (!rule.actions.includes("*") && !rule.actions.includes(target.action)) {
|
|
@@ -173,12 +226,7 @@ export const matchesTarget = (
|
|
|
173
226
|
return (
|
|
174
227
|
rule._tag === "Allow" &&
|
|
175
228
|
(descriptor === undefined ||
|
|
176
|
-
Arr.some(rule.fields, (
|
|
177
|
-
Patterns.fitsCatalog(
|
|
178
|
-
Patterns.parseBound(pattern),
|
|
179
|
-
descriptor.catalog,
|
|
180
|
-
),
|
|
181
|
-
))
|
|
229
|
+
Arr.some(rule.fields, target.fits ?? fitsOf(descriptor.catalog)))
|
|
182
230
|
)
|
|
183
231
|
}
|
|
184
232
|
const field = target.field
|
package/src/internal/search.ts
CHANGED
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as Arr from "effect/Array"
|
|
13
13
|
import * as Data from "effect/Data"
|
|
14
|
-
import * as HashMap from "effect/HashMap"
|
|
15
|
-
import * as HashSet from "effect/HashSet"
|
|
16
|
-
import * as MutableHashMap from "effect/MutableHashMap"
|
|
17
14
|
import * as Option from "effect/Option"
|
|
18
15
|
import { isObject } from "effect/Predicate"
|
|
19
16
|
import * as Algebra from "./algebra.ts"
|
|
@@ -146,14 +143,24 @@ const cellText = (cell: unknown): string =>
|
|
|
146
143
|
? `number:${Object.is(cell, -0) ? "0" : String(cell)}`
|
|
147
144
|
: `${typeof cell}:${String(cell)}`
|
|
148
145
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Keeps the first cell of each text, in order. The texts come from the
|
|
148
|
+
* values of a policy, so they go into a native `Set`: the hash of a string in
|
|
149
|
+
* Effect has no random seed, and a policy could hold very many strings with
|
|
150
|
+
* one hash. The engine hashes a string with a random seed.
|
|
151
|
+
*/
|
|
152
|
+
const dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> => {
|
|
153
|
+
// The set belongs to this call.
|
|
154
|
+
const seen = new Set<string>()
|
|
155
|
+
return Arr.filter(cells, (cell) => {
|
|
156
|
+
const text = cellText(cell)
|
|
157
|
+
if (seen.has(text)) {
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
seen.add(text)
|
|
161
|
+
return true
|
|
162
|
+
})
|
|
163
|
+
}
|
|
157
164
|
|
|
158
165
|
/** Gives one value from each cell of a field. */
|
|
159
166
|
const cellsOf = (
|
|
@@ -185,16 +192,22 @@ interface Search {
|
|
|
185
192
|
* The texts of the values of each `In` test, so one test of a value does
|
|
186
193
|
* not read every value of the test. It is a native `Map`, because it must
|
|
187
194
|
* find a node by the node itself: a hash of a node reads all its values.
|
|
195
|
+
* The texts are in a native `Set`, as in {@link dedupeCells}.
|
|
188
196
|
*/
|
|
189
|
-
readonly members: Map<Nodes.In,
|
|
197
|
+
readonly members: Map<Nodes.In, Set<string>>
|
|
198
|
+
/**
|
|
199
|
+
* The texts of the structure of the conditions that the search found to be
|
|
200
|
+
* unsatisfiable. A condition with the same structure has the same free
|
|
201
|
+
* fields, so its answer is the same. The set only grows, so the steps of a
|
|
202
|
+
* search may share it. It is a native `Set`, as in {@link dedupeCells}.
|
|
203
|
+
*/
|
|
204
|
+
readonly failed: Set<string>
|
|
190
205
|
readonly budget: number
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
/** What a search has done up to now. Each step gives a new one. */
|
|
194
209
|
interface SearchState {
|
|
195
210
|
readonly steps: number
|
|
196
|
-
/** The conditions that the search found to be unsatisfiable. */
|
|
197
|
-
readonly failed: HashSet.HashSet<Node>
|
|
198
211
|
}
|
|
199
212
|
|
|
200
213
|
interface Found {
|
|
@@ -228,13 +241,13 @@ const fixedValue = (
|
|
|
228
241
|
: null
|
|
229
242
|
: free
|
|
230
243
|
|
|
231
|
-
const membersOf = (node: Nodes.In, search: Search):
|
|
244
|
+
const membersOf = (node: Nodes.In, search: Search): Set<string> => {
|
|
232
245
|
const known = search.members.get(node)
|
|
233
246
|
if (known !== undefined) {
|
|
234
247
|
return known
|
|
235
248
|
}
|
|
236
249
|
// `===` finds no `NaN`, so the set has none, and `NaN` is in no test.
|
|
237
|
-
const members =
|
|
250
|
+
const members = new Set(
|
|
238
251
|
Arr.map(
|
|
239
252
|
Arr.filter(
|
|
240
253
|
Arr.map(node.values, literalOf),
|
|
@@ -257,8 +270,7 @@ const testFixed = (
|
|
|
257
270
|
return Algebra.testCompare(node.op, value, literalOf(node.value))
|
|
258
271
|
case "In":
|
|
259
272
|
return (
|
|
260
|
-
!Number.isNaN(value) &&
|
|
261
|
-
HashSet.has(membersOf(node, search), cellText(value))
|
|
273
|
+
!Number.isNaN(value) && membersOf(node, search).has(cellText(value))
|
|
262
274
|
)
|
|
263
275
|
case "Exists":
|
|
264
276
|
return value !== null
|
|
@@ -318,13 +330,11 @@ const testedFields = (node: Node): ReadonlyArray<string> => {
|
|
|
318
330
|
* and then the first name in order, so the choice is the same each time.
|
|
319
331
|
*/
|
|
320
332
|
const nextField = (node: Node): string => {
|
|
321
|
-
|
|
333
|
+
// A native `Map`, as in `dedupeCells`: a condition that is not bound to a
|
|
334
|
+
// domain may have any field names. The map belongs to this call.
|
|
335
|
+
const counts = new Map<string, number>()
|
|
322
336
|
Arr.forEach(testedFields(node), (field) =>
|
|
323
|
-
|
|
324
|
-
counts,
|
|
325
|
-
field,
|
|
326
|
-
Option.getOrElse(MutableHashMap.get(counts, field), () => 0) + 1,
|
|
327
|
-
),
|
|
337
|
+
counts.set(field, (counts.get(field) ?? 0) + 1),
|
|
328
338
|
)
|
|
329
339
|
const depth = (field: string) => field.split(".").length
|
|
330
340
|
const [first, ...rest] = Array.from(counts)
|
|
@@ -351,7 +361,13 @@ const searchNode = (
|
|
|
351
361
|
if (node._tag === "True") {
|
|
352
362
|
return { assignment: Option.some(assignment), state }
|
|
353
363
|
}
|
|
354
|
-
if (node._tag === "False"
|
|
364
|
+
if (node._tag === "False") {
|
|
365
|
+
return { assignment: Option.none(), state }
|
|
366
|
+
}
|
|
367
|
+
// The text reads all of the node. The step before counted the size of a
|
|
368
|
+
// larger node, so the budget counts this work too.
|
|
369
|
+
const text = Algebra.structureText(node)
|
|
370
|
+
if (search.failed.has(text)) {
|
|
355
371
|
return { assignment: Option.none(), state }
|
|
356
372
|
}
|
|
357
373
|
if (state.steps + node.size > search.budget) {
|
|
@@ -383,17 +399,10 @@ const searchNode = (
|
|
|
383
399
|
)
|
|
384
400
|
},
|
|
385
401
|
)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
state: {
|
|
391
|
-
...result.state,
|
|
392
|
-
// A condition with the same structure has the same free fields,
|
|
393
|
-
// so its answer is the same.
|
|
394
|
-
failed: HashSet.add(result.state.failed, node),
|
|
395
|
-
},
|
|
396
|
-
}
|
|
402
|
+
if (Option.isNone(result.assignment)) {
|
|
403
|
+
search.failed.add(text)
|
|
404
|
+
}
|
|
405
|
+
return result
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
/**
|
|
@@ -434,10 +443,12 @@ const exampleOf = (
|
|
|
434
443
|
(root, [field, value]) => setPath(root, field.split("."), value),
|
|
435
444
|
)
|
|
436
445
|
|
|
437
|
-
/**
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
446
|
+
/**
|
|
447
|
+
* The constants that the tests of each field use. They come from a policy, so
|
|
448
|
+
* they go into a native `Map` and `Set`, as in {@link dedupeCells}. The map
|
|
449
|
+
* belongs to one search.
|
|
450
|
+
*/
|
|
451
|
+
const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
|
|
441
452
|
const pairs = (
|
|
442
453
|
current: Node,
|
|
443
454
|
): ReadonlyArray<readonly [string, ReadonlyArray<Nodes.Scalar>]> => {
|
|
@@ -461,16 +472,13 @@ const constantsOf = (
|
|
|
461
472
|
return []
|
|
462
473
|
}
|
|
463
474
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
),
|
|
472
|
-
),
|
|
473
|
-
)
|
|
475
|
+
const all = new Map<string, Set<Nodes.Scalar>>()
|
|
476
|
+
Arr.forEach(pairs(node), ([field, values]) => {
|
|
477
|
+
const known = all.get(field) ?? new Set<Nodes.Scalar>()
|
|
478
|
+
Arr.forEach(values, (value) => known.add(value))
|
|
479
|
+
all.set(field, known)
|
|
480
|
+
})
|
|
481
|
+
return all
|
|
474
482
|
}
|
|
475
483
|
|
|
476
484
|
/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */
|
|
@@ -495,38 +503,33 @@ export const satisfiable = (
|
|
|
495
503
|
}
|
|
496
504
|
const constants = constantsOf(node)
|
|
497
505
|
// The search calls itself one time for each field that it fixes.
|
|
498
|
-
if (
|
|
506
|
+
if (constants.size > Limits.searchFields) {
|
|
499
507
|
return satisfiability.Unknown({ reason: "budget" })
|
|
500
508
|
}
|
|
501
|
-
// The cells of each field
|
|
502
|
-
|
|
503
|
-
const cells = MutableHashMap.empty<string, ReadonlyArray<unknown>>()
|
|
509
|
+
// The cells of each field, in a native `Map` as the constants are.
|
|
510
|
+
const cells = new Map<string, ReadonlyArray<unknown>>()
|
|
504
511
|
const search: Search = {
|
|
505
512
|
members: new Map(),
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
513
|
+
failed: new Set(),
|
|
514
|
+
cells: (field) => {
|
|
515
|
+
const made = cells.get(field)
|
|
516
|
+
if (made !== undefined) {
|
|
517
|
+
return made
|
|
518
|
+
}
|
|
519
|
+
// The cells come from the constants, so many constants are work
|
|
520
|
+
// that the budget must count before the cells are made.
|
|
521
|
+
const known = constants.get(field) ?? new Set<Nodes.Scalar>()
|
|
522
|
+
if (known.size > budget) {
|
|
523
|
+
throw new OutOfBudget()
|
|
524
|
+
}
|
|
525
|
+
const found = cellsOf(kindsOf(field), Array.from(known))
|
|
526
|
+
cells.set(field, found)
|
|
527
|
+
return found
|
|
528
|
+
},
|
|
521
529
|
budget,
|
|
522
530
|
}
|
|
523
531
|
try {
|
|
524
|
-
const found = searchNode(
|
|
525
|
-
Algebra.simplify(node),
|
|
526
|
-
search,
|
|
527
|
-
{ steps: 0, failed: HashSet.empty() },
|
|
528
|
-
[],
|
|
529
|
-
)
|
|
532
|
+
const found = searchNode(Algebra.simplify(node), search, { steps: 0 }, [])
|
|
530
533
|
return Option.match(found.assignment, {
|
|
531
534
|
onNone: () => satisfiability.Unsatisfiable(),
|
|
532
535
|
onSome: (assignment) =>
|