eyeling 1.9.1 → 1.9.2

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/eyeling.js CHANGED
@@ -2484,948 +2484,620 @@ function reorderPremiseForConstraints(premise) {
2484
2484
  // @ts-nocheck
2485
2485
  /* eslint-disable */
2486
2486
  // ===========================================================================
2487
- // Unification + substitution
2487
+ // BUILTINS
2488
2488
  // ===========================================================================
2489
- function containsVarTerm(t, v) {
2490
- if (t instanceof Var)
2491
- return t.name === v;
2492
- if (t instanceof ListTerm)
2493
- return t.elems.some((e) => containsVarTerm(e, v));
2494
- if (t instanceof OpenListTerm)
2495
- return t.prefix.some((e) => containsVarTerm(e, v)) || t.tailVar === v;
2496
- if (t instanceof GraphTerm)
2497
- return t.triples.some((tr) => containsVarTerm(tr.s, v) || containsVarTerm(tr.p, v) || containsVarTerm(tr.o, v));
2498
- return false;
2489
+ function literalParts(lit) {
2490
+ const cached = __literalPartsCache.get(lit);
2491
+ if (cached)
2492
+ return cached;
2493
+ // Split a literal into lexical form and datatype IRI (if any).
2494
+ // Also strip an optional language tag from the lexical form:
2495
+ // "\"hello\"@en" -> "\"hello\""
2496
+ // "\"hello\"@en^^<...>" is rejected earlier in the parser.
2497
+ const idx = lit.indexOf('^^');
2498
+ let lex = lit;
2499
+ let dt = null;
2500
+ if (idx >= 0) {
2501
+ lex = lit.slice(0, idx);
2502
+ dt = lit.slice(idx + 2).trim();
2503
+ if (dt.startsWith('<') && dt.endsWith('>')) {
2504
+ dt = dt.slice(1, -1);
2505
+ }
2506
+ }
2507
+ // Strip LANGTAG from the lexical form when present.
2508
+ if (lex.length >= 2 && lex[0] === '"') {
2509
+ const lastQuote = lex.lastIndexOf('"');
2510
+ if (lastQuote > 0 && lastQuote < lex.length - 1 && lex[lastQuote + 1] === '@') {
2511
+ const lang = lex.slice(lastQuote + 2);
2512
+ if (/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) {
2513
+ lex = lex.slice(0, lastQuote + 1);
2514
+ }
2515
+ }
2516
+ }
2517
+ const res = [lex, dt];
2518
+ __literalPartsCache.set(lit, res);
2519
+ return res;
2499
2520
  }
2500
- function isGroundTermInGraph(t) {
2501
- // variables inside graph terms are treated as local placeholders,
2502
- // so they don't make the *surrounding triple* non-ground.
2503
- if (t instanceof OpenListTerm)
2521
+ function literalHasLangTag(lit) {
2522
+ // True iff the literal is a quoted string literal with a language tag suffix,
2523
+ // e.g. "hello"@en or """hello"""@en.
2524
+ // (The parser rejects language tags combined with datatypes.)
2525
+ if (typeof lit !== 'string')
2504
2526
  return false;
2505
- if (t instanceof ListTerm)
2506
- return t.elems.every((e) => isGroundTermInGraph(e));
2507
- if (t instanceof GraphTerm)
2508
- return t.triples.every((tr) => isGroundTripleInGraph(tr));
2509
- // Iri/Literal/Blank/Var are all OK inside formulas
2510
- return true;
2511
- }
2512
- function isGroundTripleInGraph(tr) {
2513
- return isGroundTermInGraph(tr.s) && isGroundTermInGraph(tr.p) && isGroundTermInGraph(tr.o);
2514
- }
2515
- function isGroundTerm(t) {
2516
- if (t instanceof Var)
2527
+ if (lit.indexOf('^^') >= 0)
2517
2528
  return false;
2518
- if (t instanceof ListTerm)
2519
- return t.elems.every((e) => isGroundTerm(e));
2520
- if (t instanceof OpenListTerm)
2529
+ if (!lit.startsWith('"'))
2521
2530
  return false;
2522
- if (t instanceof GraphTerm)
2523
- return t.triples.every((tr) => isGroundTripleInGraph(tr));
2524
- return true;
2531
+ if (lit.startsWith('"""')) {
2532
+ const end = lit.lastIndexOf('"""');
2533
+ if (end < 0)
2534
+ return false;
2535
+ const after = end + 3;
2536
+ return after < lit.length && lit[after] === '@';
2537
+ }
2538
+ const lastQuote = lit.lastIndexOf('"');
2539
+ if (lastQuote < 0)
2540
+ return false;
2541
+ const after = lastQuote + 1;
2542
+ return after < lit.length && lit[after] === '@';
2525
2543
  }
2526
- function isGroundTriple(tr) {
2527
- return isGroundTerm(tr.s) && isGroundTerm(tr.p) && isGroundTerm(tr.o);
2544
+ function isPlainStringLiteralValue(lit) {
2545
+ // Plain string literal: quoted, no datatype, no lang.
2546
+ if (typeof lit !== 'string')
2547
+ return false;
2548
+ if (lit.indexOf('^^') >= 0)
2549
+ return false;
2550
+ if (!isQuotedLexical(lit))
2551
+ return false;
2552
+ return !literalHasLangTag(lit);
2528
2553
  }
2529
- // Canonical JSON-ish encoding for use as a Skolem cache key.
2530
- // We only *call* this on ground terms in log:skolem, but it is
2531
- // robust to seeing vars/open lists anyway.
2532
- function skolemKeyFromTerm(t) {
2533
- function enc(u) {
2534
- if (u instanceof Iri)
2535
- return ['I', u.value];
2536
- if (u instanceof Literal)
2537
- return ['L', u.value];
2538
- if (u instanceof Blank)
2539
- return ['B', u.label];
2540
- if (u instanceof Var)
2541
- return ['V', u.name];
2542
- if (u instanceof ListTerm)
2543
- return ['List', u.elems.map(enc)];
2544
- if (u instanceof OpenListTerm)
2545
- return ['OpenList', u.prefix.map(enc), u.tailVar];
2546
- if (u instanceof GraphTerm)
2547
- return ['Graph', u.triples.map((tr) => [enc(tr.s), enc(tr.p), enc(tr.o)])];
2548
- return ['Other', String(u)];
2549
- }
2550
- return JSON.stringify(enc(t));
2554
+ function literalsEquivalentAsXsdString(aLit, bLit) {
2555
+ // Treat "abc" and "abc"^^xsd:string as equal, but do NOT conflate language-tagged strings.
2556
+ if (typeof aLit !== 'string' || typeof bLit !== 'string')
2557
+ return false;
2558
+ const [alex, adt] = literalParts(aLit);
2559
+ const [blex, bdt] = literalParts(bLit);
2560
+ if (alex !== blex)
2561
+ return false;
2562
+ const aPlain = adt === null && isPlainStringLiteralValue(aLit);
2563
+ const bPlain = bdt === null && isPlainStringLiteralValue(bLit);
2564
+ const aXsdStr = adt === XSD_NS + 'string';
2565
+ const bXsdStr = bdt === XSD_NS + 'string';
2566
+ return (aPlain && bXsdStr) || (bPlain && aXsdStr);
2551
2567
  }
2552
- function applySubstTerm(t, s) {
2553
- // Common case: variable
2554
- if (t instanceof Var) {
2555
- // Fast path: unbound variable → no change
2556
- const first = s[t.name];
2557
- if (first === undefined) {
2558
- return t;
2559
- }
2560
- // Follow chains X -> Y -> ... until we hit a non-var or a cycle.
2561
- let cur = first;
2562
- const seen = new Set([t.name]);
2563
- while (cur instanceof Var) {
2564
- const name = cur.name;
2565
- if (seen.has(name))
2566
- break; // cycle
2567
- seen.add(name);
2568
- const nxt = s[name];
2569
- if (!nxt)
2570
- break;
2571
- cur = nxt;
2572
- }
2573
- if (cur instanceof Var) {
2574
- // Still a var: keep it as is (no need to clone)
2575
- return cur;
2576
- }
2577
- // Bound to a non-var term: apply substitution recursively in case it
2578
- // contains variables inside.
2579
- return applySubstTerm(cur, s);
2568
+ function normalizeLiteralForFastKey(lit) {
2569
+ // Canonicalize so that "abc" and "abc"^^xsd:string share the same index/dedup key.
2570
+ if (typeof lit !== 'string')
2571
+ return lit;
2572
+ const [lex, dt] = literalParts(lit);
2573
+ if (dt === XSD_NS + 'string') {
2574
+ return `${lex}^^<${XSD_NS}string>`;
2580
2575
  }
2581
- // Non-variable terms
2582
- if (t instanceof ListTerm) {
2583
- return new ListTerm(t.elems.map((e) => applySubstTerm(e, s)));
2576
+ if (dt === null && isPlainStringLiteralValue(lit)) {
2577
+ return `${lex}^^<${XSD_NS}string>`;
2584
2578
  }
2585
- if (t instanceof OpenListTerm) {
2586
- const newPrefix = t.prefix.map((e) => applySubstTerm(e, s));
2587
- const tailTerm = s[t.tailVar];
2588
- if (tailTerm !== undefined) {
2589
- const tailApplied = applySubstTerm(tailTerm, s);
2590
- if (tailApplied instanceof ListTerm) {
2591
- return new ListTerm(newPrefix.concat(tailApplied.elems));
2592
- }
2593
- else if (tailApplied instanceof OpenListTerm) {
2594
- return new OpenListTerm(newPrefix.concat(tailApplied.prefix), tailApplied.tailVar);
2595
- }
2596
- else {
2597
- return new OpenListTerm(newPrefix, t.tailVar);
2598
- }
2599
- }
2600
- else {
2601
- return new OpenListTerm(newPrefix, t.tailVar);
2602
- }
2579
+ return lit;
2580
+ }
2581
+ function stripQuotes(lex) {
2582
+ if (typeof lex !== 'string')
2583
+ return lex;
2584
+ // Handle both short ('...' / "...") and long ('''...''' / """...""") forms.
2585
+ if (lex.length >= 6) {
2586
+ if (lex.startsWith('"""') && lex.endsWith('"""'))
2587
+ return lex.slice(3, -3);
2588
+ if (lex.startsWith("'''") && lex.endsWith("'''"))
2589
+ return lex.slice(3, -3);
2603
2590
  }
2604
- if (t instanceof GraphTerm) {
2605
- return new GraphTerm(t.triples.map((tr) => applySubstTriple(tr, s)));
2591
+ if (lex.length >= 2) {
2592
+ const a = lex[0];
2593
+ const b = lex[lex.length - 1];
2594
+ if ((a === '"' && b === '"') || (a === "'" && b === "'"))
2595
+ return lex.slice(1, -1);
2606
2596
  }
2607
- return t;
2608
- }
2609
- function applySubstTriple(tr, s) {
2610
- return new Triple(applySubstTerm(tr.s, s), applySubstTerm(tr.p, s), applySubstTerm(tr.o, s));
2611
- }
2612
- function iriValue(t) {
2613
- return t instanceof Iri ? t.value : null;
2597
+ return lex;
2614
2598
  }
2615
- function unifyOpenWithList(prefix, tailv, ys, subst) {
2616
- if (ys.length < prefix.length)
2599
+ function termToJsXsdStringNoLang(t) {
2600
+ // Strict xsd:string extraction *without* language tags.
2601
+ // Accept:
2602
+ // - plain string literals ("...")
2603
+ // - "..."^^xsd:string
2604
+ // Reject:
2605
+ // - language-tagged strings ("..."@en)
2606
+ // - any other datatype
2607
+ if (!(t instanceof Literal))
2617
2608
  return null;
2618
- let s2 = { ...subst };
2619
- for (let i = 0; i < prefix.length; i++) {
2620
- s2 = unifyTerm(prefix[i], ys[i], s2);
2621
- if (s2 === null)
2622
- return null;
2623
- }
2624
- const rest = new ListTerm(ys.slice(prefix.length));
2625
- s2 = unifyTerm(new Var(tailv), rest, s2);
2626
- if (s2 === null)
2609
+ if (literalHasLangTag(t.value))
2627
2610
  return null;
2628
- return s2;
2629
- }
2630
- function unifyGraphTriples(xs, ys, subst) {
2631
- if (xs.length !== ys.length)
2611
+ const [lex, dt] = literalParts(t.value);
2612
+ if (!isQuotedLexical(lex))
2632
2613
  return null;
2633
- // Fast path: exact same sequence.
2634
- if (triplesListEqual(xs, ys))
2635
- return { ...subst };
2636
- // Backtracking match (order-insensitive), *threading* the substitution through.
2637
- const used = new Array(ys.length).fill(false);
2638
- function step(i, s) {
2639
- if (i >= xs.length)
2640
- return s;
2641
- const x = xs[i];
2642
- for (let j = 0; j < ys.length; j++) {
2643
- if (used[j])
2644
- continue;
2645
- const y = ys[j];
2646
- // Cheap pruning when both predicates are IRIs.
2647
- if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
2648
- continue;
2649
- const s2 = unifyTriple(x, y, s); // IMPORTANT: use `s`, not {}
2650
- if (s2 === null)
2651
- continue;
2652
- used[j] = true;
2653
- const s3 = step(i + 1, s2);
2654
- if (s3 !== null)
2655
- return s3;
2656
- used[j] = false;
2657
- }
2614
+ if (dt !== null && dt !== XSD_NS + 'string' && dt !== 'xsd:string')
2615
+ return null;
2616
+ return decodeN3StringEscapes(stripQuotes(lex));
2617
+ }
2618
+ function termToJsString(t) {
2619
+ // Domain is xsd:string for SWAP/N3 string builtins (string:*).
2620
+ //
2621
+ // Per the N3 Builtins spec, when the domain is xsd:string we must be able to
2622
+ // cast *any* IRI or literal value (incl. numeric, boolean, dateTime, anyURI,
2623
+ // rdf:langString, and plain literals) to a string.
2624
+ //
2625
+ // We implement this as:
2626
+ // - IRI -> its IRI string
2627
+ // - Literal:
2628
+ // * quoted lexical form: decode N3/Turtle escapes and strip quotes
2629
+ // * unquoted lexical form: use as-is (e.g., 1234, true, 1971-..., 1.23E4)
2630
+ // - Everything else (blank nodes, lists, formulas, vars) -> fail
2631
+ if (t instanceof Iri)
2632
+ return t.value;
2633
+ if (!(t instanceof Literal))
2658
2634
  return null;
2635
+ const [lex, _dt] = literalParts(t.value);
2636
+ if (isQuotedLexical(lex)) {
2637
+ // Interpret N3/Turtle string escapes (\" \n \uXXXX \UXXXXXXXX …)
2638
+ // to obtain the actual string value.
2639
+ return decodeN3StringEscapes(stripQuotes(lex));
2659
2640
  }
2660
- return step(0, { ...subst }); // IMPORTANT: start from the incoming subst
2641
+ // Unquoted lexical (numbers/booleans/dateTimes, etc.)
2642
+ return typeof lex === 'string' ? lex : String(lex);
2661
2643
  }
2662
- function unifyTerm(a, b, subst) {
2663
- return unifyTermWithOptions(a, b, subst, {
2664
- boolValueEq: true,
2665
- intDecimalEq: false,
2666
- });
2644
+ function makeStringLiteral(str) {
2645
+ // JSON.stringify gives us a valid N3/Turtle-style quoted string
2646
+ // (with proper escaping for quotes, backslashes, newlines, …).
2647
+ return internLiteral(JSON.stringify(str));
2667
2648
  }
2668
- function unifyTermListAppend(a, b, subst) {
2669
- // Keep list:append behavior: allow integer<->decimal exact equality,
2670
- // but do NOT add boolean-value equivalence (preserves current semantics).
2671
- return unifyTermWithOptions(a, b, subst, {
2672
- boolValueEq: false,
2673
- intDecimalEq: true,
2674
- });
2649
+ function termToJsStringDecoded(t) {
2650
+ // Like termToJsString, but for short literals it *also* interprets escapes
2651
+ // (\" \n \uXXXX …) by attempting JSON.parse on the quoted lexical form.
2652
+ if (!(t instanceof Literal))
2653
+ return null;
2654
+ const [lex, _dt] = literalParts(t.value);
2655
+ // Long strings: """ ... """ are taken verbatim.
2656
+ if (lex.length >= 6 && lex.startsWith('"""') && lex.endsWith('"""')) {
2657
+ return lex.slice(3, -3);
2658
+ }
2659
+ // Short strings: try to decode escapes (this makes "{\"a\":1}" usable too).
2660
+ if (lex.length >= 2 && lex[0] === '"' && lex[lex.length - 1] === '"') {
2661
+ try {
2662
+ return JSON.parse(lex);
2663
+ }
2664
+ catch (e) {
2665
+ /* fall through */
2666
+ }
2667
+ return stripQuotes(lex);
2668
+ }
2669
+ return stripQuotes(lex);
2675
2670
  }
2676
- function unifyTermWithOptions(a, b, subst, opts) {
2677
- a = applySubstTerm(a, subst);
2678
- b = applySubstTerm(b, subst);
2679
- // Variable binding
2680
- if (a instanceof Var) {
2681
- const v = a.name;
2682
- const t = b;
2683
- if (t instanceof Var && t.name === v)
2684
- return { ...subst };
2685
- if (containsVarTerm(t, v))
2671
+ function jsonPointerUnescape(seg) {
2672
+ // RFC6901: ~1 -> '/', ~0 -> '~'
2673
+ let out = '';
2674
+ for (let i = 0; i < seg.length; i++) {
2675
+ const c = seg[i];
2676
+ if (c !== '~') {
2677
+ out += c;
2678
+ continue;
2679
+ }
2680
+ if (i + 1 >= seg.length)
2686
2681
  return null;
2687
- const s2 = { ...subst };
2688
- s2[v] = t;
2689
- return s2;
2690
- }
2691
- if (b instanceof Var) {
2692
- return unifyTermWithOptions(b, a, subst, opts);
2682
+ const n = seg[i + 1];
2683
+ if (n === '0')
2684
+ out += '~';
2685
+ else if (n === '1')
2686
+ out += '/';
2687
+ else
2688
+ return null;
2689
+ i++;
2693
2690
  }
2694
- // Exact matches
2695
- if (a instanceof Iri && b instanceof Iri && a.value === b.value)
2696
- return { ...subst };
2697
- if (a instanceof Literal && b instanceof Literal && a.value === b.value)
2698
- return { ...subst };
2699
- if (a instanceof Blank && b instanceof Blank && a.label === b.label)
2700
- return { ...subst };
2701
- // Plain string vs xsd:string equivalence
2702
- if (a instanceof Literal && b instanceof Literal) {
2703
- if (literalsEquivalentAsXsdString(a.value, b.value))
2704
- return { ...subst };
2691
+ return out;
2692
+ }
2693
+ function jsonToTerm(v) {
2694
+ if (v === null)
2695
+ return makeStringLiteral('null');
2696
+ if (typeof v === 'string')
2697
+ return makeStringLiteral(v);
2698
+ if (typeof v === 'number')
2699
+ return internLiteral(String(v));
2700
+ if (typeof v === 'boolean')
2701
+ return internLiteral(v ? 'true' : 'false');
2702
+ if (Array.isArray(v))
2703
+ return new ListTerm(v.map(jsonToTerm));
2704
+ if (v && typeof v === 'object') {
2705
+ return makeRdfJsonLiteral(JSON.stringify(v));
2705
2706
  }
2706
- // Boolean-value equivalence (ONLY for normal unifyTerm)
2707
- if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
2708
- const ai = parseBooleanLiteralInfo(a);
2709
- const bi = parseBooleanLiteralInfo(b);
2710
- if (ai && bi && ai.value === bi.value)
2711
- return { ...subst };
2707
+ return null;
2708
+ }
2709
+ function jsonPointerLookup(jsonText, pointer) {
2710
+ let ptr = pointer;
2711
+ // Support URI fragment form "#/a/b"
2712
+ if (ptr.startsWith('#')) {
2713
+ try {
2714
+ ptr = decodeURIComponent(ptr.slice(1));
2715
+ }
2716
+ catch {
2717
+ return null;
2718
+ }
2712
2719
  }
2713
- // Numeric-value match:
2714
- // - always allow equality when datatype matches (existing behavior)
2715
- // - optionally allow integer<->decimal exact equality (list:append only)
2716
- if (a instanceof Literal && b instanceof Literal) {
2717
- const ai = parseNumericLiteralInfo(a);
2718
- const bi = parseNumericLiteralInfo(b);
2719
- if (ai && bi) {
2720
- if (ai.dt === bi.dt) {
2721
- if (ai.kind === 'bigint' && bi.kind === 'bigint') {
2722
- if (ai.value === bi.value)
2723
- return { ...subst };
2724
- }
2725
- else {
2726
- const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
2727
- const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
2728
- if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn)
2729
- return { ...subst };
2730
- }
2731
- }
2732
- if (opts.intDecimalEq) {
2733
- const intDt = XSD_NS + 'integer';
2734
- const decDt = XSD_NS + 'decimal';
2735
- if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
2736
- const intInfo = ai.dt === intDt ? ai : bi; // bigint
2737
- const decInfo = ai.dt === decDt ? ai : bi; // number + lexStr
2738
- const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
2739
- if (dec) {
2740
- const scaledInt = intInfo.value * pow10n(dec.scale);
2741
- if (scaledInt === dec.num)
2742
- return { ...subst };
2743
- }
2744
- }
2745
- }
2720
+ let entry = jsonPointerCache.get(jsonText);
2721
+ if (!entry) {
2722
+ let parsed = null;
2723
+ try {
2724
+ parsed = JSON.parse(jsonText);
2725
+ }
2726
+ catch {
2727
+ parsed = null;
2746
2728
  }
2729
+ entry = { parsed, ptrCache: new Map() };
2730
+ jsonPointerCache.set(jsonText, entry);
2747
2731
  }
2748
- // Open list vs concrete list
2749
- if (a instanceof OpenListTerm && b instanceof ListTerm) {
2750
- return unifyOpenWithList(a.prefix, a.tailVar, b.elems, subst);
2732
+ if (entry.parsed === null)
2733
+ return null;
2734
+ if (entry.ptrCache.has(ptr))
2735
+ return entry.ptrCache.get(ptr);
2736
+ let cur = entry.parsed;
2737
+ if (ptr === '') {
2738
+ const t = jsonToTerm(cur);
2739
+ entry.ptrCache.set(ptr, t);
2740
+ return t;
2751
2741
  }
2752
- if (a instanceof ListTerm && b instanceof OpenListTerm) {
2753
- return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
2742
+ if (!ptr.startsWith('/')) {
2743
+ entry.ptrCache.set(ptr, null);
2744
+ return null;
2754
2745
  }
2755
- // Open list vs open list
2756
- if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
2757
- if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
2746
+ const parts = ptr.split('/').slice(1);
2747
+ for (const raw of parts) {
2748
+ const seg = jsonPointerUnescape(raw);
2749
+ if (seg === null) {
2750
+ entry.ptrCache.set(ptr, null);
2758
2751
  return null;
2759
- let s2 = { ...subst };
2760
- for (let i = 0; i < a.prefix.length; i++) {
2761
- s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
2762
- if (s2 === null)
2752
+ }
2753
+ if (Array.isArray(cur)) {
2754
+ if (!/^(0|[1-9]\d*)$/.test(seg)) {
2755
+ entry.ptrCache.set(ptr, null);
2756
+ return null;
2757
+ }
2758
+ const idx = Number(seg);
2759
+ if (idx < 0 || idx >= cur.length) {
2760
+ entry.ptrCache.set(ptr, null);
2763
2761
  return null;
2762
+ }
2763
+ cur = cur[idx];
2764
2764
  }
2765
- return s2;
2766
- }
2767
- // List terms
2768
- if (a instanceof ListTerm && b instanceof ListTerm) {
2769
- if (a.elems.length !== b.elems.length)
2770
- return null;
2771
- let s2 = { ...subst };
2772
- for (let i = 0; i < a.elems.length; i++) {
2773
- s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
2774
- if (s2 === null)
2765
+ else if (cur !== null && typeof cur === 'object') {
2766
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
2767
+ entry.ptrCache.set(ptr, null);
2775
2768
  return null;
2769
+ }
2770
+ cur = cur[seg];
2771
+ }
2772
+ else {
2773
+ entry.ptrCache.set(ptr, null);
2774
+ return null;
2776
2775
  }
2777
- return s2;
2778
2776
  }
2779
- // Graphs
2780
- if (a instanceof GraphTerm && b instanceof GraphTerm) {
2781
- if (alphaEqGraphTriples(a.triples, b.triples))
2782
- return { ...subst };
2783
- return unifyGraphTriples(a.triples, b.triples, subst);
2784
- }
2785
- return null;
2786
- }
2787
- function unifyTriple(pat, fact, subst) {
2788
- // Predicates are usually the cheapest and most selective
2789
- const s1 = unifyTerm(pat.p, fact.p, subst);
2790
- if (s1 === null)
2791
- return null;
2792
- const s2 = unifyTerm(pat.s, fact.s, s1);
2793
- if (s2 === null)
2794
- return null;
2795
- const s3 = unifyTerm(pat.o, fact.o, s2);
2796
- return s3;
2777
+ const out = jsonToTerm(cur);
2778
+ entry.ptrCache.set(ptr, out);
2779
+ return out;
2797
2780
  }
2798
- function composeSubst(outer, delta) {
2799
- if (!delta || Object.keys(delta).length === 0) {
2800
- return { ...outer };
2801
- }
2802
- const out = { ...outer };
2803
- for (const [k, v] of Object.entries(delta)) {
2804
- if (out.hasOwnProperty(k)) {
2805
- if (!termsEqual(out[k], v))
2806
- return null;
2807
- }
2808
- else {
2809
- out[k] = v;
2781
+ // Tiny subset of sprintf: supports only %s and %%.
2782
+ // Good enough for most N3 string:format use cases that just splice strings.
2783
+ function simpleStringFormat(fmt, args) {
2784
+ let out = '';
2785
+ let argIndex = 0;
2786
+ for (let i = 0; i < fmt.length; i++) {
2787
+ const ch = fmt[i];
2788
+ if (ch === '%' && i + 1 < fmt.length) {
2789
+ const spec = fmt[i + 1];
2790
+ if (spec === 's') {
2791
+ const arg = argIndex < args.length ? args[argIndex++] : '';
2792
+ out += arg;
2793
+ i++;
2794
+ continue;
2795
+ }
2796
+ if (spec === '%') {
2797
+ out += '%';
2798
+ i++;
2799
+ continue;
2800
+ }
2801
+ // Unsupported specifier (like %d, %f, …) ⇒ fail the builtin.
2802
+ return null;
2810
2803
  }
2804
+ out += ch;
2811
2805
  }
2812
2806
  return out;
2813
2807
  }
2814
- // ===========================================================================
2815
- // BUILTINS
2816
- // ===========================================================================
2817
- function literalParts(lit) {
2818
- const cached = __literalPartsCache.get(lit);
2819
- if (cached)
2820
- return cached;
2821
- // Split a literal into lexical form and datatype IRI (if any).
2822
- // Also strip an optional language tag from the lexical form:
2823
- // "\"hello\"@en" -> "\"hello\""
2824
- // "\"hello\"@en^^<...>" is rejected earlier in the parser.
2825
- const idx = lit.indexOf('^^');
2826
- let lex = lit;
2827
- let dt = null;
2828
- if (idx >= 0) {
2829
- lex = lit.slice(0, idx);
2830
- dt = lit.slice(idx + 2).trim();
2831
- if (dt.startsWith('<') && dt.endsWith('>')) {
2832
- dt = dt.slice(1, -1);
2833
- }
2808
+ // -----------------------------------------------------------------------------
2809
+ // SWAP/N3 regex compatibility helper
2810
+ // -----------------------------------------------------------------------------
2811
+ function regexNeedsUnicodeMode(pattern) {
2812
+ // JS requires /u for Unicode property escapes and code point escapes.
2813
+ return /\\[pP]\{/.test(pattern) || /\\u\{/.test(pattern);
2814
+ }
2815
+ function sanitizeForUnicodeMode(pattern) {
2816
+ // In JS Unicode mode, “identity escapes” like \! are a SyntaxError.
2817
+ // In Perl-ish regexes they commonly mean “literal !”. So drop the redundant "\".
2818
+ // Keep escapes that are regex-syntax or are commonly needed in char classes.
2819
+ const KEEP = '^$\\.*+?()[]{}|/-';
2820
+ return pattern.replace(/\\([^A-Za-z0-9])/g, (m, ch) => {
2821
+ return KEEP.includes(ch) ? m : ch;
2822
+ });
2823
+ }
2824
+ function compileSwapRegex(pattern, extraFlags) {
2825
+ const needU = regexNeedsUnicodeMode(pattern);
2826
+ const flags = (extraFlags || '') + (needU ? 'u' : '');
2827
+ try {
2828
+ return new RegExp(pattern, flags);
2834
2829
  }
2835
- // Strip LANGTAG from the lexical form when present.
2836
- if (lex.length >= 2 && lex[0] === '"') {
2837
- const lastQuote = lex.lastIndexOf('"');
2838
- if (lastQuote > 0 && lastQuote < lex.length - 1 && lex[lastQuote + 1] === '@') {
2839
- const lang = lex.slice(lastQuote + 2);
2840
- if (/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) {
2841
- lex = lex.slice(0, lastQuote + 1);
2830
+ catch (e) {
2831
+ if (needU) {
2832
+ const p2 = sanitizeForUnicodeMode(pattern);
2833
+ if (p2 !== pattern) {
2834
+ try {
2835
+ return new RegExp(p2, flags);
2836
+ }
2837
+ catch (_e2) { }
2842
2838
  }
2843
2839
  }
2840
+ return null;
2844
2841
  }
2845
- const res = [lex, dt];
2846
- __literalPartsCache.set(lit, res);
2847
- return res;
2848
2842
  }
2849
- function literalHasLangTag(lit) {
2850
- // True iff the literal is a quoted string literal with a language tag suffix,
2851
- // e.g. "hello"@en or """hello"""@en.
2852
- // (The parser rejects language tags combined with datatypes.)
2853
- if (typeof lit !== 'string')
2854
- return false;
2855
- if (lit.indexOf('^^') >= 0)
2856
- return false;
2857
- if (!lit.startsWith('"'))
2858
- return false;
2859
- if (lit.startsWith('"""')) {
2860
- const end = lit.lastIndexOf('"""');
2861
- if (end < 0)
2862
- return false;
2863
- const after = end + 3;
2864
- return after < lit.length && lit[after] === '@';
2843
+ // -----------------------------------------------------------------------------
2844
+ // Strict numeric literal parsing for math: builtins
2845
+ // -----------------------------------------------------------------------------
2846
+ const XSD_DECIMAL_DT = XSD_NS + 'decimal';
2847
+ const XSD_DOUBLE_DT = XSD_NS + 'double';
2848
+ const XSD_FLOAT_DT = XSD_NS + 'float';
2849
+ const XSD_INTEGER_DT = XSD_NS + 'integer';
2850
+ // Integer-derived datatypes from XML Schema Part 2 (and commonly used ones).
2851
+ const XSD_INTEGER_DERIVED_DTS = new Set([
2852
+ XSD_INTEGER_DT,
2853
+ XSD_NS + 'nonPositiveInteger',
2854
+ XSD_NS + 'negativeInteger',
2855
+ XSD_NS + 'long',
2856
+ XSD_NS + 'int',
2857
+ XSD_NS + 'short',
2858
+ XSD_NS + 'byte',
2859
+ XSD_NS + 'nonNegativeInteger',
2860
+ XSD_NS + 'unsignedLong',
2861
+ XSD_NS + 'unsignedInt',
2862
+ XSD_NS + 'unsignedShort',
2863
+ XSD_NS + 'unsignedByte',
2864
+ XSD_NS + 'positiveInteger',
2865
+ ]);
2866
+ function parseBooleanLiteralInfo(t) {
2867
+ if (!(t instanceof Literal))
2868
+ return null;
2869
+ const boolDt = XSD_NS + 'boolean';
2870
+ const v = t.value;
2871
+ const [lex, dt] = literalParts(v);
2872
+ // Typed xsd:boolean: accept "true"/"false"/"1"/"0"
2873
+ if (dt !== null) {
2874
+ if (dt !== boolDt)
2875
+ return null;
2876
+ const s = stripQuotes(lex);
2877
+ if (s === 'true' || s === '1')
2878
+ return { dt: boolDt, value: true };
2879
+ if (s === 'false' || s === '0')
2880
+ return { dt: boolDt, value: false };
2881
+ return null;
2865
2882
  }
2866
- const lastQuote = lit.lastIndexOf('"');
2867
- if (lastQuote < 0)
2868
- return false;
2869
- const after = lastQuote + 1;
2870
- return after < lit.length && lit[after] === '@';
2883
+ // Untyped boolean token: true/false
2884
+ if (v === 'true')
2885
+ return { dt: boolDt, value: true };
2886
+ if (v === 'false')
2887
+ return { dt: boolDt, value: false };
2888
+ return null;
2871
2889
  }
2872
- function isPlainStringLiteralValue(lit) {
2873
- // Plain string literal: quoted, no datatype, no lang.
2874
- if (typeof lit !== 'string')
2875
- return false;
2876
- if (lit.indexOf('^^') >= 0)
2877
- return false;
2878
- if (!isQuotedLexical(lit))
2879
- return false;
2880
- return !literalHasLangTag(lit);
2890
+ function parseXsdFloatSpecialLex(s) {
2891
+ if (s === 'INF' || s === '+INF')
2892
+ return Infinity;
2893
+ if (s === '-INF')
2894
+ return -Infinity;
2895
+ if (s === 'NaN')
2896
+ return NaN;
2897
+ return null;
2881
2898
  }
2882
- function literalsEquivalentAsXsdString(aLit, bLit) {
2883
- // Treat "abc" and "abc"^^xsd:string as equal, but do NOT conflate language-tagged strings.
2884
- if (typeof aLit !== 'string' || typeof bLit !== 'string')
2899
+ // ===========================================================================
2900
+ // Math builtin helpers
2901
+ // ===========================================================================
2902
+ function formatXsdFloatSpecialLex(n) {
2903
+ if (n === Infinity)
2904
+ return 'INF';
2905
+ if (n === -Infinity)
2906
+ return '-INF';
2907
+ if (Number.isNaN(n))
2908
+ return 'NaN';
2909
+ return null;
2910
+ }
2911
+ function isQuotedLexical(lex) {
2912
+ // Accept both Turtle/N3 quoting styles:
2913
+ // short: "..." or '...'
2914
+ // long: """...""" or '''...'''
2915
+ if (typeof lex !== 'string')
2885
2916
  return false;
2886
- const [alex, adt] = literalParts(aLit);
2887
- const [blex, bdt] = literalParts(bLit);
2888
- if (alex !== blex)
2917
+ const n = lex.length;
2918
+ if (n >= 6 && ((lex.startsWith('"""') && lex.endsWith('"""')) || (lex.startsWith("'''") && lex.endsWith("'''"))))
2919
+ return true;
2920
+ if (n >= 2) {
2921
+ const a = lex[0];
2922
+ const b = lex[n - 1];
2923
+ return (a === '"' && b === '"') || (a === "'" && b === "'");
2924
+ }
2925
+ return false;
2926
+ }
2927
+ function isXsdNumericDatatype(dt) {
2928
+ if (dt === null)
2889
2929
  return false;
2890
- const aPlain = adt === null && isPlainStringLiteralValue(aLit);
2891
- const bPlain = bdt === null && isPlainStringLiteralValue(bLit);
2892
- const aXsdStr = adt === XSD_NS + 'string';
2893
- const bXsdStr = bdt === XSD_NS + 'string';
2894
- return (aPlain && bXsdStr) || (bPlain && aXsdStr);
2930
+ return dt === XSD_DECIMAL_DT || dt === XSD_DOUBLE_DT || dt === XSD_FLOAT_DT || XSD_INTEGER_DERIVED_DTS.has(dt);
2895
2931
  }
2896
- function normalizeLiteralForFastKey(lit) {
2897
- // Canonicalize so that "abc" and "abc"^^xsd:string share the same index/dedup key.
2898
- if (typeof lit !== 'string')
2899
- return lit;
2900
- const [lex, dt] = literalParts(lit);
2901
- if (dt === XSD_NS + 'string') {
2902
- return `${lex}^^<${XSD_NS}string>`;
2903
- }
2904
- if (dt === null && isPlainStringLiteralValue(lit)) {
2905
- return `${lex}^^<${XSD_NS}string>`;
2906
- }
2907
- return lit;
2932
+ function isXsdIntegerDatatype(dt) {
2933
+ if (dt === null)
2934
+ return false;
2935
+ return XSD_INTEGER_DERIVED_DTS.has(dt);
2908
2936
  }
2909
- function stripQuotes(lex) {
2910
- if (typeof lex !== 'string')
2911
- return lex;
2912
- // Handle both short ('...' / "...") and long ('''...''' / """...""") forms.
2913
- if (lex.length >= 6) {
2914
- if (lex.startsWith('"""') && lex.endsWith('"""'))
2915
- return lex.slice(3, -3);
2916
- if (lex.startsWith("'''") && lex.endsWith("'''"))
2917
- return lex.slice(3, -3);
2918
- }
2919
- if (lex.length >= 2) {
2920
- const a = lex[0];
2921
- const b = lex[lex.length - 1];
2922
- if ((a === '"' && b === '"') || (a === "'" && b === "'"))
2923
- return lex.slice(1, -1);
2924
- }
2925
- return lex;
2937
+ function looksLikeUntypedNumericTokenLex(lex) {
2938
+ // We only treat *unquoted* tokens as "untyped numeric" (Turtle/N3 numeric literal).
2939
+ // Quoted literals without datatype are strings, never numbers.
2940
+ if (isQuotedLexical(lex))
2941
+ return false;
2942
+ // integer
2943
+ if (/^[+-]?\d+$/.test(lex))
2944
+ return true;
2945
+ // decimal (no exponent)
2946
+ if (/^[+-]?(?:\d+\.\d*|\.\d+)$/.test(lex))
2947
+ return true;
2948
+ // double (with exponent)
2949
+ if (/^[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)$/.test(lex))
2950
+ return true;
2951
+ return false;
2926
2952
  }
2927
- function termToJsXsdStringNoLang(t) {
2928
- // Strict xsd:string extraction *without* language tags.
2929
- // Accept:
2930
- // - plain string literals ("...")
2931
- // - "..."^^xsd:string
2932
- // Reject:
2933
- // - language-tagged strings ("..."@en)
2934
- // - any other datatype
2953
+ function parseNum(t) {
2954
+ // Parse as JS Number, but ONLY for xsd numeric datatypes or untyped numeric tokens.
2955
+ // For xsd:float/xsd:double, accept INF/-INF/NaN (and +INF).
2935
2956
  if (!(t instanceof Literal))
2936
2957
  return null;
2937
- if (literalHasLangTag(t.value))
2938
- return null;
2939
- const [lex, dt] = literalParts(t.value);
2940
- if (!isQuotedLexical(lex))
2941
- return null;
2942
- if (dt !== null && dt !== XSD_NS + 'string' && dt !== 'xsd:string')
2958
+ const key = t.value;
2959
+ if (__parseNumCache.has(key))
2960
+ return __parseNumCache.get(key);
2961
+ const [lex, dt] = literalParts(key);
2962
+ // Typed literals: must be xsd numeric.
2963
+ if (dt !== null) {
2964
+ if (!isXsdNumericDatatype(dt)) {
2965
+ __parseNumCache.set(key, null);
2966
+ return null;
2967
+ }
2968
+ const val = stripQuotes(lex);
2969
+ // float/double: allow INF/-INF/NaN and allow +/-Infinity results
2970
+ if (dt === XSD_FLOAT_DT || dt === XSD_DOUBLE_DT) {
2971
+ const sp = parseXsdFloatSpecialLex(val);
2972
+ if (sp !== null) {
2973
+ __parseNumCache.set(key, sp);
2974
+ return sp;
2975
+ }
2976
+ const n = Number(val);
2977
+ if (Number.isNaN(n)) {
2978
+ __parseNumCache.set(key, null);
2979
+ return null;
2980
+ }
2981
+ __parseNumCache.set(key, n);
2982
+ return n; // may be finite, +/-Infinity
2983
+ }
2984
+ // decimal/integer-derived: keep strict finite parsing
2985
+ const n = Number(val);
2986
+ if (!Number.isFinite(n)) {
2987
+ __parseNumCache.set(key, null);
2988
+ return null;
2989
+ }
2990
+ __parseNumCache.set(key, n);
2991
+ return n;
2992
+ }
2993
+ // Untyped literals: accept only unquoted numeric tokens.
2994
+ if (!looksLikeUntypedNumericTokenLex(lex)) {
2995
+ __parseNumCache.set(key, null);
2943
2996
  return null;
2944
- return decodeN3StringEscapes(stripQuotes(lex));
2945
- }
2946
- function termToJsString(t) {
2947
- // Domain is xsd:string for SWAP/N3 string builtins (string:*).
2948
- //
2949
- // Per the N3 Builtins spec, when the domain is xsd:string we must be able to
2950
- // cast *any* IRI or literal value (incl. numeric, boolean, dateTime, anyURI,
2951
- // rdf:langString, and plain literals) to a string.
2952
- //
2953
- // We implement this as:
2954
- // - IRI -> its IRI string
2955
- // - Literal:
2956
- // * quoted lexical form: decode N3/Turtle escapes and strip quotes
2957
- // * unquoted lexical form: use as-is (e.g., 1234, true, 1971-..., 1.23E4)
2958
- // - Everything else (blank nodes, lists, formulas, vars) -> fail
2959
- if (t instanceof Iri)
2960
- return t.value;
2961
- if (!(t instanceof Literal))
2997
+ }
2998
+ const n = Number(lex);
2999
+ if (!Number.isFinite(n)) {
3000
+ __parseNumCache.set(key, null);
2962
3001
  return null;
2963
- const [lex, _dt] = literalParts(t.value);
2964
- if (isQuotedLexical(lex)) {
2965
- // Interpret N3/Turtle string escapes (\" \n \uXXXX \UXXXXXXXX …)
2966
- // to obtain the actual string value.
2967
- return decodeN3StringEscapes(stripQuotes(lex));
2968
3002
  }
2969
- // Unquoted lexical (numbers/booleans/dateTimes, etc.)
2970
- return typeof lex === 'string' ? lex : String(lex);
2971
- }
2972
- function makeStringLiteral(str) {
2973
- // JSON.stringify gives us a valid N3/Turtle-style quoted string
2974
- // (with proper escaping for quotes, backslashes, newlines, …).
2975
- return internLiteral(JSON.stringify(str));
3003
+ __parseNumCache.set(key, n);
3004
+ return n;
2976
3005
  }
2977
- function termToJsStringDecoded(t) {
2978
- // Like termToJsString, but for short literals it *also* interprets escapes
2979
- // (\" \n \uXXXX …) by attempting JSON.parse on the quoted lexical form.
3006
+ function parseIntLiteral(t) {
3007
+ // Parse as BigInt if (and only if) it is an integer literal in an integer datatype,
3008
+ // or an untyped integer token.
2980
3009
  if (!(t instanceof Literal))
2981
3010
  return null;
2982
- const [lex, _dt] = literalParts(t.value);
2983
- // Long strings: """ ... """ are taken verbatim.
2984
- if (lex.length >= 6 && lex.startsWith('"""') && lex.endsWith('"""')) {
2985
- return lex.slice(3, -3);
2986
- }
2987
- // Short strings: try to decode escapes (this makes "{\"a\":1}" usable too).
2988
- if (lex.length >= 2 && lex[0] === '"' && lex[lex.length - 1] === '"') {
2989
- try {
2990
- return JSON.parse(lex);
2991
- }
2992
- catch (e) {
2993
- /* fall through */
2994
- }
2995
- return stripQuotes(lex);
2996
- }
2997
- return stripQuotes(lex);
2998
- }
2999
- function jsonPointerUnescape(seg) {
3000
- // RFC6901: ~1 -> '/', ~0 -> '~'
3001
- let out = '';
3002
- for (let i = 0; i < seg.length; i++) {
3003
- const c = seg[i];
3004
- if (c !== '~') {
3005
- out += c;
3006
- continue;
3007
- }
3008
- if (i + 1 >= seg.length)
3009
- return null;
3010
- const n = seg[i + 1];
3011
- if (n === '0')
3012
- out += '~';
3013
- else if (n === '1')
3014
- out += '/';
3015
- else
3011
+ const key = t.value;
3012
+ if (__parseIntCache.has(key))
3013
+ return __parseIntCache.get(key);
3014
+ const [lex, dt] = literalParts(key);
3015
+ if (dt !== null) {
3016
+ if (!isXsdIntegerDatatype(dt)) {
3017
+ __parseIntCache.set(key, null);
3016
3018
  return null;
3017
- i++;
3018
- }
3019
- return out;
3020
- }
3021
- function jsonToTerm(v) {
3022
- if (v === null)
3023
- return makeStringLiteral('null');
3024
- if (typeof v === 'string')
3025
- return makeStringLiteral(v);
3026
- if (typeof v === 'number')
3027
- return internLiteral(String(v));
3028
- if (typeof v === 'boolean')
3029
- return internLiteral(v ? 'true' : 'false');
3030
- if (Array.isArray(v))
3031
- return new ListTerm(v.map(jsonToTerm));
3032
- if (v && typeof v === 'object') {
3033
- return makeRdfJsonLiteral(JSON.stringify(v));
3034
- }
3035
- return null;
3036
- }
3037
- function jsonPointerLookup(jsonText, pointer) {
3038
- let ptr = pointer;
3039
- // Support URI fragment form "#/a/b"
3040
- if (ptr.startsWith('#')) {
3041
- try {
3042
- ptr = decodeURIComponent(ptr.slice(1));
3043
3019
  }
3044
- catch {
3020
+ const val = stripQuotes(lex);
3021
+ if (!/^[+-]?\d+$/.test(val)) {
3022
+ __parseIntCache.set(key, null);
3045
3023
  return null;
3046
3024
  }
3047
- }
3048
- let entry = jsonPointerCache.get(jsonText);
3049
- if (!entry) {
3050
- let parsed = null;
3051
3025
  try {
3052
- parsed = JSON.parse(jsonText);
3026
+ const out = BigInt(val);
3027
+ __parseIntCache.set(key, out);
3028
+ return out;
3053
3029
  }
3054
3030
  catch {
3055
- parsed = null;
3031
+ __parseIntCache.set(key, null);
3032
+ return null;
3056
3033
  }
3057
- entry = { parsed, ptrCache: new Map() };
3058
- jsonPointerCache.set(jsonText, entry);
3059
3034
  }
3060
- if (entry.parsed === null)
3035
+ // Untyped: only accept unquoted integer tokens.
3036
+ if (isQuotedLexical(lex)) {
3037
+ __parseIntCache.set(key, null);
3061
3038
  return null;
3062
- if (entry.ptrCache.has(ptr))
3063
- return entry.ptrCache.get(ptr);
3064
- let cur = entry.parsed;
3065
- if (ptr === '') {
3066
- const t = jsonToTerm(cur);
3067
- entry.ptrCache.set(ptr, t);
3068
- return t;
3069
3039
  }
3070
- if (!ptr.startsWith('/')) {
3071
- entry.ptrCache.set(ptr, null);
3040
+ if (!/^[+-]?\d+$/.test(lex)) {
3041
+ __parseIntCache.set(key, null);
3072
3042
  return null;
3073
3043
  }
3074
- const parts = ptr.split('/').slice(1);
3075
- for (const raw of parts) {
3076
- const seg = jsonPointerUnescape(raw);
3077
- if (seg === null) {
3078
- entry.ptrCache.set(ptr, null);
3079
- return null;
3080
- }
3081
- if (Array.isArray(cur)) {
3082
- if (!/^(0|[1-9]\d*)$/.test(seg)) {
3083
- entry.ptrCache.set(ptr, null);
3084
- return null;
3085
- }
3086
- const idx = Number(seg);
3087
- if (idx < 0 || idx >= cur.length) {
3088
- entry.ptrCache.set(ptr, null);
3089
- return null;
3090
- }
3091
- cur = cur[idx];
3092
- }
3093
- else if (cur !== null && typeof cur === 'object') {
3094
- if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
3095
- entry.ptrCache.set(ptr, null);
3096
- return null;
3097
- }
3098
- cur = cur[seg];
3099
- }
3100
- else {
3101
- entry.ptrCache.set(ptr, null);
3102
- return null;
3103
- }
3044
+ try {
3045
+ const out = BigInt(lex);
3046
+ __parseIntCache.set(key, out);
3047
+ return out;
3104
3048
  }
3105
- const out = jsonToTerm(cur);
3106
- entry.ptrCache.set(ptr, out);
3107
- return out;
3108
- }
3109
- // Tiny subset of sprintf: supports only %s and %%.
3110
- // Good enough for most N3 string:format use cases that just splice strings.
3111
- function simpleStringFormat(fmt, args) {
3112
- let out = '';
3113
- let argIndex = 0;
3114
- for (let i = 0; i < fmt.length; i++) {
3115
- const ch = fmt[i];
3116
- if (ch === '%' && i + 1 < fmt.length) {
3117
- const spec = fmt[i + 1];
3118
- if (spec === 's') {
3119
- const arg = argIndex < args.length ? args[argIndex++] : '';
3120
- out += arg;
3121
- i++;
3122
- continue;
3123
- }
3124
- if (spec === '%') {
3125
- out += '%';
3126
- i++;
3127
- continue;
3128
- }
3129
- // Unsupported specifier (like %d, %f, …) ⇒ fail the builtin.
3130
- return null;
3131
- }
3132
- out += ch;
3049
+ catch {
3050
+ __parseIntCache.set(key, null);
3051
+ return null;
3133
3052
  }
3134
- return out;
3135
- }
3136
- // -----------------------------------------------------------------------------
3137
- // SWAP/N3 regex compatibility helper
3138
- // -----------------------------------------------------------------------------
3139
- function regexNeedsUnicodeMode(pattern) {
3140
- // JS requires /u for Unicode property escapes and code point escapes.
3141
- return /\\[pP]\{/.test(pattern) || /\\u\{/.test(pattern);
3142
3053
  }
3143
- function sanitizeForUnicodeMode(pattern) {
3144
- // In JS Unicode mode, “identity escapes” like \! are a SyntaxError.
3145
- // In Perl-ish regexes they commonly mean “literal !”. So drop the redundant "\".
3146
- // Keep escapes that are regex-syntax or are commonly needed in char classes.
3147
- const KEEP = '^$\\.*+?()[]{}|/-';
3148
- return pattern.replace(/\\([^A-Za-z0-9])/g, (m, ch) => {
3149
- return KEEP.includes(ch) ? m : ch;
3150
- });
3054
+ function formatNum(n) {
3055
+ return String(n);
3151
3056
  }
3152
- function compileSwapRegex(pattern, extraFlags) {
3153
- const needU = regexNeedsUnicodeMode(pattern);
3154
- const flags = (extraFlags || '') + (needU ? 'u' : '');
3155
- try {
3156
- return new RegExp(pattern, flags);
3057
+ function parseXsdDecimalToBigIntScale(s) {
3058
+ let t = String(s).trim();
3059
+ let sign = 1n;
3060
+ if (t.startsWith('+'))
3061
+ t = t.slice(1);
3062
+ else if (t.startsWith('-')) {
3063
+ sign = -1n;
3064
+ t = t.slice(1);
3157
3065
  }
3158
- catch (e) {
3159
- if (needU) {
3160
- const p2 = sanitizeForUnicodeMode(pattern);
3161
- if (p2 !== pattern) {
3162
- try {
3163
- return new RegExp(p2, flags);
3164
- }
3165
- catch (_e2) { }
3166
- }
3167
- }
3066
+ // xsd:decimal lexical: (\d+(\.\d*)?|\.\d+)
3067
+ if (!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(t))
3168
3068
  return null;
3069
+ let intPart = '0';
3070
+ let fracPart = '';
3071
+ if (t.includes('.')) {
3072
+ const parts = t.split('.');
3073
+ intPart = parts[0] === '' ? '0' : parts[0];
3074
+ fracPart = parts[1] || '';
3169
3075
  }
3170
- }
3171
- // -----------------------------------------------------------------------------
3172
- // Strict numeric literal parsing for math: builtins
3173
- // -----------------------------------------------------------------------------
3174
- const XSD_DECIMAL_DT = XSD_NS + 'decimal';
3175
- const XSD_DOUBLE_DT = XSD_NS + 'double';
3176
- const XSD_FLOAT_DT = XSD_NS + 'float';
3177
- const XSD_INTEGER_DT = XSD_NS + 'integer';
3178
- // Integer-derived datatypes from XML Schema Part 2 (and commonly used ones).
3179
- const XSD_INTEGER_DERIVED_DTS = new Set([
3180
- XSD_INTEGER_DT,
3181
- XSD_NS + 'nonPositiveInteger',
3182
- XSD_NS + 'negativeInteger',
3183
- XSD_NS + 'long',
3184
- XSD_NS + 'int',
3185
- XSD_NS + 'short',
3186
- XSD_NS + 'byte',
3187
- XSD_NS + 'nonNegativeInteger',
3188
- XSD_NS + 'unsignedLong',
3189
- XSD_NS + 'unsignedInt',
3190
- XSD_NS + 'unsignedShort',
3191
- XSD_NS + 'unsignedByte',
3192
- XSD_NS + 'positiveInteger',
3193
- ]);
3194
- function parseBooleanLiteralInfo(t) {
3195
- if (!(t instanceof Literal))
3196
- return null;
3197
- const boolDt = XSD_NS + 'boolean';
3198
- const v = t.value;
3199
- const [lex, dt] = literalParts(v);
3200
- // Typed xsd:boolean: accept "true"/"false"/"1"/"0"
3201
- if (dt !== null) {
3202
- if (dt !== boolDt)
3203
- return null;
3204
- const s = stripQuotes(lex);
3205
- if (s === 'true' || s === '1')
3206
- return { dt: boolDt, value: true };
3207
- if (s === 'false' || s === '0')
3208
- return { dt: boolDt, value: false };
3209
- return null;
3076
+ else {
3077
+ intPart = t;
3210
3078
  }
3211
- // Untyped boolean token: true/false
3212
- if (v === 'true')
3213
- return { dt: boolDt, value: true };
3214
- if (v === 'false')
3215
- return { dt: boolDt, value: false };
3216
- return null;
3079
+ // normalize
3080
+ intPart = intPart.replace(/^0+(?=\d)/, '');
3081
+ fracPart = fracPart.replace(/0+$/, ''); // drop trailing zeros
3082
+ const scale = fracPart.length;
3083
+ const digits = intPart + fracPart || '0';
3084
+ return { num: sign * BigInt(digits), scale };
3217
3085
  }
3218
- function parseXsdFloatSpecialLex(s) {
3219
- if (s === 'INF' || s === '+INF')
3220
- return Infinity;
3221
- if (s === '-INF')
3222
- return -Infinity;
3223
- if (s === 'NaN')
3224
- return NaN;
3225
- return null;
3086
+ function pow10n(k) {
3087
+ return 10n ** BigInt(k);
3226
3088
  }
3227
3089
  // ===========================================================================
3228
- // Math builtin helpers
3090
+ // Time & duration builtin helpers
3229
3091
  // ===========================================================================
3230
- function formatXsdFloatSpecialLex(n) {
3231
- if (n === Infinity)
3232
- return 'INF';
3233
- if (n === -Infinity)
3234
- return '-INF';
3235
- if (Number.isNaN(n))
3236
- return 'NaN';
3237
- return null;
3238
- }
3239
- function isQuotedLexical(lex) {
3240
- // Accept both Turtle/N3 quoting styles:
3241
- // short: "..." or '...'
3242
- // long: """...""" or '''...'''
3243
- if (typeof lex !== 'string')
3244
- return false;
3245
- const n = lex.length;
3246
- if (n >= 6 && ((lex.startsWith('"""') && lex.endsWith('"""')) || (lex.startsWith("'''") && lex.endsWith("'''"))))
3247
- return true;
3248
- if (n >= 2) {
3249
- const a = lex[0];
3250
- const b = lex[n - 1];
3251
- return (a === '"' && b === '"') || (a === "'" && b === "'");
3252
- }
3253
- return false;
3254
- }
3255
- function isXsdNumericDatatype(dt) {
3256
- if (dt === null)
3257
- return false;
3258
- return dt === XSD_DECIMAL_DT || dt === XSD_DOUBLE_DT || dt === XSD_FLOAT_DT || XSD_INTEGER_DERIVED_DTS.has(dt);
3259
- }
3260
- function isXsdIntegerDatatype(dt) {
3261
- if (dt === null)
3262
- return false;
3263
- return XSD_INTEGER_DERIVED_DTS.has(dt);
3264
- }
3265
- function looksLikeUntypedNumericTokenLex(lex) {
3266
- // We only treat *unquoted* tokens as "untyped numeric" (Turtle/N3 numeric literal).
3267
- // Quoted literals without datatype are strings, never numbers.
3268
- if (isQuotedLexical(lex))
3269
- return false;
3270
- // integer
3271
- if (/^[+-]?\d+$/.test(lex))
3272
- return true;
3273
- // decimal (no exponent)
3274
- if (/^[+-]?(?:\d+\.\d*|\.\d+)$/.test(lex))
3275
- return true;
3276
- // double (with exponent)
3277
- if (/^[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)$/.test(lex))
3278
- return true;
3279
- return false;
3280
- }
3281
- function parseNum(t) {
3282
- // Parse as JS Number, but ONLY for xsd numeric datatypes or untyped numeric tokens.
3283
- // For xsd:float/xsd:double, accept INF/-INF/NaN (and +INF).
3092
+ function parseXsdDateTerm(t) {
3284
3093
  if (!(t instanceof Literal))
3285
3094
  return null;
3286
- const key = t.value;
3287
- if (__parseNumCache.has(key))
3288
- return __parseNumCache.get(key);
3289
- const [lex, dt] = literalParts(key);
3290
- // Typed literals: must be xsd numeric.
3291
- if (dt !== null) {
3292
- if (!isXsdNumericDatatype(dt)) {
3293
- __parseNumCache.set(key, null);
3294
- return null;
3295
- }
3296
- const val = stripQuotes(lex);
3297
- // float/double: allow INF/-INF/NaN and allow +/-Infinity results
3298
- if (dt === XSD_FLOAT_DT || dt === XSD_DOUBLE_DT) {
3299
- const sp = parseXsdFloatSpecialLex(val);
3300
- if (sp !== null) {
3301
- __parseNumCache.set(key, sp);
3302
- return sp;
3303
- }
3304
- const n = Number(val);
3305
- if (Number.isNaN(n)) {
3306
- __parseNumCache.set(key, null);
3307
- return null;
3308
- }
3309
- __parseNumCache.set(key, n);
3310
- return n; // may be finite, +/-Infinity
3311
- }
3312
- // decimal/integer-derived: keep strict finite parsing
3313
- const n = Number(val);
3314
- if (!Number.isFinite(n)) {
3315
- __parseNumCache.set(key, null);
3316
- return null;
3317
- }
3318
- __parseNumCache.set(key, n);
3319
- return n;
3320
- }
3321
- // Untyped literals: accept only unquoted numeric tokens.
3322
- if (!looksLikeUntypedNumericTokenLex(lex)) {
3323
- __parseNumCache.set(key, null);
3095
+ const [lex, dt] = literalParts(t.value);
3096
+ if (dt !== XSD_NS + 'date')
3324
3097
  return null;
3325
- }
3326
- const n = Number(lex);
3327
- if (!Number.isFinite(n)) {
3328
- __parseNumCache.set(key, null);
3329
- return null;
3330
- }
3331
- __parseNumCache.set(key, n);
3332
- return n;
3333
- }
3334
- function parseIntLiteral(t) {
3335
- // Parse as BigInt if (and only if) it is an integer literal in an integer datatype,
3336
- // or an untyped integer token.
3337
- if (!(t instanceof Literal))
3338
- return null;
3339
- const key = t.value;
3340
- if (__parseIntCache.has(key))
3341
- return __parseIntCache.get(key);
3342
- const [lex, dt] = literalParts(key);
3343
- if (dt !== null) {
3344
- if (!isXsdIntegerDatatype(dt)) {
3345
- __parseIntCache.set(key, null);
3346
- return null;
3347
- }
3348
- const val = stripQuotes(lex);
3349
- if (!/^[+-]?\d+$/.test(val)) {
3350
- __parseIntCache.set(key, null);
3351
- return null;
3352
- }
3353
- try {
3354
- const out = BigInt(val);
3355
- __parseIntCache.set(key, out);
3356
- return out;
3357
- }
3358
- catch {
3359
- __parseIntCache.set(key, null);
3360
- return null;
3361
- }
3362
- }
3363
- // Untyped: only accept unquoted integer tokens.
3364
- if (isQuotedLexical(lex)) {
3365
- __parseIntCache.set(key, null);
3366
- return null;
3367
- }
3368
- if (!/^[+-]?\d+$/.test(lex)) {
3369
- __parseIntCache.set(key, null);
3370
- return null;
3371
- }
3372
- try {
3373
- const out = BigInt(lex);
3374
- __parseIntCache.set(key, out);
3375
- return out;
3376
- }
3377
- catch {
3378
- __parseIntCache.set(key, null);
3379
- return null;
3380
- }
3381
- }
3382
- function formatNum(n) {
3383
- return String(n);
3384
- }
3385
- function parseXsdDecimalToBigIntScale(s) {
3386
- let t = String(s).trim();
3387
- let sign = 1n;
3388
- if (t.startsWith('+'))
3389
- t = t.slice(1);
3390
- else if (t.startsWith('-')) {
3391
- sign = -1n;
3392
- t = t.slice(1);
3393
- }
3394
- // xsd:decimal lexical: (\d+(\.\d*)?|\.\d+)
3395
- if (!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(t))
3396
- return null;
3397
- let intPart = '0';
3398
- let fracPart = '';
3399
- if (t.includes('.')) {
3400
- const parts = t.split('.');
3401
- intPart = parts[0] === '' ? '0' : parts[0];
3402
- fracPart = parts[1] || '';
3403
- }
3404
- else {
3405
- intPart = t;
3406
- }
3407
- // normalize
3408
- intPart = intPart.replace(/^0+(?=\d)/, '');
3409
- fracPart = fracPart.replace(/0+$/, ''); // drop trailing zeros
3410
- const scale = fracPart.length;
3411
- const digits = intPart + fracPart || '0';
3412
- return { num: sign * BigInt(digits), scale };
3413
- }
3414
- function pow10n(k) {
3415
- return 10n ** BigInt(k);
3416
- }
3417
- // ===========================================================================
3418
- // Time & duration builtin helpers
3419
- // ===========================================================================
3420
- function parseXsdDateTerm(t) {
3421
- if (!(t instanceof Literal))
3422
- return null;
3423
- const [lex, dt] = literalParts(t.value);
3424
- if (dt !== XSD_NS + 'date')
3425
- return null;
3426
- const val = stripQuotes(lex);
3427
- const d = new Date(val + 'T00:00:00Z');
3428
- if (Number.isNaN(d.getTime()))
3098
+ const val = stripQuotes(lex);
3099
+ const d = new Date(val + 'T00:00:00Z');
3100
+ if (Number.isNaN(d.getTime()))
3429
3101
  return null;
3430
3102
  return d;
3431
3103
  }
@@ -6094,6 +5766,336 @@ function isBuiltinPred(p) {
6094
5766
  v.startsWith(TIME_NS) ||
6095
5767
  v.startsWith(LIST_NS));
6096
5768
  }
5769
+ // @ts-nocheck
5770
+ /* eslint-disable */
5771
+ // ===========================================================================
5772
+ // Unification + substitution
5773
+ // ===========================================================================
5774
+ function containsVarTerm(t, v) {
5775
+ if (t instanceof Var)
5776
+ return t.name === v;
5777
+ if (t instanceof ListTerm)
5778
+ return t.elems.some((e) => containsVarTerm(e, v));
5779
+ if (t instanceof OpenListTerm)
5780
+ return t.prefix.some((e) => containsVarTerm(e, v)) || t.tailVar === v;
5781
+ if (t instanceof GraphTerm)
5782
+ return t.triples.some((tr) => containsVarTerm(tr.s, v) || containsVarTerm(tr.p, v) || containsVarTerm(tr.o, v));
5783
+ return false;
5784
+ }
5785
+ function isGroundTermInGraph(t) {
5786
+ // variables inside graph terms are treated as local placeholders,
5787
+ // so they don't make the *surrounding triple* non-ground.
5788
+ if (t instanceof OpenListTerm)
5789
+ return false;
5790
+ if (t instanceof ListTerm)
5791
+ return t.elems.every((e) => isGroundTermInGraph(e));
5792
+ if (t instanceof GraphTerm)
5793
+ return t.triples.every((tr) => isGroundTripleInGraph(tr));
5794
+ // Iri/Literal/Blank/Var are all OK inside formulas
5795
+ return true;
5796
+ }
5797
+ function isGroundTripleInGraph(tr) {
5798
+ return isGroundTermInGraph(tr.s) && isGroundTermInGraph(tr.p) && isGroundTermInGraph(tr.o);
5799
+ }
5800
+ function isGroundTerm(t) {
5801
+ if (t instanceof Var)
5802
+ return false;
5803
+ if (t instanceof ListTerm)
5804
+ return t.elems.every((e) => isGroundTerm(e));
5805
+ if (t instanceof OpenListTerm)
5806
+ return false;
5807
+ if (t instanceof GraphTerm)
5808
+ return t.triples.every((tr) => isGroundTripleInGraph(tr));
5809
+ return true;
5810
+ }
5811
+ function isGroundTriple(tr) {
5812
+ return isGroundTerm(tr.s) && isGroundTerm(tr.p) && isGroundTerm(tr.o);
5813
+ }
5814
+ // Canonical JSON-ish encoding for use as a Skolem cache key.
5815
+ // We only *call* this on ground terms in log:skolem, but it is
5816
+ // robust to seeing vars/open lists anyway.
5817
+ function skolemKeyFromTerm(t) {
5818
+ function enc(u) {
5819
+ if (u instanceof Iri)
5820
+ return ['I', u.value];
5821
+ if (u instanceof Literal)
5822
+ return ['L', u.value];
5823
+ if (u instanceof Blank)
5824
+ return ['B', u.label];
5825
+ if (u instanceof Var)
5826
+ return ['V', u.name];
5827
+ if (u instanceof ListTerm)
5828
+ return ['List', u.elems.map(enc)];
5829
+ if (u instanceof OpenListTerm)
5830
+ return ['OpenList', u.prefix.map(enc), u.tailVar];
5831
+ if (u instanceof GraphTerm)
5832
+ return ['Graph', u.triples.map((tr) => [enc(tr.s), enc(tr.p), enc(tr.o)])];
5833
+ return ['Other', String(u)];
5834
+ }
5835
+ return JSON.stringify(enc(t));
5836
+ }
5837
+ function applySubstTerm(t, s) {
5838
+ // Common case: variable
5839
+ if (t instanceof Var) {
5840
+ // Fast path: unbound variable → no change
5841
+ const first = s[t.name];
5842
+ if (first === undefined) {
5843
+ return t;
5844
+ }
5845
+ // Follow chains X -> Y -> ... until we hit a non-var or a cycle.
5846
+ let cur = first;
5847
+ const seen = new Set([t.name]);
5848
+ while (cur instanceof Var) {
5849
+ const name = cur.name;
5850
+ if (seen.has(name))
5851
+ break; // cycle
5852
+ seen.add(name);
5853
+ const nxt = s[name];
5854
+ if (!nxt)
5855
+ break;
5856
+ cur = nxt;
5857
+ }
5858
+ if (cur instanceof Var) {
5859
+ // Still a var: keep it as is (no need to clone)
5860
+ return cur;
5861
+ }
5862
+ // Bound to a non-var term: apply substitution recursively in case it
5863
+ // contains variables inside.
5864
+ return applySubstTerm(cur, s);
5865
+ }
5866
+ // Non-variable terms
5867
+ if (t instanceof ListTerm) {
5868
+ return new ListTerm(t.elems.map((e) => applySubstTerm(e, s)));
5869
+ }
5870
+ if (t instanceof OpenListTerm) {
5871
+ const newPrefix = t.prefix.map((e) => applySubstTerm(e, s));
5872
+ const tailTerm = s[t.tailVar];
5873
+ if (tailTerm !== undefined) {
5874
+ const tailApplied = applySubstTerm(tailTerm, s);
5875
+ if (tailApplied instanceof ListTerm) {
5876
+ return new ListTerm(newPrefix.concat(tailApplied.elems));
5877
+ }
5878
+ else if (tailApplied instanceof OpenListTerm) {
5879
+ return new OpenListTerm(newPrefix.concat(tailApplied.prefix), tailApplied.tailVar);
5880
+ }
5881
+ else {
5882
+ return new OpenListTerm(newPrefix, t.tailVar);
5883
+ }
5884
+ }
5885
+ else {
5886
+ return new OpenListTerm(newPrefix, t.tailVar);
5887
+ }
5888
+ }
5889
+ if (t instanceof GraphTerm) {
5890
+ return new GraphTerm(t.triples.map((tr) => applySubstTriple(tr, s)));
5891
+ }
5892
+ return t;
5893
+ }
5894
+ function applySubstTriple(tr, s) {
5895
+ return new Triple(applySubstTerm(tr.s, s), applySubstTerm(tr.p, s), applySubstTerm(tr.o, s));
5896
+ }
5897
+ function iriValue(t) {
5898
+ return t instanceof Iri ? t.value : null;
5899
+ }
5900
+ function unifyOpenWithList(prefix, tailv, ys, subst) {
5901
+ if (ys.length < prefix.length)
5902
+ return null;
5903
+ let s2 = { ...subst };
5904
+ for (let i = 0; i < prefix.length; i++) {
5905
+ s2 = unifyTerm(prefix[i], ys[i], s2);
5906
+ if (s2 === null)
5907
+ return null;
5908
+ }
5909
+ const rest = new ListTerm(ys.slice(prefix.length));
5910
+ s2 = unifyTerm(new Var(tailv), rest, s2);
5911
+ if (s2 === null)
5912
+ return null;
5913
+ return s2;
5914
+ }
5915
+ function unifyGraphTriples(xs, ys, subst) {
5916
+ if (xs.length !== ys.length)
5917
+ return null;
5918
+ // Fast path: exact same sequence.
5919
+ if (triplesListEqual(xs, ys))
5920
+ return { ...subst };
5921
+ // Backtracking match (order-insensitive), *threading* the substitution through.
5922
+ const used = new Array(ys.length).fill(false);
5923
+ function step(i, s) {
5924
+ if (i >= xs.length)
5925
+ return s;
5926
+ const x = xs[i];
5927
+ for (let j = 0; j < ys.length; j++) {
5928
+ if (used[j])
5929
+ continue;
5930
+ const y = ys[j];
5931
+ // Cheap pruning when both predicates are IRIs.
5932
+ if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
5933
+ continue;
5934
+ const s2 = unifyTriple(x, y, s); // IMPORTANT: use `s`, not {}
5935
+ if (s2 === null)
5936
+ continue;
5937
+ used[j] = true;
5938
+ const s3 = step(i + 1, s2);
5939
+ if (s3 !== null)
5940
+ return s3;
5941
+ used[j] = false;
5942
+ }
5943
+ return null;
5944
+ }
5945
+ return step(0, { ...subst }); // IMPORTANT: start from the incoming subst
5946
+ }
5947
+ function unifyTerm(a, b, subst) {
5948
+ return unifyTermWithOptions(a, b, subst, {
5949
+ boolValueEq: true,
5950
+ intDecimalEq: false,
5951
+ });
5952
+ }
5953
+ function unifyTermListAppend(a, b, subst) {
5954
+ // Keep list:append behavior: allow integer<->decimal exact equality,
5955
+ // but do NOT add boolean-value equivalence (preserves current semantics).
5956
+ return unifyTermWithOptions(a, b, subst, {
5957
+ boolValueEq: false,
5958
+ intDecimalEq: true,
5959
+ });
5960
+ }
5961
+ function unifyTermWithOptions(a, b, subst, opts) {
5962
+ a = applySubstTerm(a, subst);
5963
+ b = applySubstTerm(b, subst);
5964
+ // Variable binding
5965
+ if (a instanceof Var) {
5966
+ const v = a.name;
5967
+ const t = b;
5968
+ if (t instanceof Var && t.name === v)
5969
+ return { ...subst };
5970
+ if (containsVarTerm(t, v))
5971
+ return null;
5972
+ const s2 = { ...subst };
5973
+ s2[v] = t;
5974
+ return s2;
5975
+ }
5976
+ if (b instanceof Var) {
5977
+ return unifyTermWithOptions(b, a, subst, opts);
5978
+ }
5979
+ // Exact matches
5980
+ if (a instanceof Iri && b instanceof Iri && a.value === b.value)
5981
+ return { ...subst };
5982
+ if (a instanceof Literal && b instanceof Literal && a.value === b.value)
5983
+ return { ...subst };
5984
+ if (a instanceof Blank && b instanceof Blank && a.label === b.label)
5985
+ return { ...subst };
5986
+ // Plain string vs xsd:string equivalence
5987
+ if (a instanceof Literal && b instanceof Literal) {
5988
+ if (literalsEquivalentAsXsdString(a.value, b.value))
5989
+ return { ...subst };
5990
+ }
5991
+ // Boolean-value equivalence (ONLY for normal unifyTerm)
5992
+ if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
5993
+ const ai = parseBooleanLiteralInfo(a);
5994
+ const bi = parseBooleanLiteralInfo(b);
5995
+ if (ai && bi && ai.value === bi.value)
5996
+ return { ...subst };
5997
+ }
5998
+ // Numeric-value match:
5999
+ // - always allow equality when datatype matches (existing behavior)
6000
+ // - optionally allow integer<->decimal exact equality (list:append only)
6001
+ if (a instanceof Literal && b instanceof Literal) {
6002
+ const ai = parseNumericLiteralInfo(a);
6003
+ const bi = parseNumericLiteralInfo(b);
6004
+ if (ai && bi) {
6005
+ if (ai.dt === bi.dt) {
6006
+ if (ai.kind === 'bigint' && bi.kind === 'bigint') {
6007
+ if (ai.value === bi.value)
6008
+ return { ...subst };
6009
+ }
6010
+ else {
6011
+ const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
6012
+ const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
6013
+ if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn)
6014
+ return { ...subst };
6015
+ }
6016
+ }
6017
+ if (opts.intDecimalEq) {
6018
+ const intDt = XSD_NS + 'integer';
6019
+ const decDt = XSD_NS + 'decimal';
6020
+ if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
6021
+ const intInfo = ai.dt === intDt ? ai : bi; // bigint
6022
+ const decInfo = ai.dt === decDt ? ai : bi; // number + lexStr
6023
+ const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
6024
+ if (dec) {
6025
+ const scaledInt = intInfo.value * pow10n(dec.scale);
6026
+ if (scaledInt === dec.num)
6027
+ return { ...subst };
6028
+ }
6029
+ }
6030
+ }
6031
+ }
6032
+ }
6033
+ // Open list vs concrete list
6034
+ if (a instanceof OpenListTerm && b instanceof ListTerm) {
6035
+ return unifyOpenWithList(a.prefix, a.tailVar, b.elems, subst);
6036
+ }
6037
+ if (a instanceof ListTerm && b instanceof OpenListTerm) {
6038
+ return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
6039
+ }
6040
+ // Open list vs open list
6041
+ if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
6042
+ if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
6043
+ return null;
6044
+ let s2 = { ...subst };
6045
+ for (let i = 0; i < a.prefix.length; i++) {
6046
+ s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
6047
+ if (s2 === null)
6048
+ return null;
6049
+ }
6050
+ return s2;
6051
+ }
6052
+ // List terms
6053
+ if (a instanceof ListTerm && b instanceof ListTerm) {
6054
+ if (a.elems.length !== b.elems.length)
6055
+ return null;
6056
+ let s2 = { ...subst };
6057
+ for (let i = 0; i < a.elems.length; i++) {
6058
+ s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
6059
+ if (s2 === null)
6060
+ return null;
6061
+ }
6062
+ return s2;
6063
+ }
6064
+ // Graphs
6065
+ if (a instanceof GraphTerm && b instanceof GraphTerm) {
6066
+ if (alphaEqGraphTriples(a.triples, b.triples))
6067
+ return { ...subst };
6068
+ return unifyGraphTriples(a.triples, b.triples, subst);
6069
+ }
6070
+ return null;
6071
+ }
6072
+ function unifyTriple(pat, fact, subst) {
6073
+ // Predicates are usually the cheapest and most selective
6074
+ const s1 = unifyTerm(pat.p, fact.p, subst);
6075
+ if (s1 === null)
6076
+ return null;
6077
+ const s2 = unifyTerm(pat.s, fact.s, s1);
6078
+ if (s2 === null)
6079
+ return null;
6080
+ const s3 = unifyTerm(pat.o, fact.o, s2);
6081
+ return s3;
6082
+ }
6083
+ function composeSubst(outer, delta) {
6084
+ if (!delta || Object.keys(delta).length === 0) {
6085
+ return { ...outer };
6086
+ }
6087
+ const out = { ...outer };
6088
+ for (const [k, v] of Object.entries(delta)) {
6089
+ if (out.hasOwnProperty(k)) {
6090
+ if (!termsEqual(out[k], v))
6091
+ return null;
6092
+ }
6093
+ else {
6094
+ out[k] = v;
6095
+ }
6096
+ }
6097
+ return out;
6098
+ }
6097
6099
  // ===========================================================================
6098
6100
  // Backward proof (SLD-style)
6099
6101
  // ===========================================================================