/ test / formula.spec.js
formula.spec.js
  1  import {
  2    fact,
  3    same,
  4    Collection,
  5    Memory,
  6    Task,
  7    Link,
  8    Text,
  9    UTF8,
 10    Math,
 11    Data,
 12    $,
 13  } from './lib.js'
 14  
 15  const refer = Memory.entity
 16  
 17  const db = Memory.create([
 18    { of: refer(1), the: 'type/text', is: 'hello' },
 19    { of: refer(1), the: 'type/int', is: 3 },
 20    { of: refer(1), the: 'type/bigint', is: 2n ** 60n },
 21    { of: refer(1), the: 'type/float', is: 5.2 },
 22    { of: refer(1), the: 'type/true', is: true },
 23    { of: refer(1), the: 'type/false', is: false },
 24    { of: refer(1), the: 'type/bytes', is: new Uint8Array([1, 2, 3]) },
 25    { of: refer(1), the: 'type/null', is: null },
 26    { of: refer(1), the: 'type/id', is: refer(1) },
 27  ])
 28  
 29  /**
 30   * @type {import('entail').Suite}
 31   */
 32  export const testRelation = {
 33    'test type relation': (assert) =>
 34      Task.spawn(function* () {
 35        const expert = /** @type {const} */ ({
 36          'type/text': 'string',
 37          'type/int': 'integer',
 38          'type/bigint': 'bigint',
 39          'type/float': 'float',
 40          'type/true': 'boolean',
 41          'type/false': 'boolean',
 42          'type/bytes': 'bytes',
 43          'type/null': 'null',
 44          'type/id': 'reference',
 45        })
 46  
 47        for (const [key, type] of Object.entries(expert)) {
 48          const Type = fact({ type: String })
 49            .with({ q: Object })
 50            .where(({ type, q }) => [
 51              Collection({ this: refer(1), at: key, of: q }),
 52              // match({ the: key, of: refer(1), is: q }),
 53              Data.Type({ of: q, is: type }),
 54              Type.claim({ type }),
 55            ])
 56  
 57          const result = yield* Type().query({ from: db })
 58  
 59          assert.deepEqual(
 60            result,
 61            [Type.assert({ type: type })],
 62            `Expected ${type} got ${result} `
 63          )
 64        }
 65  
 66        const Query = fact({ type: String }).where(({ type }) => [
 67          Data.Type({ of: Infinity, is: type }),
 68          Query.claim({ type }),
 69        ])
 70  
 71        assert.deepEqual(
 72          yield* Query().query({ from: db }),
 73          [],
 74          'produces no frames'
 75        )
 76      }),
 77  
 78    'reference relation': (assert) =>
 79      Task.spawn(function* () {
 80        const fixtures = [
 81          'hello',
 82          refer(1),
 83          3,
 84          2n ** 60n,
 85          5.2,
 86          true,
 87          false,
 88          new Uint8Array([1, 2, 3]),
 89        ]
 90  
 91        for (const data of fixtures) {
 92          const Target = fact({ link: Object }).where(({ link }) => [
 93            Data.Reference({ of: data, is: link }),
 94            Target.claim({ link }),
 95          ])
 96  
 97          assert.deepEqual(yield* Target().query({ from: db }), [
 98            Target.assert({ link: Link.of(data) }),
 99          ])
100        }
101      }),
102  
103    'test == relation': (assert) =>
104      Task.spawn(function* () {
105        const expert = {
106          'type/text': 'hello',
107          'type/int': 3,
108          'type/bigint': 2n ** 60n,
109          'type/float': 5.2,
110          'type/true': true,
111          'type/false': false,
112          'type/bytes': new Uint8Array([1, 2, 3]),
113          // 'type/null': 'null',
114          'type/id': refer(1),
115        }
116  
117        for (const [key, value] of Object.entries(expert)) {
118          const Query = fact({ q: Object }).where(({ q }) => [
119            Collection({ this: refer(1), at: key, of: q }),
120            Data.same({ this: q, as: value }),
121            Query.claim({ q }),
122          ])
123  
124          assert.deepEqual(yield* Query().query({ from: db }), [
125            Query.assert({ q: /** @type {any} */ (value) }),
126          ])
127        }
128  
129        const AssignmentQuery = fact({ q: Number }).where(({ q }) => [
130          Data.same({ this: 5, as: q }),
131          AssignmentQuery.claim({ q }),
132        ])
133  
134        assert.deepEqual(
135          yield* AssignmentQuery().query({ from: db }),
136          [AssignmentQuery.assert({ q: 5 })],
137          'will perform assignment'
138        )
139      }),
140  
141    'test text/concat': (assert) =>
142      Task.spawn(function* () {
143        const TwoPartsQuery = fact({ out: String })
144          .with({ text: String })
145          .where(({ text, out }) => [
146            Data.same({ this: 'hello', as: text }),
147            Text.Concat({ of: [text, ' world'], is: out }),
148            TwoPartsQuery.claim({ out }),
149          ])
150  
151        assert.deepEqual(yield* TwoPartsQuery().query({ from: db }), [
152          TwoPartsQuery.assert({ out: 'hello world' }),
153        ])
154  
155        const ThreePartsQuery = fact({ out: String })
156          .with({ text: String })
157          .where(({ text, out }) => [
158            Data.same({ this: 'hello', as: text }),
159            Text.Concat({ of: [text, ' world'], is: out }),
160            ThreePartsQuery.claim({ out }),
161          ])
162  
163        assert.deepEqual(yield* ThreePartsQuery().query({ from: db }), [
164          ThreePartsQuery.assert({ out: 'hello world' }),
165        ])
166      }),
167  
168    'test text/words': (assert) =>
169      Task.spawn(function* () {
170        const Word = fact({ word: String })
171          .with({ text: String })
172          .where(({ text, word }) => [
173            Data.same({ this: 'hello world', as: text }),
174            Text.Words({ of: text, is: word }),
175            Word.claim({ word }),
176          ])
177  
178        assert.deepEqual(yield* Word().query({ from: db }), [
179          Word.assert({ word: 'hello' }),
180          Word.assert({ word: 'world' }),
181        ])
182      }),
183  
184    'test text/lines': (assert) =>
185      Task.spawn(function* () {
186        const Line = fact({ content: String })
187          .with({ text: String })
188          .where(({ text, content }) => [
189            Data.same({ this: 'hello,\nhow are you\r\n', as: text }),
190            Text.Lines({ of: text, is: content }),
191            Line.claim({ content }),
192          ])
193  
194        assert.deepEqual(yield* Line().query({ from: db }), [
195          Line.assert({ content: 'hello,' }),
196          Line.assert({ content: 'how are you' }),
197          Line.assert({ content: '' }),
198        ])
199      }),
200  
201    'test text/case/upper': (assert) =>
202      Task.spawn(function* () {
203        const UpperCase = fact({ word: String })
204          .with({ text: String })
205          .where(({ word, text }) => [
206            Data.same({ this: 'hello', as: text }),
207            Text.UpperCase({ of: text, is: word }),
208            UpperCase.claim({ word }),
209          ])
210  
211        assert.deepEqual(yield* UpperCase().query({ from: db }), [
212          UpperCase.assert({ word: 'HELLO' }),
213        ])
214      }),
215  
216    'test text/case/lower': (assert) =>
217      Task.spawn(function* () {
218        const LowerCase = fact({ word: String })
219          .with({ text: String })
220          .where(({ text, word }) => [
221            Data.same({ this: 'Hello', as: text }),
222            Text.LowerCase({ of: text, is: word }),
223            LowerCase.claim({ word }),
224          ])
225  
226        assert.deepEqual(yield* LowerCase().query({ from: db }), [
227          LowerCase.assert({ word: 'hello' }),
228        ])
229      }),
230  
231    'test string/trim': (assert) =>
232      Task.spawn(function* () {
233        const Trim = fact({ text: String })
234          .with({ source: String })
235          .where(({ text, source }) => [
236            Data.same({ this: '   Hello world!   ', as: source }),
237            Text.Trim({ of: source, is: text }),
238            Trim.claim({ text }),
239          ])
240  
241        assert.deepEqual(yield* Trim().query({ from: db }), [
242          Trim.assert({ text: 'Hello world!' }),
243        ])
244      }),
245  
246    'test text/trim/start': (assert) =>
247      Task.spawn(function* () {
248        const TrimStart = fact({ text: String })
249          .with({ source: String })
250          .where(({ source, text }) => [
251            Data.same({ this: '   Hello world!   ', as: source }),
252            Text.TrimStart({ of: source, is: text }),
253            TrimStart.claim({ text }),
254          ])
255  
256        assert.deepEqual(yield* TrimStart().query({ from: db }), [
257          TrimStart.assert({ text: 'Hello world!   ' }),
258        ])
259      }),
260    'test string/trim/end': (assert) =>
261      Task.spawn(function* () {
262        const TrimEnd = fact({ text: String })
263          .with({ source: String })
264          .where(({ text, source }) => [
265            Data.same({ this: '   Hello world!   ', as: source }),
266            Text.TrimEnd({ of: source, is: text }),
267            TrimEnd.claim({ text }),
268          ])
269  
270        assert.deepEqual(yield* TrimEnd().query({ from: db }), [
271          TrimEnd.assert({ text: '   Hello world!' }),
272        ])
273      }),
274    'test utf8/to/text': (assert) =>
275      Task.spawn(function* () {
276        const Text = fact({ content: String })
277          .with({ bytes: Uint8Array })
278          .where(({ bytes, content }) => [
279            Data.same({
280              this: new TextEncoder().encode('Hello world!'),
281              as: bytes,
282            }),
283            UTF8.ToText({ of: bytes, is: content }),
284            Text.claim({ content }),
285          ])
286  
287        assert.deepEqual(yield* Text().query({ from: db }), [
288          Text.assert({ content: 'Hello world!' }),
289        ])
290      }),
291  
292    'test text/to/utf8': (assert) =>
293      Task.spawn(function* () {
294        const Bytes = fact({ content: Uint8Array })
295          .with({ text: String })
296          .where(({ text, content }) => [
297            Data.same({ this: 'Hello world!', as: text }),
298            UTF8.FromText({ of: text, is: content }),
299            Bytes.claim({ content }),
300          ])
301  
302        assert.deepEqual(yield* Bytes().query({ from: db }), [
303          Bytes.assert({ content: new TextEncoder().encode('Hello world!') }),
304        ])
305      }),
306  
307    'test text/length': (assert) =>
308      Task.spawn(function* () {
309        const Info = fact({ length: Number })
310          .with({ text: String })
311          .where(({ text, length }) => [
312            Data.same({ this: 'Hello world!', as: text }),
313            Text.Length({ of: text, is: length }),
314            Info.claim({ length }),
315          ])
316  
317        assert.deepEqual(yield* Info().query({ from: db }), [
318          Info.assert({ length: 12 }),
319        ])
320      }),
321  
322    'test + operator': (assert) =>
323      Task.spawn(function* () {
324        const Count = fact({ c: Number })
325          .with({ a: Number, b: Number })
326          .where(({ a, b, c }) => [
327            same({ this: a, as: 1 }),
328            same({ this: b, as: 2 }),
329            Math.Sum({ of: a, with: b, is: c }),
330            Count.claim({ c }),
331          ])
332  
333        assert.deepEqual(yield* Count().query({ from: db }), [
334          Count.assert({ c: 3 }),
335        ])
336  
337        const Compute = fact({ c: Number })
338          .with({ a: Number, b: Number, ab: Number, ab10: Number })
339          .where(({ a, b, ab, ab10, c }) => [
340            same({ this: a, as: 1 }),
341            same({ this: 2, as: b }),
342            // Note: Multiple term addition is not directly supported by the API
343            // This is a simplification that adds terms sequentially
344            Math.Sum({ of: a, with: b, is: ab }),
345            Math.Sum({ of: ab, with: 10, is: ab10 }),
346            Math.Sum({ of: ab10, with: b, is: c }),
347            Compute.claim({ c }),
348          ])
349  
350        assert.deepEqual(yield* Compute().query({ from: db }), [
351          Compute.assert({ c: 15 }),
352        ])
353  
354        const Five = fact({ c: Number }).where(({ c }) => [
355          same({ this: c, as: 5 }),
356          Five.claim({ c }),
357        ])
358  
359        assert.deepEqual(yield* Five().query({ from: db }), [
360          Five.assert({ c: 5 }),
361        ])
362      }),
363  
364    'test - operator': (assert) =>
365      Task.spawn(function* () {
366        const TwoTermsSubtract = fact({ c: Number })
367          .with({ a: Number, b: Number })
368          .where(({ a, b, c }) => [
369            Data.same({ this: 10, as: a }),
370            Data.same({ this: 2, as: b }),
371            Math.Subtraction({ of: a, by: b, is: c }),
372            TwoTermsSubtract.claim({ c }),
373          ])
374  
375        assert.deepEqual(yield* TwoTermsSubtract().query({ from: db }), [
376          TwoTermsSubtract.assert({ c: 8 }),
377        ])
378  
379        const MultiTermsSubtract = fact({ c: Number })
380          .with({ a: Number, b: Number, ab: Number, ab1: Number })
381          .where(({ a, b, ab, ab1, c }) => [
382            Data.same({ this: 10, as: a }),
383            Data.same({ this: 2, as: b }),
384            // Multi-term subtraction not directly supported
385            Math.Subtraction({ of: a, by: b, is: ab }),
386            Math.Subtraction({ of: ab, by: 1, is: ab1 }),
387            Math.Subtraction({ of: ab1, by: b, is: c }),
388            MultiTermsSubtract.claim({ c }),
389          ])
390  
391        assert.deepEqual(yield* MultiTermsSubtract().query({ from: db }), [
392          MultiTermsSubtract.assert({ c: 5 }),
393        ])
394      }),
395  
396    'test * operator': (assert) =>
397      Task.spawn(function* () {
398        const TwoTermsQuery = fact({ c: Number })
399          .with({ a: Number, b: Number })
400          .where(({ a, b, c }) => [
401            Data.same({ this: 10, as: a }),
402            Data.same({ this: 2, as: b }),
403            Math.Multiplication({ of: a, by: b, is: c }),
404            TwoTermsQuery.claim({ c }),
405          ])
406  
407        assert.deepEqual(yield* TwoTermsQuery().query({ from: db }), [
408          TwoTermsQuery.assert({ c: 20 }),
409        ])
410  
411        const MultiTermsQuery = fact({ c: Number })
412          .with({ a: Number, b: Number, ab: Number, ab3: Number })
413          .where(({ a, b, c, ab, ab3 }) => [
414            Data.same({ this: 10, as: a }),
415            Data.same({ this: 2, as: b }),
416            // Multi-term multiplication not directly supported
417            Math.Multiplication({ of: a, by: b, is: ab }),
418            Math.Multiplication({ of: ab, by: 3, is: ab3 }),
419            Math.Multiplication({ of: ab3, by: b, is: c }),
420            MultiTermsQuery.claim({ c }),
421          ])
422  
423        assert.deepEqual(yield* MultiTermsQuery().query({ from: db }), [
424          MultiTermsQuery.assert({ c: 120 }),
425        ])
426      }),
427  
428    'test / operator': (assert) =>
429      Task.spawn(function* () {
430        const TwoTermsQuery = fact({ c: Number })
431          .with({ a: Number, b: Number })
432          .where(({ a, b, c }) => [
433            Data.same({ this: 10, as: a }),
434            Data.same({ this: 2, as: b }),
435            Math.Division({ of: a, by: b, is: c }),
436            TwoTermsQuery.claim({ c }),
437          ])
438  
439        assert.deepEqual(yield* TwoTermsQuery().query({ from: db }), [
440          TwoTermsQuery.assert({ c: 5 }),
441        ])
442  
443        const MultiTermsQuery = fact({ c: Number })
444          .with({ a: Number, b: Number, ab: Number, ab3: Number })
445          .where(({ a, b, c, ab, ab3 }) => [
446            Data.same({ this: 48, as: a }),
447            Data.same({ this: 2, as: b }),
448            // Multi-term division not directly supported
449            Math.Division({ of: a, by: b, is: ab }),
450            Math.Division({ of: ab, by: 3, is: ab3 }),
451            Math.Division({ of: ab3, by: b, is: c }),
452            MultiTermsQuery.claim({ c }),
453          ])
454  
455        assert.deepEqual(yield* MultiTermsQuery().query({ from: db }), [
456          MultiTermsQuery.assert({ c: 4 }),
457        ])
458  
459        const SingleTermQuery = fact({ c: Number })
460          .with({ a: Number })
461          .where(({ a, c }) => [
462            Data.same({ this: 5, as: a }),
463            Math.Division({ of: a, by: 2, is: c }),
464            SingleTermQuery.claim({ c }),
465          ])
466  
467        assert.deepEqual(yield* SingleTermQuery().query({ from: db }), [
468          SingleTermQuery.assert({ c: 2.5 }),
469        ])
470  
471        const DivisionByZeroQuery = fact({ c: Number })
472          .with({ a: Number })
473          .where(({ a, c }) => [
474            Data.same({ this: 5, as: a }),
475            // Division by zero
476            Math.Division({ of: a, by: 0, is: c }),
477            DivisionByZeroQuery.claim({ c }),
478          ])
479  
480        assert.deepEqual(
481          yield* DivisionByZeroQuery().query({ from: db }),
482          [],
483          'division by zero not allowed'
484        )
485      }),
486  
487    'test % operator': (assert) =>
488      Task.spawn(function* () {
489        const Query = fact({ c: Number })
490          .with({ a: Number, b: Number })
491          .where(({ a, b, c }) => [
492            Data.same({ this: 9, as: a }),
493            Data.same({ this: 4, as: b }),
494            Math.Modulo({ of: a, by: b, is: c }),
495            Query.claim({ c }),
496          ])
497  
498        assert.deepEqual(yield* Query().query({ from: db }), [
499          Query.assert({ c: 1 }),
500        ])
501      }),
502  
503    'test ** operator': (assert) =>
504      Task.spawn(function* () {
505        const Query = fact({ c: Number })
506          .with({ b: Number })
507          .where(({ b, c }) => [
508            Data.same({ this: 3, as: b }),
509            Math.Power({ of: 2, exponent: b, is: c }),
510            Query.claim({ c }),
511          ])
512  
513        assert.deepEqual(yield* Query().query({ from: db }), [
514          Query.assert({ c: 8 }),
515        ])
516      }),
517  
518    'test math/absolute': (assert) =>
519      Task.spawn(function* () {
520        const Query = fact({ c: Number, d: Number })
521          .with({ a: Number, b: Number })
522          .where(({ a, b, c, d }) => [
523            Data.same({ this: 2, as: a }),
524            Data.same({ this: -3, as: b }),
525            Math.Absolute({ of: a, is: c }),
526            Math.Absolute({ of: b, is: d }),
527            Query.claim({ c, d }),
528          ])
529  
530        assert.deepEqual(yield* Query().query({ from: db }), [
531          Query.assert({ c: 2, d: 3 }),
532        ])
533      }),
534  
535    'test text/like': (assert) =>
536      Task.spawn(function* () {
537        const WithResultQuery = fact({ out: String })
538          .with({ text: String })
539          .where(({ text, out }) => [
540            Data.same({ this: 'Hello World', as: text }),
541            Text.match({ this: text, pattern: 'Hello*' }),
542            Data.same({ this: text, as: out }),
543            WithResultQuery.claim({ out }),
544          ])
545  
546        assert.deepEqual(yield* WithResultQuery().query({ from: db }), [
547          WithResultQuery.assert({ out: 'Hello World' }),
548        ])
549  
550        const BooleanPatternQuery = fact({ text: String }).where(({ text }) => [
551          Data.same({ this: 'Hello World', as: text }),
552          Text.match({ this: text, pattern: 'Hello*' }),
553          BooleanPatternQuery.claim({ text }),
554        ])
555  
556        assert.deepEqual(yield* BooleanPatternQuery().query({ from: db }), [
557          BooleanPatternQuery.assert({ text: 'Hello World' }),
558        ])
559  
560        const NoMatchQuery = fact({ out: String })
561          .with({ text: String })
562          .where(({ text, out }) => [
563            Data.same({ this: 'Hello World', as: text }),
564            Text.match({ this: text, pattern: 'hello*' }),
565            Data.same({ this: text, as: out }),
566            NoMatchQuery.claim({ out }),
567          ])
568  
569        assert.deepEqual(yield* NoMatchQuery().query({ from: db }), [])
570      }),
571  
572    'test text/includes': (assert) =>
573      Task.spawn(function* () {
574        const WithResultQuery = fact({ out: String })
575          .with({ text: String })
576          .where(({ text, out }) => [
577            Data.same({ this: 'Hello World', as: text }),
578            Text.includes({ this: text, slice: 'Hello' }),
579            Data.same({ this: text, as: out }),
580            WithResultQuery.claim({ out }),
581          ])
582  
583        assert.deepEqual(yield* WithResultQuery().query({ from: db }), [
584          WithResultQuery.assert({ out: 'Hello World' }),
585        ])
586  
587        const BooleanQuery = fact({ text: String }).where(({ text }) => [
588          Data.same({ this: 'Hello World', as: text }),
589          Text.includes({ this: text, slice: 'World' }),
590          BooleanQuery.claim({ text }),
591        ])
592  
593        assert.deepEqual(yield* BooleanQuery().query({ from: db }), [
594          BooleanQuery.assert({ text: 'Hello World' }),
595        ])
596  
597        const NoMatchQuery = fact({ out: String })
598          .with({ text: String })
599          .where(({ text, out }) => [
600            Data.same({ this: 'Hello World', as: text }),
601            Text.includes({ this: text, slice: 'hello' }),
602            Data.same({ this: text, as: out }),
603            NoMatchQuery.claim({ out }),
604          ])
605  
606        assert.deepEqual(yield* NoMatchQuery().query({ from: db }), [])
607      }),
608  
609    'test reference group': async (assert) => {
610      const Test = fact({ out: Object, a: String, b: String }).where(
611        ({ a, b, out }) => [
612          Data.same({ this: 'world', as: b }),
613          Data.Reference({ is: out, of: { a, b } }),
614          Test.claim({ out, a, b }),
615        ]
616      )
617  
618      assert.deepEqual(
619        await Test.match({ a: 'hello', b: $.b, out: $.q }).query({
620          from: db,
621        }),
622        [
623          Test.assert({
624            a: 'hello',
625            b: 'world',
626            out: Link.of({ a: 'hello', b: 'world' }),
627          }),
628        ]
629      )
630    },
631    'test fact': async (assert) => {
632      const Test = fact({ out: Object, a: String, b: String }).where(
633        ({ a, b, out }) => [
634          Data.same({ this: 'world', as: b }),
635          Data.Fact({ this: out, a, b }),
636          Test.claim({ a, b, out }),
637        ]
638      )
639  
640      assert.deepEqual(
641        await Test.match({ a: 'hello' }).query({
642          from: db,
643        }),
644        [
645          Test.assert({
646            a: 'hello',
647            b: 'world',
648            out: Link.of({ a: 'hello', b: 'world' }),
649          }),
650        ]
651      )
652    },
653  }