/ test / loop.spec.js
loop.spec.js
  1  import { $, fact, Data, Math, Memory, API, Link, Collection } from './lib.js'
  2  
  3  const id = Memory.entity
  4  
  5  const db = Memory.create([
  6    // bafyr4ibnhlpn74i3mhyuzcdogwx2anttnxgypj2ne624cuicexiplexccm
  7    { of: id(0), the: 'data/type', is: 'list' },
  8    // bafyr4ici7rzb7o6bolqjex5cplywohpcew5je4juqauzrmikcvukdcdffm
  9    { of: id(1), the: 'meta/name', is: 'a' },
 10    // bafyr4iflco7n6qxijoxa67dcy7owvcw2k4piqkn623vflaqx6a3bwxrf2a
 11    { of: id(2), the: 'meta/name', is: 'b' },
 12    // bafyr4ihb4dub23vdtmgprodp7vcasiibd5luadf4h53krilrsbvjxdlvau
 13    { of: id(3), the: 'meta/name', is: 'c' },
 14    { of: id(0), the: 'list/next', is: id(1) },
 15    { of: id(1), the: 'list/next', is: id(2) },
 16    { of: id(2), the: 'list/next', is: id(3) },
 17  ])
 18  
 19  /**
 20   * @type {import('entail').Suite}
 21   */
 22  export const testRecursion = {
 23    'test ancestor': async (assert) => {
 24      const Parent = fact({
 25        the: 'child',
 26        parent: Object,
 27      })
 28  
 29      const Ancestor = fact({ of: Object })
 30        .with({ parent: Object })
 31        .when(({ this: ancestor, of: child, parent }) => ({
 32          direct: [Parent({ parent: ancestor, this: child })],
 33          transitive: [
 34            Parent({ parent, this: child }),
 35            Ancestor({ this: ancestor, of: parent }),
 36          ],
 37        }))
 38  
 39      const eve = {
 40        'person/name': 'Eve',
 41      }
 42      const adam = {
 43        'person/name': 'Adam',
 44        'child/parent': eve,
 45      }
 46      const jack = {
 47        'person/name': 'Jack',
 48        'child/parent': adam,
 49      }
 50      const mallory = {
 51        'person/name': 'Mallory',
 52        'child/parent': jack,
 53      }
 54      const bob = {
 55        'person/name': 'Bob',
 56        'child/parent': mallory,
 57      }
 58      const alice = {
 59        'person/name': 'Alice',
 60        'child/parent': bob,
 61      }
 62  
 63      const people = {
 64        alice,
 65        bob,
 66        mallory,
 67        jack,
 68        adam,
 69        eve,
 70      }
 71  
 72      const ancestors = await Ancestor().query({
 73        from: Memory.create({ people }),
 74      })
 75  
 76      assert.deepEqual(
 77        new Set(ancestors),
 78        new Set([
 79          Ancestor.assert({ this: Link.of(bob), of: Link.of(alice) }),
 80          Ancestor.assert({ this: Link.of(mallory), of: Link.of(bob) }),
 81          Ancestor.assert({ this: Link.of(jack), of: Link.of(mallory) }),
 82          Ancestor.assert({ this: Link.of(adam), of: Link.of(jack) }),
 83          Ancestor.assert({ this: Link.of(mallory), of: Link.of(alice) }),
 84          Ancestor.assert({ this: Link.of(jack), of: Link.of(bob) }),
 85          Ancestor.assert({ this: Link.of(adam), of: Link.of(mallory) }),
 86          Ancestor.assert({ this: Link.of(jack), of: Link.of(alice) }),
 87          Ancestor.assert({ this: Link.of(adam), of: Link.of(bob) }),
 88          Ancestor.assert({ this: Link.of(adam), of: Link.of(alice) }),
 89          Ancestor.assert({ this: Link.of(eve), of: Link.of(alice) }),
 90          Ancestor.assert({ this: Link.of(eve), of: Link.of(bob) }),
 91          Ancestor.assert({ this: Link.of(eve), of: Link.of(mallory) }),
 92          Ancestor.assert({ this: Link.of(eve), of: Link.of(jack) }),
 93          Ancestor.assert({ this: Link.of(eve), of: Link.of(adam) }),
 94        ])
 95      )
 96    },
 97  
 98    'complex ancestor test': async (assert) => {
 99      const Parent = fact({
100        the: 'child',
101        parent: Object,
102      })
103  
104      const Ancestor = fact({ of: Object })
105        .with({ parent: Object })
106        .when(({ this: ancestor, of: child, parent }) => ({
107          direct: [Parent({ parent: ancestor, this: child })],
108          transitive: [
109            Parent({ parent, this: child }),
110            Ancestor({ this: ancestor, of: parent }),
111          ],
112        }))
113  
114      // Create a complex family tree with multiple paths to the same nodes
115      //
116      // Family Tree Structure (→ means "has parent"):
117      //
118      //      david → mallory
119      //     /               \
120      // alice → bob         jack
121      //     \     \        /   \
122      //      \     mallory     adam → eve
123      //       \                /
124      //        charlie → dave
125      //
126      // Note: Both bob and david have mallory as parent
127      // Note: Both mallory and dave have jack as parent
128      // Note: There are multiple paths from alice to jack (and thus to adam and eve)
129  
130      const alice = Link.of('alice')
131      const bob = Link.of('bob')
132      const charlie = Link.of('charlie')
133      const david = Link.of('david')
134      const mallory = Link.of('mallory')
135      const jack = Link.of('jack')
136      const adam = Link.of('adam')
137      const eve = Link.of('eve')
138      const dave = Link.of('dave')
139      const db = Memory.create([
140        // First branch
141        { of: alice, the: 'child/parent', is: bob },
142        { of: bob, the: 'child/parent', is: mallory },
143        { of: mallory, the: 'child/parent', is: jack },
144  
145        // Second branch
146        { of: alice, the: 'child/parent', is: charlie },
147        { of: charlie, the: 'child/parent', is: dave },
148        { of: dave, the: 'child/parent', is: jack },
149  
150        // Third branch
151        { of: alice, the: 'child/parent', is: david },
152        { of: david, the: 'child/parent', is: mallory },
153  
154        // Common descendants
155        { of: jack, the: 'child/parent', is: adam },
156        { of: adam, the: 'child/parent', is: eve },
157      ])
158  
159      const ancestors = await Ancestor().query({ from: db })
160  
161      // Define the expected ancestor relationships
162      const expectedAncestors = [
163        // Direct parent relationships (10)
164        Ancestor.assert({ this: bob, of: alice }),
165        Ancestor.assert({ this: charlie, of: alice }),
166        Ancestor.assert({ this: david, of: alice }),
167        Ancestor.assert({ this: mallory, of: bob }),
168        Ancestor.assert({ this: mallory, of: david }),
169        Ancestor.assert({ this: jack, of: mallory }),
170        Ancestor.assert({ this: dave, of: charlie }),
171        Ancestor.assert({ this: jack, of: dave }),
172        Ancestor.assert({ this: adam, of: jack }),
173        Ancestor.assert({ this: eve, of: adam }),
174  
175        // Transitive relationships (19)
176        Ancestor.assert({ this: mallory, of: alice }), // via bob or via david
177        Ancestor.assert({ this: jack, of: alice }), // via bob/mallory or via david/mallory or via charlie/dave
178        Ancestor.assert({ this: jack, of: bob }), // via mallory
179        Ancestor.assert({ this: jack, of: david }), // via mallory
180        Ancestor.assert({ this: jack, of: charlie }), // via dave
181        Ancestor.assert({ this: adam, of: alice }), // via any path to jack
182        Ancestor.assert({ this: adam, of: bob }), // via mallory/jack
183        Ancestor.assert({ this: adam, of: david }), // via mallory/jack
184        Ancestor.assert({ this: adam, of: mallory }), // via jack
185        Ancestor.assert({ this: adam, of: charlie }), // via dave/jack
186        Ancestor.assert({ this: adam, of: dave }), // via jack
187        Ancestor.assert({ this: dave, of: alice }), // via charlie
188        Ancestor.assert({ this: eve, of: alice }), // via any path to adam
189        Ancestor.assert({ this: eve, of: bob }), // via mallory/jack/adam
190        Ancestor.assert({ this: eve, of: david }), // via mallory/jack/adam
191        Ancestor.assert({ this: eve, of: mallory }), // via jack/adam
192        Ancestor.assert({ this: eve, of: jack }), // via adam
193        Ancestor.assert({ this: eve, of: charlie }), // via dave/jack/adam
194        Ancestor.assert({ this: eve, of: dave }), // via jack/adam
195      ]
196  
197      // Create sets for comparison (using string representations for equality comparison)
198      const expectedSet = new Set(
199        expectedAncestors.map((rel) =>
200          JSON.stringify({ this: rel.this, of: rel.of })
201        )
202      )
203  
204      const actualSet = new Set(
205        ancestors.map((rel) => JSON.stringify({ this: rel.this, of: rel.of }))
206      )
207  
208      // Find missing and extra relationships
209      const missing = [...expectedSet].filter((rel) => !actualSet.has(rel))
210      const extra = [...actualSet].filter((rel) => !expectedSet.has(rel))
211  
212      if (missing.length > 0 || extra.length > 0) {
213        console.log(
214          'Missing relationships:',
215          missing.map((r) => JSON.parse(r))
216        )
217        console.log(
218          'Extra relationships:',
219          extra.map((r) => JSON.parse(r))
220        )
221      }
222  
223      // Verify we have the correct total number of unique ancestor relationships
224      // Direct relationships: 10
225      // Transitive relationships: 19
226      // Total: 29 unique relationships
227      assert.strictEqual(
228        actualSet.size,
229        29,
230        `Should have 29 total unique ancestor relationships, but got ${actualSet.size}`
231      )
232    },
233    'test traverse': async (assert) => {
234      const Range = fact({ n: Number, from: Number, to: Number })
235        .with({ m: Number })
236        .when(({ n, from, to, m }) => ({
237          base: [
238            Data.less({ this: from, than: to }),
239            Data.same({ this: from, as: n }),
240            Range.claim({ n, from, to }),
241          ],
242          step: [
243            Data.less({ this: from, than: to }),
244            Math.Sum({ of: from, with: 1, is: m }),
245            Range({ n, from: m, to: to }),
246          ],
247        }))
248  
249      assert.deepEqual(
250        await Range.match({ from: 1, to: 5 }).query({ from: db }),
251        [
252          Range.assert({ from: 1, n: 1, to: 5 }),
253          Range.assert({
254            this: Range.assert({ from: 2, n: 2, to: 5 }).this,
255            from: 1,
256            n: 2,
257            to: 5,
258          }),
259          Range.assert({
260            this: Range.assert({ from: 3, n: 3, to: 5 }).this,
261            from: 1,
262            n: 3,
263            to: 5,
264          }),
265          Range.assert({
266            this: Range.assert({ from: 4, n: 4, to: 5 }).this,
267            from: 1,
268            n: 4,
269            to: 5,
270          }),
271        ]
272      )
273    },
274  
275    'test recursion': async (assert) => {
276      const Head = fact({ the: 'list', next: Object })
277  
278      const List = Head.with({ transient: Object }).when(
279        ({ transient, ...head }) => ({
280          head: [Head(head)],
281          child: [
282            Head({ this: head.this, next: transient }),
283            List({ this: transient, next: head.next }),
284          ],
285        })
286      )
287  
288      const Meta = fact({
289        the: 'meta',
290        name: String,
291      })
292  
293      const Connection = fact({
294        from: Object,
295        to: Object,
296        name: String,
297      }).where(({ from, to, name }) => [
298        List({ this: from, next: to }),
299        Meta({ this: to, name: name }),
300        Connection.claim({ from, to, name }),
301      ])
302  
303      return assert.deepEqual(await Connection().query({ from: db }), [
304        Connection.assert({ from: id(0), to: id(1), name: 'a' }),
305        Connection.assert({ from: id(1), to: id(2), name: 'b' }),
306        Connection.assert({ from: id(0), to: id(2), name: 'b' }),
307        Connection.assert({ from: id(2), to: id(3), name: 'c' }),
308        Connection.assert({ from: id(1), to: id(3), name: 'c' }),
309        Connection.assert({ from: id(0), to: id(3), name: 'c' }),
310      ])
311    },
312    'test rooted recursion': async (assert) => {
313      // return 'seems to enter infinite loop'
314      const Content = fact({ the: 'data', type: String })
315      const Head = fact({ the: 'list', next: Object })
316      const Meta = fact({ the: 'meta', name: String })
317      const List = Head.with({ trasitive: Object }).when(
318        ({ trasitive, ...head }) => ({
319          direct: [Head(head)],
320          trasitive: [
321            Head({ this: head.this, next: trasitive }),
322            List({ this: trasitive, next: head.next }),
323          ],
324        })
325      )
326  
327      const Node = fact({
328        node: Object,
329        name: String,
330      })
331        .with({ root: Object })
332        .where(({ node, name, root }) => [
333          Content({ this: root, type: 'list' }),
334          List({ this: root, next: node }),
335          Meta({ this: node, name }),
336          Node.claim({ node, name }),
337        ])
338  
339      assert.deepEqual(await Node().query({ from: db }), [
340        Node.assert({ node: id(1), name: 'a' }),
341        Node.assert({ node: id(2), name: 'b' }),
342        Node.assert({ node: id(3), name: 'c' }),
343      ])
344    },
345    'test implicit': async (assert) => {
346      const Content = fact({ the: 'data', type: String })
347      const Head = fact({ the: 'list', next: Object })
348      const Meta = fact({ the: 'meta', name: String })
349      const List = Head.with({ transient: Object }).when(
350        ({ transient, ...head }) => ({
351          head: [Head(head)],
352          child: [
353            Head({ this: head.this, next: transient }),
354            List({ this: transient, next: head.next }),
355          ],
356        })
357      )
358  
359      const Implicit = fact({
360        at: String,
361        is: Object,
362        default: null,
363      }).when(({ at, this: of, is, default: implicit, _ }) => ({
364        explicit: [
365          Collection({ this: of, at, of: is }),
366          Data.Type({ of: implicit, is: _ }),
367        ],
368        implicit: [
369          Collection.not({ this: of, at }),
370          Data.same({ this: implicit, as: is }),
371        ],
372      }))
373  
374      const Node = fact({
375        each: Object,
376        name: String,
377        next: Object,
378      })
379        .with({ root: Object })
380        .where(({ each, name, next, root }) => [
381          Content({ this: root, type: 'list' }),
382          List({ this: root, next: each }),
383          Meta({ this: each, name }),
384          Implicit({
385            this: each,
386            at: 'list/next',
387            is: next,
388            default: null,
389          }),
390          Node.claim({ each, name, next }),
391        ])
392  
393      assert.deepEqual(await Node().query({ from: db }), [
394        Node.assert({ each: id(1), name: 'a', next: id(2) }),
395        Node.assert({ each: id(2), name: 'b', next: id(3) }),
396        // @ts-expect-error - inference doesn't know next is entity | null
397        Node.assert({ each: id(3), name: 'c', next: null }),
398      ])
399    },
400  
401    'test recursion termination': async (assert) => {
402      const Content = fact({ the: 'data', type: String })
403      const Head = fact({ the: 'list', next: Object })
404      const Meta = fact({ the: 'meta', name: String })
405      const List = Head.with({ trasitive: Object }).when(
406        ({ trasitive, ...head }) => ({
407          direct: [Head(head)],
408          trasitive: [
409            Head({ this: head.this, next: trasitive }),
410            List({ this: trasitive, next: head.next }),
411          ],
412        })
413      )
414  
415      const Node = fact({
416        node: Object,
417        name: String,
418      })
419        .with({ root: Object })
420        .where(({ node, name, root }) => [
421          Content({ this: root, type: 'list' }),
422          List({ this: root, next: node }),
423          Meta({ this: node, name }),
424          Node.claim({ node, name }),
425        ])
426  
427      assert.deepEqual(await Node().query({ from: db }), [
428        Node.assert({
429          node: id(1),
430          name: 'a',
431        }),
432        Node.assert({
433          node: id(2),
434          name: 'b',
435        }),
436        Node.assert({
437          node: id(3),
438          name: 'c',
439        }),
440      ])
441    },
442  
443    'test not really recursive': async (assert) => {
444      const Person = fact({
445        the: 'meta',
446        name: String,
447      })
448  
449      const Query = Person.when((person) => ({
450        base: [Person(person)],
451        recur: [Query(person)],
452      }))
453  
454      const results = await Query().query({ from: db })
455      // Check result length rather than exact content due to ID/hash mismatches
456      assert.strictEqual(results.length, 3, 'Should return 3 people')
457  
458      // Check each object has the correct name
459      const names = results.map((r) => r.name).sort()
460      assert.deepEqual(names, ['a', 'b', 'c'], 'Should include all 3 names')
461    },
462  
463    'test tautology': async (assert) => {
464      const Person = fact({
465        the: 'meta',
466        name: String,
467      })
468  
469      const Tautology = Person.where((person) => [
470        Person(person),
471        Tautology(person),
472      ])
473  
474      const results = await Tautology().query({ from: db })
475      // Check result length rather than exact content due to ID/hash mismatches
476      assert.strictEqual(results.length, 3, 'Should return 3 people')
477  
478      // Check each object has the correct name
479      const names = results.map((r) => r.name).sort()
480      assert.deepEqual(names, ['a', 'b', 'c'], 'Should include all 3 names')
481    },
482  
483    'test partial binding tautology': async (assert) => {
484      const Person = fact({
485        the: 'meta',
486        name: String,
487      })
488  
489      // Define a relationship that introduces a second unbound variable
490      const PartialTautology = fact({
491        name: String,
492        extra: Object, // This will remain unbound in the recursive case
493      }).where(({ this: person, name, extra }) => [
494        Person({ this: person, name }),
495        PartialTautology({ this: person, name, extra }),
496      ])
497  
498      // This test should complete without infinite recursion
499      // The query should return results only for valid bindings
500      const results = await PartialTautology().query({ from: db })
501  
502      assert.strictEqual(results.length, 0, 'Should have no results')
503    },
504  }