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 }