@nmnmcc/ability 2.0.4 → 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.
@@ -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
- /** Keeps the first cell of each text, in order. */
150
- const dedupeCells = (cells: ReadonlyArray<unknown>): ReadonlyArray<unknown> =>
151
- Arr.flatten(
152
- Arr.mapAccum(cells, HashSet.empty<string>(), (seen, cell) => {
153
- const text = cellText(cell)
154
- return [HashSet.add(seen, text), HashSet.has(seen, text) ? [] : [cell]]
155
- })[1],
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, HashSet.HashSet<string>>
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): HashSet.HashSet<string> => {
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 = HashSet.fromIterable(
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
- const counts = MutableHashMap.empty<string, number>()
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
- MutableHashMap.set(
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" || HashSet.has(state.failed, node)) {
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
- return Option.isSome(result.assignment)
387
- ? result
388
- : {
389
- assignment: Option.none(),
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
- /** The constants that the tests of each field use. */
438
- const constantsOf = (
439
- node: Node,
440
- ): HashMap.HashMap<string, HashSet.HashSet<Nodes.Scalar>> => {
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
- return Arr.reduce(pairs(node), HashMap.empty(), (all, [field, values]) =>
465
- HashMap.set(
466
- all,
467
- field,
468
- HashSet.union(
469
- Option.getOrElse(HashMap.get(all, field), HashSet.empty),
470
- HashSet.fromIterable(values),
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 (HashMap.size(constants) > Limits.searchFields) {
506
+ if (constants.size > Limits.searchFields) {
499
507
  return satisfiability.Unknown({ reason: "budget" })
500
508
  }
501
- // The cells of each field. A field name is a string, so an Effect map
502
- // finds it.
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
- cells: (field) =>
507
- Option.getOrElse(MutableHashMap.get(cells, field), () => {
508
- // The cells come from the constants, so many constants are work
509
- // that the budget must count before the cells are made.
510
- const known = Option.getOrElse(
511
- HashMap.get(constants, field),
512
- HashSet.empty,
513
- )
514
- if (HashSet.size(known) > budget) {
515
- throw new OutOfBudget()
516
- }
517
- const found = cellsOf(kindsOf(field), Array.from(known))
518
- MutableHashMap.set(cells, field, found)
519
- return found
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) =>