eyeling 1.9.1 → 1.9.3
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 +2156 -2149
- package/package.json +1 -1
- package/src/eyeling-buitins.ts +3348 -0
- package/src/eyeling-core.ts +0 -1260
- package/src/eyeling-engine.ts +0 -3346
- package/src/eyeling-n3.ts +1267 -0
package/eyeling.js
CHANGED
|
@@ -624,2193 +624,1870 @@ class DerivedFact {
|
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
// ===========================================================================
|
|
627
|
-
//
|
|
627
|
+
// Blank-node lifting and Skolemization
|
|
628
628
|
// ===========================================================================
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
629
|
+
function liftBlankRuleVars(premise, conclusion) {
|
|
630
|
+
function convertTerm(t, mapping, counter) {
|
|
631
|
+
if (t instanceof Blank) {
|
|
632
|
+
const label = t.label;
|
|
633
|
+
if (!mapping.hasOwnProperty(label)) {
|
|
634
|
+
counter[0] += 1;
|
|
635
|
+
mapping[label] = `_b${counter[0]}`;
|
|
636
|
+
}
|
|
637
|
+
return new Var(mapping[label]);
|
|
638
|
+
}
|
|
639
|
+
if (t instanceof ListTerm) {
|
|
640
|
+
return new ListTerm(t.elems.map((e) => convertTerm(e, mapping, counter)));
|
|
641
|
+
}
|
|
642
|
+
if (t instanceof OpenListTerm) {
|
|
643
|
+
return new OpenListTerm(t.prefix.map((e) => convertTerm(e, mapping, counter)), t.tailVar);
|
|
644
|
+
}
|
|
645
|
+
if (t instanceof GraphTerm) {
|
|
646
|
+
const triples = t.triples.map((tr) => new Triple(convertTerm(tr.s, mapping, counter), convertTerm(tr.p, mapping, counter), convertTerm(tr.o, mapping, counter)));
|
|
647
|
+
return new GraphTerm(triples);
|
|
648
|
+
}
|
|
649
|
+
return t;
|
|
641
650
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
constructor(message, offset = null) {
|
|
645
|
-
super(message);
|
|
646
|
-
this.name = 'N3SyntaxError';
|
|
647
|
-
this.offset = offset;
|
|
651
|
+
function convertTriple(tr, mapping, counter) {
|
|
652
|
+
return new Triple(convertTerm(tr.s, mapping, counter), convertTerm(tr.p, mapping, counter), convertTerm(tr.o, mapping, counter));
|
|
648
653
|
}
|
|
654
|
+
const mapping = {};
|
|
655
|
+
const counter = [0];
|
|
656
|
+
const newPremise = premise.map((tr) => convertTriple(tr, mapping, counter));
|
|
657
|
+
return [newPremise, conclusion];
|
|
649
658
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
out += '\\';
|
|
666
|
-
continue;
|
|
659
|
+
// Skolemization for blank nodes that occur explicitly in a rule head.
|
|
660
|
+
//
|
|
661
|
+
// IMPORTANT: we must be *stable per rule firing*, otherwise a rule whose
|
|
662
|
+
// premises stay true would keep generating fresh _:sk_N blank nodes on every
|
|
663
|
+
// outer fixpoint iteration (non-termination once we do strict duplicate checks).
|
|
664
|
+
//
|
|
665
|
+
// We achieve this by optionally keying head-blank allocations by a "firingKey"
|
|
666
|
+
// (usually derived from the instantiated premises and rule index) and caching
|
|
667
|
+
// them in a run-global map.
|
|
668
|
+
function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
|
|
669
|
+
if (t instanceof Blank) {
|
|
670
|
+
const label = t.label;
|
|
671
|
+
// Only skolemize blanks that occur explicitly in the rule head
|
|
672
|
+
if (!headBlankLabels || !headBlankLabels.has(label)) {
|
|
673
|
+
return t; // this is a data blank (e.g. bound via ?X), keep it
|
|
667
674
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
case 'b':
|
|
680
|
-
out += '\b';
|
|
681
|
-
break;
|
|
682
|
-
case 'f':
|
|
683
|
-
out += '\f';
|
|
684
|
-
break;
|
|
685
|
-
case '"':
|
|
686
|
-
out += '"';
|
|
687
|
-
break;
|
|
688
|
-
case "'":
|
|
689
|
-
out += "'";
|
|
690
|
-
break;
|
|
691
|
-
case '\\':
|
|
692
|
-
out += '\\';
|
|
693
|
-
break;
|
|
694
|
-
case 'u': {
|
|
695
|
-
const hex = s.slice(i + 1, i + 5);
|
|
696
|
-
if (/^[0-9A-Fa-f]{4}$/.test(hex)) {
|
|
697
|
-
out += String.fromCharCode(parseInt(hex, 16));
|
|
698
|
-
i += 4;
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
out += '\\u';
|
|
675
|
+
if (!mapping.hasOwnProperty(label)) {
|
|
676
|
+
// If we have a global cache keyed by firingKey, use it to ensure
|
|
677
|
+
// deterministic blank IDs for the same rule+substitution instance.
|
|
678
|
+
if (globalMap && firingKey) {
|
|
679
|
+
const gk = `${firingKey}|${label}`;
|
|
680
|
+
let sk = globalMap.get(gk);
|
|
681
|
+
if (!sk) {
|
|
682
|
+
const idx = skCounter[0];
|
|
683
|
+
skCounter[0] += 1;
|
|
684
|
+
sk = `_:sk_${idx}`;
|
|
685
|
+
globalMap.set(gk, sk);
|
|
702
686
|
}
|
|
703
|
-
|
|
687
|
+
mapping[label] = sk;
|
|
704
688
|
}
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (cp >= 0 && cp <= 0x10ffff)
|
|
710
|
-
out += String.fromCodePoint(cp);
|
|
711
|
-
else
|
|
712
|
-
out += '\\U' + hex;
|
|
713
|
-
i += 8;
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
out += '\\U';
|
|
717
|
-
}
|
|
718
|
-
break;
|
|
689
|
+
else {
|
|
690
|
+
const idx = skCounter[0];
|
|
691
|
+
skCounter[0] += 1;
|
|
692
|
+
mapping[label] = `_:sk_${idx}`;
|
|
719
693
|
}
|
|
720
|
-
default:
|
|
721
|
-
// preserve unknown escapes
|
|
722
|
-
out += '\\' + e;
|
|
723
694
|
}
|
|
695
|
+
return new Blank(mapping[label]);
|
|
724
696
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
function lex(inputText) {
|
|
728
|
-
const chars = Array.from(inputText);
|
|
729
|
-
const n = chars.length;
|
|
730
|
-
let i = 0;
|
|
731
|
-
const tokens = [];
|
|
732
|
-
function peek(offset = 0) {
|
|
733
|
-
const j = i + offset;
|
|
734
|
-
return j >= 0 && j < n ? chars[j] : null;
|
|
697
|
+
if (t instanceof ListTerm) {
|
|
698
|
+
return new ListTerm(t.elems.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter, firingKey, globalMap)));
|
|
735
699
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
700
|
+
if (t instanceof OpenListTerm) {
|
|
701
|
+
return new OpenListTerm(t.prefix.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter, firingKey, globalMap)), t.tailVar);
|
|
702
|
+
}
|
|
703
|
+
if (t instanceof GraphTerm) {
|
|
704
|
+
return new GraphTerm(t.triples.map((tr) => skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap)));
|
|
705
|
+
}
|
|
706
|
+
return t;
|
|
707
|
+
}
|
|
708
|
+
function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
|
|
709
|
+
return new Triple(skolemizeTermForHeadBlanks(tr.s, headBlankLabels, mapping, skCounter, firingKey, globalMap), skolemizeTermForHeadBlanks(tr.p, headBlankLabels, mapping, skCounter, firingKey, globalMap), skolemizeTermForHeadBlanks(tr.o, headBlankLabels, mapping, skCounter, firingKey, globalMap));
|
|
710
|
+
}
|
|
711
|
+
// ===========================================================================
|
|
712
|
+
// Alpha equivalence helpers
|
|
713
|
+
// ===========================================================================
|
|
714
|
+
function termsEqual(a, b) {
|
|
715
|
+
if (a === b)
|
|
716
|
+
return true;
|
|
717
|
+
if (!a || !b)
|
|
718
|
+
return false;
|
|
719
|
+
if (a.constructor !== b.constructor)
|
|
720
|
+
return false;
|
|
721
|
+
if (a instanceof Iri)
|
|
722
|
+
return a.value === b.value;
|
|
723
|
+
if (a instanceof Literal) {
|
|
724
|
+
if (a.value === b.value)
|
|
725
|
+
return true;
|
|
726
|
+
// Plain "abc" == "abc"^^xsd:string (but not language-tagged strings)
|
|
727
|
+
if (literalsEquivalentAsXsdString(a.value, b.value))
|
|
728
|
+
return true;
|
|
729
|
+
// Keep in sync with unifyTerm(): numeric-value equality, datatype-aware.
|
|
730
|
+
const ai = parseNumericLiteralInfo(a);
|
|
731
|
+
const bi = parseNumericLiteralInfo(b);
|
|
732
|
+
if (ai && bi) {
|
|
733
|
+
// Same datatype => compare values
|
|
734
|
+
if (ai.dt === bi.dt) {
|
|
735
|
+
if (ai.kind === 'bigint' && bi.kind === 'bigint')
|
|
736
|
+
return ai.value === bi.value;
|
|
737
|
+
const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
|
|
738
|
+
const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
|
|
739
|
+
return !Number.isNaN(an) && !Number.isNaN(bn) && an === bn;
|
|
740
|
+
}
|
|
744
741
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
if (a instanceof Var)
|
|
745
|
+
return a.name === b.name;
|
|
746
|
+
if (a instanceof Blank)
|
|
747
|
+
return a.label === b.label;
|
|
748
|
+
if (a instanceof ListTerm) {
|
|
749
|
+
if (a.elems.length !== b.elems.length)
|
|
750
|
+
return false;
|
|
751
|
+
for (let i = 0; i < a.elems.length; i++) {
|
|
752
|
+
if (!termsEqual(a.elems[i], b.elems[i]))
|
|
753
|
+
return false;
|
|
750
754
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
i += 1;
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
if (a instanceof OpenListTerm) {
|
|
758
|
+
if (a.tailVar !== b.tailVar)
|
|
759
|
+
return false;
|
|
760
|
+
if (a.prefix.length !== b.prefix.length)
|
|
761
|
+
return false;
|
|
762
|
+
for (let i = 0; i < a.prefix.length; i++) {
|
|
763
|
+
if (!termsEqual(a.prefix[i], b.prefix[i]))
|
|
764
|
+
return false;
|
|
764
765
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
if (a instanceof GraphTerm) {
|
|
769
|
+
return alphaEqGraphTriples(a.triples, b.triples);
|
|
770
|
+
}
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
function termsEqualNoIntDecimal(a, b) {
|
|
774
|
+
if (a === b)
|
|
775
|
+
return true;
|
|
776
|
+
if (!a || !b)
|
|
777
|
+
return false;
|
|
778
|
+
if (a.constructor !== b.constructor)
|
|
779
|
+
return false;
|
|
780
|
+
if (a instanceof Iri)
|
|
781
|
+
return a.value === b.value;
|
|
782
|
+
if (a instanceof Literal) {
|
|
783
|
+
if (a.value === b.value)
|
|
784
|
+
return true;
|
|
785
|
+
// Plain "abc" == "abc"^^xsd:string (but not language-tagged)
|
|
786
|
+
if (literalsEquivalentAsXsdString(a.value, b.value))
|
|
787
|
+
return true;
|
|
788
|
+
// Numeric equality ONLY when datatypes agree (no integer<->decimal here)
|
|
789
|
+
const ai = parseNumericLiteralInfo(a);
|
|
790
|
+
const bi = parseNumericLiteralInfo(b);
|
|
791
|
+
if (ai && bi && ai.dt === bi.dt) {
|
|
792
|
+
// integer: exact bigint
|
|
793
|
+
if (ai.kind === 'bigint' && bi.kind === 'bigint')
|
|
794
|
+
return ai.value === bi.value;
|
|
795
|
+
// decimal: compare exactly via num/scale if possible
|
|
796
|
+
if (ai.dt === XSD_NS + 'decimal') {
|
|
797
|
+
const da = parseXsdDecimalToBigIntScale(ai.lexStr);
|
|
798
|
+
const db = parseXsdDecimalToBigIntScale(bi.lexStr);
|
|
799
|
+
if (da && db) {
|
|
800
|
+
const scale = Math.max(da.scale, db.scale);
|
|
801
|
+
const na = da.num * pow10n(scale - da.scale);
|
|
802
|
+
const nb = db.num * pow10n(scale - db.scale);
|
|
803
|
+
return na === nb;
|
|
804
|
+
}
|
|
787
805
|
}
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
806
|
+
// double/float-ish: JS number (same as your normal same-dt path)
|
|
807
|
+
const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
|
|
808
|
+
const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
|
|
809
|
+
return !Number.isNaN(an) && !Number.isNaN(bn) && an === bn;
|
|
792
810
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
if (a instanceof Var)
|
|
814
|
+
return a.name === b.name;
|
|
815
|
+
if (a instanceof Blank)
|
|
816
|
+
return a.label === b.label;
|
|
817
|
+
if (a instanceof ListTerm) {
|
|
818
|
+
if (a.elems.length !== b.elems.length)
|
|
819
|
+
return false;
|
|
820
|
+
for (let i = 0; i < a.elems.length; i++) {
|
|
821
|
+
if (!termsEqualNoIntDecimal(a.elems[i], b.elems[i]))
|
|
822
|
+
return false;
|
|
798
823
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
if (a instanceof OpenListTerm) {
|
|
827
|
+
if (a.tailVar !== b.tailVar)
|
|
828
|
+
return false;
|
|
829
|
+
if (a.prefix.length !== b.prefix.length)
|
|
830
|
+
return false;
|
|
831
|
+
for (let i = 0; i < a.prefix.length; i++) {
|
|
832
|
+
if (!termsEqualNoIntDecimal(a.prefix[i], b.prefix[i]))
|
|
833
|
+
return false;
|
|
808
834
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
if (a instanceof GraphTerm) {
|
|
838
|
+
return alphaEqGraphTriples(a.triples, b.triples);
|
|
839
|
+
}
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
function triplesEqual(a, b) {
|
|
843
|
+
return termsEqual(a.s, b.s) && termsEqual(a.p, b.p) && termsEqual(a.o, b.o);
|
|
844
|
+
}
|
|
845
|
+
function triplesListEqual(xs, ys) {
|
|
846
|
+
if (xs.length !== ys.length)
|
|
847
|
+
return false;
|
|
848
|
+
for (let i = 0; i < xs.length; i++) {
|
|
849
|
+
if (!triplesEqual(xs[i], ys[i]))
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
// Alpha-equivalence for quoted formulas, up to *variable* and blank-node renaming.
|
|
855
|
+
// Treats a formula as an unordered set of triples (order-insensitive match).
|
|
856
|
+
function alphaEqVarName(x, y, vmap) {
|
|
857
|
+
if (vmap.hasOwnProperty(x))
|
|
858
|
+
return vmap[x] === y;
|
|
859
|
+
vmap[x] = y;
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
function alphaEqTermInGraph(a, b, vmap, bmap) {
|
|
863
|
+
// Blank nodes: renamable
|
|
864
|
+
if (a instanceof Blank && b instanceof Blank) {
|
|
865
|
+
const x = a.label;
|
|
866
|
+
const y = b.label;
|
|
867
|
+
if (bmap.hasOwnProperty(x))
|
|
868
|
+
return bmap[x] === y;
|
|
869
|
+
bmap[x] = y;
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
// Variables: renamable (ONLY inside quoted formulas)
|
|
873
|
+
if (a instanceof Var && b instanceof Var) {
|
|
874
|
+
return alphaEqVarName(a.name, b.name, vmap);
|
|
875
|
+
}
|
|
876
|
+
if (a instanceof Iri && b instanceof Iri)
|
|
877
|
+
return a.value === b.value;
|
|
878
|
+
if (a instanceof Literal && b instanceof Literal)
|
|
879
|
+
return a.value === b.value;
|
|
880
|
+
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
881
|
+
if (a.elems.length !== b.elems.length)
|
|
882
|
+
return false;
|
|
883
|
+
for (let i = 0; i < a.elems.length; i++) {
|
|
884
|
+
if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap))
|
|
885
|
+
return false;
|
|
825
886
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
while (i < n) {
|
|
835
|
-
const cc = chars[i];
|
|
836
|
-
// Preserve escapes verbatim (same behavior as short strings)
|
|
837
|
-
if (cc === '\\') {
|
|
838
|
-
i++;
|
|
839
|
-
if (i < n) {
|
|
840
|
-
const esc = chars[i];
|
|
841
|
-
i++;
|
|
842
|
-
sChars.push('\\');
|
|
843
|
-
sChars.push(esc);
|
|
844
|
-
}
|
|
845
|
-
else {
|
|
846
|
-
sChars.push('\\');
|
|
847
|
-
}
|
|
848
|
-
continue;
|
|
849
|
-
}
|
|
850
|
-
// In long strings, a run of >= 3 delimiter quotes terminates the literal.
|
|
851
|
-
// Any extra quotes beyond the final 3 are part of the content.
|
|
852
|
-
if (cc === '"') {
|
|
853
|
-
let run = 0;
|
|
854
|
-
while (i + run < n && chars[i + run] === '"')
|
|
855
|
-
run++;
|
|
856
|
-
if (run >= 3) {
|
|
857
|
-
for (let k = 0; k < run - 3; k++)
|
|
858
|
-
sChars.push('"');
|
|
859
|
-
i += run; // consume content quotes (if any) + closing delimiter
|
|
860
|
-
closed = true;
|
|
861
|
-
break;
|
|
862
|
-
}
|
|
863
|
-
for (let k = 0; k < run; k++)
|
|
864
|
-
sChars.push('"');
|
|
865
|
-
i += run;
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
sChars.push(cc);
|
|
869
|
-
i++;
|
|
870
|
-
}
|
|
871
|
-
if (!closed)
|
|
872
|
-
throw new N3SyntaxError('Unterminated long string literal """..."""', start);
|
|
873
|
-
const raw = '"""' + sChars.join('') + '"""';
|
|
874
|
-
const decoded = decodeN3StringEscapes(stripQuotes(raw));
|
|
875
|
-
const s = JSON.stringify(decoded); // canonical short quoted form
|
|
876
|
-
tokens.push(new Token('Literal', s, start));
|
|
877
|
-
continue;
|
|
878
|
-
}
|
|
879
|
-
// Short string literal " ... "
|
|
880
|
-
i++; // consume opening "
|
|
881
|
-
const sChars = [];
|
|
882
|
-
while (i < n) {
|
|
883
|
-
let cc = chars[i];
|
|
884
|
-
i++;
|
|
885
|
-
if (cc === '\\') {
|
|
886
|
-
if (i < n) {
|
|
887
|
-
const esc = chars[i];
|
|
888
|
-
i++;
|
|
889
|
-
sChars.push('\\');
|
|
890
|
-
sChars.push(esc);
|
|
891
|
-
}
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
if (cc === '"')
|
|
895
|
-
break;
|
|
896
|
-
sChars.push(cc);
|
|
897
|
-
}
|
|
898
|
-
const raw = '"' + sChars.join('') + '"';
|
|
899
|
-
const decoded = decodeN3StringEscapes(stripQuotes(raw));
|
|
900
|
-
const s = JSON.stringify(decoded); // canonical short quoted form
|
|
901
|
-
tokens.push(new Token('Literal', s, start));
|
|
902
|
-
continue;
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
890
|
+
if (a.prefix.length !== b.prefix.length)
|
|
891
|
+
return false;
|
|
892
|
+
for (let i = 0; i < a.prefix.length; i++) {
|
|
893
|
+
if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap))
|
|
894
|
+
return false;
|
|
903
895
|
}
|
|
904
|
-
//
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
while (i + run < n && chars[i + run] === "'")
|
|
933
|
-
run++;
|
|
934
|
-
if (run >= 3) {
|
|
935
|
-
for (let k = 0; k < run - 3; k++)
|
|
936
|
-
sChars.push("'");
|
|
937
|
-
i += run; // consume content quotes (if any) + closing delimiter
|
|
938
|
-
closed = true;
|
|
939
|
-
break;
|
|
940
|
-
}
|
|
941
|
-
for (let k = 0; k < run; k++)
|
|
942
|
-
sChars.push("'");
|
|
943
|
-
i += run;
|
|
944
|
-
continue;
|
|
945
|
-
}
|
|
946
|
-
sChars.push(cc);
|
|
947
|
-
i++;
|
|
948
|
-
}
|
|
949
|
-
if (!closed)
|
|
950
|
-
throw new N3SyntaxError("Unterminated long string literal '''...'''", start);
|
|
951
|
-
const raw = "'''" + sChars.join('') + "'''";
|
|
952
|
-
const decoded = decodeN3StringEscapes(stripQuotes(raw));
|
|
953
|
-
const s = JSON.stringify(decoded); // canonical short quoted form
|
|
954
|
-
tokens.push(new Token('Literal', s, start));
|
|
896
|
+
// tailVar is a var-name string, so treat it as renamable too
|
|
897
|
+
return alphaEqVarName(a.tailVar, b.tailVar, vmap);
|
|
898
|
+
}
|
|
899
|
+
// Nested formulas: compare with fresh maps (separate scope)
|
|
900
|
+
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
901
|
+
return alphaEqGraphTriples(a.triples, b.triples);
|
|
902
|
+
}
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
function alphaEqTripleInGraph(a, b, vmap, bmap) {
|
|
906
|
+
return (alphaEqTermInGraph(a.s, b.s, vmap, bmap) &&
|
|
907
|
+
alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
|
|
908
|
+
alphaEqTermInGraph(a.o, b.o, vmap, bmap));
|
|
909
|
+
}
|
|
910
|
+
function alphaEqGraphTriples(xs, ys) {
|
|
911
|
+
if (xs.length !== ys.length)
|
|
912
|
+
return false;
|
|
913
|
+
// Fast path: exact same sequence.
|
|
914
|
+
if (triplesListEqual(xs, ys))
|
|
915
|
+
return true;
|
|
916
|
+
// Order-insensitive backtracking match, threading var/blank mappings.
|
|
917
|
+
const used = new Array(ys.length).fill(false);
|
|
918
|
+
function step(i, vmap, bmap) {
|
|
919
|
+
if (i >= xs.length)
|
|
920
|
+
return true;
|
|
921
|
+
const x = xs[i];
|
|
922
|
+
for (let j = 0; j < ys.length; j++) {
|
|
923
|
+
if (used[j])
|
|
955
924
|
continue;
|
|
956
|
-
|
|
957
|
-
//
|
|
958
|
-
|
|
959
|
-
const sChars = [];
|
|
960
|
-
while (i < n) {
|
|
961
|
-
let cc = chars[i];
|
|
962
|
-
i++;
|
|
963
|
-
if (cc === '\\') {
|
|
964
|
-
if (i < n) {
|
|
965
|
-
const esc = chars[i];
|
|
966
|
-
i++;
|
|
967
|
-
sChars.push('\\');
|
|
968
|
-
sChars.push(esc);
|
|
969
|
-
}
|
|
970
|
-
continue;
|
|
971
|
-
}
|
|
972
|
-
if (cc === "'")
|
|
973
|
-
break;
|
|
974
|
-
sChars.push(cc);
|
|
975
|
-
}
|
|
976
|
-
const raw = "'" + sChars.join('') + "'";
|
|
977
|
-
const decoded = decodeN3StringEscapes(stripQuotes(raw));
|
|
978
|
-
const s = JSON.stringify(decoded); // canonical short quoted form
|
|
979
|
-
tokens.push(new Token('Literal', s, start));
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
// Variable ?name
|
|
983
|
-
if (c === '?') {
|
|
984
|
-
const start = i;
|
|
985
|
-
i++;
|
|
986
|
-
const nameChars = [];
|
|
987
|
-
let cc;
|
|
988
|
-
while ((cc = peek()) !== null && isNameChar(cc)) {
|
|
989
|
-
nameChars.push(cc);
|
|
990
|
-
i++;
|
|
991
|
-
}
|
|
992
|
-
const name = nameChars.join('');
|
|
993
|
-
tokens.push(new Token('Var', name, start));
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
// Directives: @prefix, @base (and language tags after string literals)
|
|
997
|
-
if (c === '@') {
|
|
998
|
-
const start = i;
|
|
999
|
-
const prevTok = tokens.length ? tokens[tokens.length - 1] : null;
|
|
1000
|
-
const prevWasQuotedLiteral = prevTok && prevTok.typ === 'Literal' && typeof prevTok.value === 'string' && prevTok.value.startsWith('"');
|
|
1001
|
-
i++; // consume '@'
|
|
1002
|
-
if (prevWasQuotedLiteral) {
|
|
1003
|
-
// N3 grammar production LANGTAG:
|
|
1004
|
-
// "@" [a-zA-Z]+ ("-" [a-zA-Z0-9]+)*
|
|
1005
|
-
const tagChars = [];
|
|
1006
|
-
let cc = peek();
|
|
1007
|
-
if (cc === null || !/[A-Za-z]/.test(cc)) {
|
|
1008
|
-
throw new N3SyntaxError("Invalid language tag (expected [A-Za-z] after '@')", start);
|
|
1009
|
-
}
|
|
1010
|
-
while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
|
|
1011
|
-
tagChars.push(cc);
|
|
1012
|
-
i++;
|
|
1013
|
-
}
|
|
1014
|
-
while (peek() === '-') {
|
|
1015
|
-
tagChars.push('-');
|
|
1016
|
-
i++; // consume '-'
|
|
1017
|
-
const segChars = [];
|
|
1018
|
-
while ((cc = peek()) !== null && /[A-Za-z0-9]/.test(cc)) {
|
|
1019
|
-
segChars.push(cc);
|
|
1020
|
-
i++;
|
|
1021
|
-
}
|
|
1022
|
-
if (!segChars.length) {
|
|
1023
|
-
throw new N3SyntaxError("Invalid language tag (expected [A-Za-z0-9]+ after '-')", start);
|
|
1024
|
-
}
|
|
1025
|
-
tagChars.push(...segChars);
|
|
1026
|
-
}
|
|
1027
|
-
tokens.push(new Token('LangTag', tagChars.join(''), start));
|
|
925
|
+
const y = ys[j];
|
|
926
|
+
// Cheap pruning when both predicates are IRIs.
|
|
927
|
+
if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
|
|
1028
928
|
continue;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const word = wordChars.join('');
|
|
1038
|
-
if (word === 'prefix')
|
|
1039
|
-
tokens.push(new Token('AtPrefix', null, start));
|
|
1040
|
-
else if (word === 'base')
|
|
1041
|
-
tokens.push(new Token('AtBase', null, start));
|
|
1042
|
-
else
|
|
1043
|
-
throw new N3SyntaxError(`Unknown directive @${word}`, start);
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
// 6) Numeric literal (integer or float)
|
|
1047
|
-
if (/[0-9]/.test(c) || (c === '-' && peek(1) !== null && /[0-9]/.test(peek(1)))) {
|
|
1048
|
-
const start = i;
|
|
1049
|
-
const numChars = [c];
|
|
1050
|
-
i++;
|
|
1051
|
-
while (i < n) {
|
|
1052
|
-
const cc = chars[i];
|
|
1053
|
-
if (/[0-9]/.test(cc)) {
|
|
1054
|
-
numChars.push(cc);
|
|
1055
|
-
i++;
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
if (cc === '.') {
|
|
1059
|
-
if (i + 1 < n && /[0-9]/.test(chars[i + 1])) {
|
|
1060
|
-
numChars.push('.');
|
|
1061
|
-
i++;
|
|
1062
|
-
continue;
|
|
1063
|
-
}
|
|
1064
|
-
else {
|
|
1065
|
-
break;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
break;
|
|
1069
|
-
}
|
|
1070
|
-
// Optional exponent part: e.g., 1e0, 1.1e-3, 1.1E+0
|
|
1071
|
-
if (i < n && (chars[i] === 'e' || chars[i] === 'E')) {
|
|
1072
|
-
let j = i + 1;
|
|
1073
|
-
if (j < n && (chars[j] === '+' || chars[j] === '-'))
|
|
1074
|
-
j++;
|
|
1075
|
-
if (j < n && /[0-9]/.test(chars[j])) {
|
|
1076
|
-
numChars.push(chars[i]); // e/E
|
|
1077
|
-
i++;
|
|
1078
|
-
if (i < n && (chars[i] === '+' || chars[i] === '-')) {
|
|
1079
|
-
numChars.push(chars[i]);
|
|
1080
|
-
i++;
|
|
1081
|
-
}
|
|
1082
|
-
while (i < n && /[0-9]/.test(chars[i])) {
|
|
1083
|
-
numChars.push(chars[i]);
|
|
1084
|
-
i++;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
tokens.push(new Token('Literal', numChars.join(''), start));
|
|
1089
|
-
continue;
|
|
1090
|
-
}
|
|
1091
|
-
// 7) Identifiers / keywords / QNames
|
|
1092
|
-
const start = i;
|
|
1093
|
-
const wordChars = [];
|
|
1094
|
-
let cc;
|
|
1095
|
-
while ((cc = peek()) !== null && isNameChar(cc)) {
|
|
1096
|
-
wordChars.push(cc);
|
|
1097
|
-
i++;
|
|
1098
|
-
}
|
|
1099
|
-
if (!wordChars.length) {
|
|
1100
|
-
throw new N3SyntaxError(`Unexpected char: ${JSON.stringify(c)}`, i);
|
|
1101
|
-
}
|
|
1102
|
-
const word = wordChars.join('');
|
|
1103
|
-
if (word === 'true' || word === 'false') {
|
|
1104
|
-
tokens.push(new Token('Literal', word, start));
|
|
929
|
+
const v2 = { ...vmap };
|
|
930
|
+
const b2 = { ...bmap };
|
|
931
|
+
if (!alphaEqTripleInGraph(x, y, v2, b2))
|
|
932
|
+
continue;
|
|
933
|
+
used[j] = true;
|
|
934
|
+
if (step(i + 1, v2, b2))
|
|
935
|
+
return true;
|
|
936
|
+
used[j] = false;
|
|
1105
937
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
return step(0, {}, {});
|
|
941
|
+
}
|
|
942
|
+
function alphaEqTerm(a, b, bmap) {
|
|
943
|
+
if (a instanceof Blank && b instanceof Blank) {
|
|
944
|
+
const x = a.label;
|
|
945
|
+
const y = b.label;
|
|
946
|
+
if (bmap.hasOwnProperty(x)) {
|
|
947
|
+
return bmap[x] === y;
|
|
1108
948
|
}
|
|
1109
949
|
else {
|
|
1110
|
-
|
|
950
|
+
bmap[x] = y;
|
|
951
|
+
return true;
|
|
1111
952
|
}
|
|
1112
953
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
m['rdfs'] = RDFS_NS;
|
|
1128
|
-
m['xsd'] = XSD_NS;
|
|
1129
|
-
m['log'] = LOG_NS;
|
|
1130
|
-
m['math'] = MATH_NS;
|
|
1131
|
-
m['string'] = STRING_NS;
|
|
1132
|
-
m['list'] = LIST_NS;
|
|
1133
|
-
m['time'] = TIME_NS;
|
|
1134
|
-
m['genid'] = SKOLEM_NS;
|
|
1135
|
-
m[''] = ''; // empty prefix default namespace
|
|
1136
|
-
return new PrefixEnv(m, ''); // base IRI starts empty
|
|
954
|
+
if (a instanceof Iri && b instanceof Iri)
|
|
955
|
+
return a.value === b.value;
|
|
956
|
+
if (a instanceof Literal && b instanceof Literal)
|
|
957
|
+
return a.value === b.value;
|
|
958
|
+
if (a instanceof Var && b instanceof Var)
|
|
959
|
+
return a.name === b.name;
|
|
960
|
+
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
961
|
+
if (a.elems.length !== b.elems.length)
|
|
962
|
+
return false;
|
|
963
|
+
for (let i = 0; i < a.elems.length; i++) {
|
|
964
|
+
if (!alphaEqTerm(a.elems[i], b.elems[i], bmap))
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
return true;
|
|
1137
968
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
969
|
+
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
970
|
+
if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
|
|
971
|
+
return false;
|
|
972
|
+
for (let i = 0; i < a.prefix.length; i++) {
|
|
973
|
+
if (!alphaEqTerm(a.prefix[i], b.prefix[i], bmap))
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
return true;
|
|
1140
977
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
978
|
+
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
979
|
+
// formulas are alpha-equivalent up to var/blank renaming
|
|
980
|
+
return alphaEqGraphTriples(a.triples, b.triples);
|
|
1143
981
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
function alphaEqTriple(a, b) {
|
|
985
|
+
const bmap = {};
|
|
986
|
+
return alphaEqTerm(a.s, b.s, bmap) && alphaEqTerm(a.p, b.p, bmap) && alphaEqTerm(a.o, b.o, bmap);
|
|
987
|
+
}
|
|
988
|
+
// ===========================================================================
|
|
989
|
+
// Indexes (facts + backward rules)
|
|
990
|
+
// ===========================================================================
|
|
991
|
+
//
|
|
992
|
+
// Facts:
|
|
993
|
+
// - __byPred: Map<predicateIRI, Triple[]>
|
|
994
|
+
// - __byPO: Map<predicateIRI, Map<objectKey, Triple[]>>
|
|
995
|
+
// - __keySet: Set<"S\tP\tO"> for IRI/Literal-only triples (fast dup check)
|
|
996
|
+
//
|
|
997
|
+
// Backward rules:
|
|
998
|
+
// - __byHeadPred: Map<headPredicateIRI, Rule[]>
|
|
999
|
+
// - __wildHeadPred: Rule[] (non-IRI head predicate)
|
|
1000
|
+
function termFastKey(t) {
|
|
1001
|
+
if (t instanceof Iri)
|
|
1002
|
+
return 'I:' + t.value;
|
|
1003
|
+
if (t instanceof Literal)
|
|
1004
|
+
return 'L:' + normalizeLiteralForFastKey(t.value);
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
function tripleFastKey(tr) {
|
|
1008
|
+
const ks = termFastKey(tr.s);
|
|
1009
|
+
const kp = termFastKey(tr.p);
|
|
1010
|
+
const ko = termFastKey(tr.o);
|
|
1011
|
+
if (ks === null || kp === null || ko === null)
|
|
1012
|
+
return null;
|
|
1013
|
+
return ks + '\t' + kp + '\t' + ko;
|
|
1014
|
+
}
|
|
1015
|
+
function ensureFactIndexes(facts) {
|
|
1016
|
+
if (facts.__byPred && facts.__byPS && facts.__byPO && facts.__keySet)
|
|
1017
|
+
return;
|
|
1018
|
+
Object.defineProperty(facts, '__byPred', {
|
|
1019
|
+
value: new Map(),
|
|
1020
|
+
enumerable: false,
|
|
1021
|
+
writable: true,
|
|
1022
|
+
});
|
|
1023
|
+
Object.defineProperty(facts, '__byPS', {
|
|
1024
|
+
value: new Map(),
|
|
1025
|
+
enumerable: false,
|
|
1026
|
+
writable: true,
|
|
1027
|
+
});
|
|
1028
|
+
Object.defineProperty(facts, '__byPO', {
|
|
1029
|
+
value: new Map(),
|
|
1030
|
+
enumerable: false,
|
|
1031
|
+
writable: true,
|
|
1032
|
+
});
|
|
1033
|
+
Object.defineProperty(facts, '__keySet', {
|
|
1034
|
+
value: new Set(),
|
|
1035
|
+
enumerable: false,
|
|
1036
|
+
writable: true,
|
|
1037
|
+
});
|
|
1038
|
+
for (const f of facts)
|
|
1039
|
+
indexFact(facts, f);
|
|
1040
|
+
}
|
|
1041
|
+
function indexFact(facts, tr) {
|
|
1042
|
+
if (tr.p instanceof Iri) {
|
|
1043
|
+
const pk = tr.p.value;
|
|
1044
|
+
let pb = facts.__byPred.get(pk);
|
|
1045
|
+
if (!pb) {
|
|
1046
|
+
pb = [];
|
|
1047
|
+
facts.__byPred.set(pk, pb);
|
|
1151
1048
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
if (best === null || cand[1].length < best[1].length)
|
|
1165
|
-
best = cand;
|
|
1049
|
+
pb.push(tr);
|
|
1050
|
+
const sk = termFastKey(tr.s);
|
|
1051
|
+
if (sk !== null) {
|
|
1052
|
+
let ps = facts.__byPS.get(pk);
|
|
1053
|
+
if (!ps) {
|
|
1054
|
+
ps = new Map();
|
|
1055
|
+
facts.__byPS.set(pk, ps);
|
|
1056
|
+
}
|
|
1057
|
+
let psb = ps.get(sk);
|
|
1058
|
+
if (!psb) {
|
|
1059
|
+
psb = [];
|
|
1060
|
+
ps.set(sk, psb);
|
|
1166
1061
|
}
|
|
1062
|
+
psb.push(tr);
|
|
1167
1063
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
}
|
|
1175
|
-
prefixesUsedForOutput(triples) {
|
|
1176
|
-
const used = new Set();
|
|
1177
|
-
for (const t of triples) {
|
|
1178
|
-
const iris = [];
|
|
1179
|
-
iris.push(...collectIrisInTerm(t.s));
|
|
1180
|
-
if (!isRdfTypePred(t.p)) {
|
|
1181
|
-
iris.push(...collectIrisInTerm(t.p));
|
|
1064
|
+
const ok = termFastKey(tr.o);
|
|
1065
|
+
if (ok !== null) {
|
|
1066
|
+
let po = facts.__byPO.get(pk);
|
|
1067
|
+
if (!po) {
|
|
1068
|
+
po = new Map();
|
|
1069
|
+
facts.__byPO.set(pk, po);
|
|
1182
1070
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
used.add(p);
|
|
1188
|
-
}
|
|
1071
|
+
let pob = po.get(ok);
|
|
1072
|
+
if (!pob) {
|
|
1073
|
+
pob = [];
|
|
1074
|
+
po.set(ok, pob);
|
|
1189
1075
|
}
|
|
1076
|
+
pob.push(tr);
|
|
1190
1077
|
}
|
|
1191
|
-
const v = [];
|
|
1192
|
-
for (const p of used) {
|
|
1193
|
-
if (this.map.hasOwnProperty(p))
|
|
1194
|
-
v.push([p, this.map[p]]);
|
|
1195
|
-
}
|
|
1196
|
-
v.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
1197
|
-
return v;
|
|
1198
1078
|
}
|
|
1079
|
+
const key = tripleFastKey(tr);
|
|
1080
|
+
if (key !== null)
|
|
1081
|
+
facts.__keySet.add(key);
|
|
1199
1082
|
}
|
|
1200
|
-
function
|
|
1201
|
-
|
|
1202
|
-
if (
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
out.push(...collectIrisInTerm(x));
|
|
1213
|
-
}
|
|
1214
|
-
else if (t instanceof OpenListTerm) {
|
|
1215
|
-
for (const x of t.prefix)
|
|
1216
|
-
out.push(...collectIrisInTerm(x));
|
|
1217
|
-
}
|
|
1218
|
-
else if (t instanceof GraphTerm) {
|
|
1219
|
-
for (const tr of t.triples) {
|
|
1220
|
-
out.push(...collectIrisInTerm(tr.s));
|
|
1221
|
-
out.push(...collectIrisInTerm(tr.p));
|
|
1222
|
-
out.push(...collectIrisInTerm(tr.o));
|
|
1083
|
+
function candidateFacts(facts, goal) {
|
|
1084
|
+
ensureFactIndexes(facts);
|
|
1085
|
+
if (goal.p instanceof Iri) {
|
|
1086
|
+
const pk = goal.p.value;
|
|
1087
|
+
const sk = termFastKey(goal.s);
|
|
1088
|
+
const ok = termFastKey(goal.o);
|
|
1089
|
+
/** @type {Triple[] | null} */
|
|
1090
|
+
let byPS = null;
|
|
1091
|
+
if (sk !== null) {
|
|
1092
|
+
const ps = facts.__byPS.get(pk);
|
|
1093
|
+
if (ps)
|
|
1094
|
+
byPS = ps.get(sk) || null;
|
|
1223
1095
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
}
|
|
1231
|
-
else if (t instanceof ListTerm) {
|
|
1232
|
-
for (const x of t.elems)
|
|
1233
|
-
collectVarsInTerm(x, acc);
|
|
1234
|
-
}
|
|
1235
|
-
else if (t instanceof OpenListTerm) {
|
|
1236
|
-
for (const x of t.prefix)
|
|
1237
|
-
collectVarsInTerm(x, acc);
|
|
1238
|
-
acc.add(t.tailVar);
|
|
1239
|
-
}
|
|
1240
|
-
else if (t instanceof GraphTerm) {
|
|
1241
|
-
for (const tr of t.triples) {
|
|
1242
|
-
collectVarsInTerm(tr.s, acc);
|
|
1243
|
-
collectVarsInTerm(tr.p, acc);
|
|
1244
|
-
collectVarsInTerm(tr.o, acc);
|
|
1096
|
+
/** @type {Triple[] | null} */
|
|
1097
|
+
let byPO = null;
|
|
1098
|
+
if (ok !== null) {
|
|
1099
|
+
const po = facts.__byPO.get(pk);
|
|
1100
|
+
if (po)
|
|
1101
|
+
byPO = po.get(ok) || null;
|
|
1245
1102
|
}
|
|
1103
|
+
if (byPS && byPO)
|
|
1104
|
+
return byPS.length <= byPO.length ? byPS : byPO;
|
|
1105
|
+
if (byPS)
|
|
1106
|
+
return byPS;
|
|
1107
|
+
if (byPO)
|
|
1108
|
+
return byPO;
|
|
1109
|
+
return facts.__byPred.get(pk) || [];
|
|
1246
1110
|
}
|
|
1111
|
+
return facts;
|
|
1247
1112
|
}
|
|
1248
|
-
function
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1113
|
+
function hasFactIndexed(facts, tr) {
|
|
1114
|
+
ensureFactIndexes(facts);
|
|
1115
|
+
const key = tripleFastKey(tr);
|
|
1116
|
+
if (key !== null)
|
|
1117
|
+
return facts.__keySet.has(key);
|
|
1118
|
+
if (tr.p instanceof Iri) {
|
|
1119
|
+
const pk = tr.p.value;
|
|
1120
|
+
const ok = termFastKey(tr.o);
|
|
1121
|
+
if (ok !== null) {
|
|
1122
|
+
const po = facts.__byPO.get(pk);
|
|
1123
|
+
if (po) {
|
|
1124
|
+
const pob = po.get(ok) || [];
|
|
1125
|
+
// Facts are all in the same graph. Different blank node labels represent
|
|
1126
|
+
// different existentials unless explicitly connected. Do NOT treat
|
|
1127
|
+
// triples as duplicates modulo blank renaming, or you'll incorrectly
|
|
1128
|
+
// drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
|
|
1129
|
+
return pob.some((t) => triplesEqual(t, tr));
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const pb = facts.__byPred.get(pk) || [];
|
|
1133
|
+
return pb.some((t) => triplesEqual(t, tr));
|
|
1254
1134
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1135
|
+
// Non-IRI predicate: fall back to strict triple equality.
|
|
1136
|
+
return facts.some((t) => triplesEqual(t, tr));
|
|
1137
|
+
}
|
|
1138
|
+
function pushFactIndexed(facts, tr) {
|
|
1139
|
+
ensureFactIndexes(facts);
|
|
1140
|
+
facts.push(tr);
|
|
1141
|
+
indexFact(facts, tr);
|
|
1142
|
+
}
|
|
1143
|
+
function ensureBackRuleIndexes(backRules) {
|
|
1144
|
+
if (backRules.__byHeadPred && backRules.__wildHeadPred)
|
|
1145
|
+
return;
|
|
1146
|
+
Object.defineProperty(backRules, '__byHeadPred', {
|
|
1147
|
+
value: new Map(),
|
|
1148
|
+
enumerable: false,
|
|
1149
|
+
writable: true,
|
|
1150
|
+
});
|
|
1151
|
+
Object.defineProperty(backRules, '__wildHeadPred', {
|
|
1152
|
+
value: [],
|
|
1153
|
+
enumerable: false,
|
|
1154
|
+
writable: true,
|
|
1155
|
+
});
|
|
1156
|
+
for (const r of backRules)
|
|
1157
|
+
indexBackRule(backRules, r);
|
|
1158
|
+
}
|
|
1159
|
+
function indexBackRule(backRules, r) {
|
|
1160
|
+
if (!r || !r.conclusion || r.conclusion.length !== 1)
|
|
1161
|
+
return;
|
|
1162
|
+
const head = r.conclusion[0];
|
|
1163
|
+
if (head && head.p instanceof Iri) {
|
|
1164
|
+
const k = head.p.value;
|
|
1165
|
+
let bucket = backRules.__byHeadPred.get(k);
|
|
1166
|
+
if (!bucket) {
|
|
1167
|
+
bucket = [];
|
|
1168
|
+
backRules.__byHeadPred.set(k, bucket);
|
|
1169
|
+
}
|
|
1170
|
+
bucket.push(r);
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
backRules.__wildHeadPred.push(r);
|
|
1259
1174
|
}
|
|
1260
|
-
return acc;
|
|
1261
1175
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1176
|
+
// ===========================================================================
|
|
1177
|
+
// Special predicate helpers
|
|
1178
|
+
// ===========================================================================
|
|
1179
|
+
function isRdfTypePred(p) {
|
|
1180
|
+
return p instanceof Iri && p.value === RDF_NS + 'type';
|
|
1181
|
+
}
|
|
1182
|
+
function isOwlSameAsPred(t) {
|
|
1183
|
+
return t instanceof Iri && t.value === OWL_NS + 'sameAs';
|
|
1184
|
+
}
|
|
1185
|
+
function isLogImplies(p) {
|
|
1186
|
+
return p instanceof Iri && p.value === LOG_NS + 'implies';
|
|
1187
|
+
}
|
|
1188
|
+
function isLogImpliedBy(p) {
|
|
1189
|
+
return p instanceof Iri && p.value === LOG_NS + 'impliedBy';
|
|
1190
|
+
}
|
|
1191
|
+
// ===========================================================================
|
|
1192
|
+
// Constraint / "test" builtins
|
|
1193
|
+
// ===========================================================================
|
|
1194
|
+
function isConstraintBuiltin(tr) {
|
|
1195
|
+
if (!(tr.p instanceof Iri))
|
|
1196
|
+
return false;
|
|
1197
|
+
const v = tr.p.value;
|
|
1198
|
+
// math: numeric comparisons (no new bindings, just tests)
|
|
1199
|
+
if (v === MATH_NS + 'equalTo' ||
|
|
1200
|
+
v === MATH_NS + 'greaterThan' ||
|
|
1201
|
+
v === MATH_NS + 'lessThan' ||
|
|
1202
|
+
v === MATH_NS + 'notEqualTo' ||
|
|
1203
|
+
v === MATH_NS + 'notGreaterThan' ||
|
|
1204
|
+
v === MATH_NS + 'notLessThan') {
|
|
1205
|
+
return true;
|
|
1265
1206
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1207
|
+
// list: membership test with no bindings
|
|
1208
|
+
if (v === LIST_NS + 'notMember') {
|
|
1209
|
+
return true;
|
|
1269
1210
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1211
|
+
// log: tests that are purely constraints (no new bindings)
|
|
1212
|
+
if (v === LOG_NS + 'forAllIn' ||
|
|
1213
|
+
v === LOG_NS + 'notEqualTo' ||
|
|
1214
|
+
v === LOG_NS + 'notIncludes' ||
|
|
1215
|
+
v === LOG_NS + 'outputString') {
|
|
1216
|
+
return true;
|
|
1273
1217
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1218
|
+
// string: relational / membership style tests (no bindings)
|
|
1219
|
+
if (v === STRING_NS + 'contains' ||
|
|
1220
|
+
v === STRING_NS + 'containsIgnoringCase' ||
|
|
1221
|
+
v === STRING_NS + 'endsWith' ||
|
|
1222
|
+
v === STRING_NS + 'equalIgnoringCase' ||
|
|
1223
|
+
v === STRING_NS + 'greaterThan' ||
|
|
1224
|
+
v === STRING_NS + 'lessThan' ||
|
|
1225
|
+
v === STRING_NS + 'matches' ||
|
|
1226
|
+
v === STRING_NS + 'notEqualIgnoringCase' ||
|
|
1227
|
+
v === STRING_NS + 'notGreaterThan' ||
|
|
1228
|
+
v === STRING_NS + 'notLessThan' ||
|
|
1229
|
+
v === STRING_NS + 'notMatches' ||
|
|
1230
|
+
v === STRING_NS + 'startsWith') {
|
|
1231
|
+
return true;
|
|
1280
1232
|
}
|
|
1233
|
+
return false;
|
|
1281
1234
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1235
|
+
// Move constraint builtins to the end of the rule premise.
|
|
1236
|
+
// This is a simple "delaying" strategy similar in spirit to Prolog's when/2:
|
|
1237
|
+
// - normal goals first (can bind variables),
|
|
1238
|
+
// - pure test / constraint builtins last (checked once bindings are in place).
|
|
1239
|
+
function reorderPremiseForConstraints(premise) {
|
|
1240
|
+
if (!premise || premise.length === 0)
|
|
1241
|
+
return premise;
|
|
1242
|
+
const normal = [];
|
|
1243
|
+
const delayed = [];
|
|
1244
|
+
for (const tr of premise) {
|
|
1245
|
+
if (isConstraintBuiltin(tr))
|
|
1246
|
+
delayed.push(tr);
|
|
1247
|
+
else
|
|
1248
|
+
normal.push(tr);
|
|
1288
1249
|
}
|
|
1289
|
-
return
|
|
1250
|
+
return normal.concat(delayed);
|
|
1290
1251
|
}
|
|
1252
|
+
// @ts-nocheck
|
|
1253
|
+
/* eslint-disable */
|
|
1291
1254
|
// ===========================================================================
|
|
1292
|
-
//
|
|
1255
|
+
// N3 lexer + parser
|
|
1293
1256
|
// ===========================================================================
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
this.
|
|
1300
|
-
this.
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
return this.toks[this.pos];
|
|
1257
|
+
// ===========================================================================
|
|
1258
|
+
// LEXER
|
|
1259
|
+
// ===========================================================================
|
|
1260
|
+
class Token {
|
|
1261
|
+
constructor(typ, value = null, offset = null) {
|
|
1262
|
+
this.typ = typ;
|
|
1263
|
+
this.value = value;
|
|
1264
|
+
// Codepoint offset in the original source (Array.from(text) index).
|
|
1265
|
+
this.offset = offset;
|
|
1304
1266
|
}
|
|
1305
|
-
|
|
1306
|
-
const
|
|
1307
|
-
this.
|
|
1308
|
-
|
|
1267
|
+
toString() {
|
|
1268
|
+
const loc = typeof this.offset === 'number' ? `@${this.offset}` : '';
|
|
1269
|
+
if (this.value == null)
|
|
1270
|
+
return `Token(${this.typ}${loc})`;
|
|
1271
|
+
return `Token(${this.typ}${loc}, ${JSON.stringify(this.value)})`;
|
|
1309
1272
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1273
|
+
}
|
|
1274
|
+
class N3SyntaxError extends SyntaxError {
|
|
1275
|
+
constructor(message, offset = null) {
|
|
1276
|
+
super(message);
|
|
1277
|
+
this.name = 'N3SyntaxError';
|
|
1278
|
+
this.offset = offset;
|
|
1313
1279
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1280
|
+
}
|
|
1281
|
+
function isWs(c) {
|
|
1282
|
+
return /\s/.test(c);
|
|
1283
|
+
}
|
|
1284
|
+
function isNameChar(c) {
|
|
1285
|
+
return /[0-9A-Za-z_\-:]/.test(c);
|
|
1286
|
+
}
|
|
1287
|
+
function decodeN3StringEscapes(s) {
|
|
1288
|
+
let out = '';
|
|
1289
|
+
for (let i = 0; i < s.length; i++) {
|
|
1290
|
+
const c = s[i];
|
|
1291
|
+
if (c !== '\\') {
|
|
1292
|
+
out += c;
|
|
1293
|
+
continue;
|
|
1318
1294
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
this.next(); // consume BASE keyword
|
|
1355
|
-
this.parseSparqlBaseDirective();
|
|
1356
|
-
}
|
|
1357
|
-
else {
|
|
1358
|
-
const first = this.parseTerm();
|
|
1359
|
-
if (this.peek().typ === 'OpImplies') {
|
|
1360
|
-
this.next();
|
|
1361
|
-
const second = this.parseTerm();
|
|
1362
|
-
this.expectDot();
|
|
1363
|
-
forwardRules.push(this.makeRule(first, second, true));
|
|
1295
|
+
if (i + 1 >= s.length) {
|
|
1296
|
+
out += '\\';
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
const e = s[++i];
|
|
1300
|
+
switch (e) {
|
|
1301
|
+
case 't':
|
|
1302
|
+
out += '\t';
|
|
1303
|
+
break;
|
|
1304
|
+
case 'n':
|
|
1305
|
+
out += '\n';
|
|
1306
|
+
break;
|
|
1307
|
+
case 'r':
|
|
1308
|
+
out += '\r';
|
|
1309
|
+
break;
|
|
1310
|
+
case 'b':
|
|
1311
|
+
out += '\b';
|
|
1312
|
+
break;
|
|
1313
|
+
case 'f':
|
|
1314
|
+
out += '\f';
|
|
1315
|
+
break;
|
|
1316
|
+
case '"':
|
|
1317
|
+
out += '"';
|
|
1318
|
+
break;
|
|
1319
|
+
case "'":
|
|
1320
|
+
out += "'";
|
|
1321
|
+
break;
|
|
1322
|
+
case '\\':
|
|
1323
|
+
out += '\\';
|
|
1324
|
+
break;
|
|
1325
|
+
case 'u': {
|
|
1326
|
+
const hex = s.slice(i + 1, i + 5);
|
|
1327
|
+
if (/^[0-9A-Fa-f]{4}$/.test(hex)) {
|
|
1328
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
1329
|
+
i += 4;
|
|
1364
1330
|
}
|
|
1365
|
-
else
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1331
|
+
else {
|
|
1332
|
+
out += '\\u';
|
|
1333
|
+
}
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
case 'U': {
|
|
1337
|
+
const hex = s.slice(i + 1, i + 9);
|
|
1338
|
+
if (/^[0-9A-Fa-f]{8}$/.test(hex)) {
|
|
1339
|
+
const cp = parseInt(hex, 16);
|
|
1340
|
+
if (cp >= 0 && cp <= 0x10ffff)
|
|
1341
|
+
out += String.fromCodePoint(cp);
|
|
1342
|
+
else
|
|
1343
|
+
out += '\\U' + hex;
|
|
1344
|
+
i += 8;
|
|
1370
1345
|
}
|
|
1371
1346
|
else {
|
|
1372
|
-
|
|
1373
|
-
if (this.peek().typ === 'Dot') {
|
|
1374
|
-
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
1375
|
-
// So a bare subject followed by '.' is syntactically valid.
|
|
1376
|
-
// If the subject was a path / property-list that generated helper triples,
|
|
1377
|
-
// we emit those; otherwise this statement contributes no triples.
|
|
1378
|
-
more = [];
|
|
1379
|
-
if (this.pendingTriples.length > 0) {
|
|
1380
|
-
more = this.pendingTriples;
|
|
1381
|
-
this.pendingTriples = [];
|
|
1382
|
-
}
|
|
1383
|
-
this.next(); // consume '.'
|
|
1384
|
-
}
|
|
1385
|
-
else {
|
|
1386
|
-
more = this.parsePredicateObjectList(first);
|
|
1387
|
-
this.expectDot();
|
|
1388
|
-
}
|
|
1389
|
-
// normalize explicit log:implies / log:impliedBy at top-level
|
|
1390
|
-
for (const tr of more) {
|
|
1391
|
-
if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
1392
|
-
forwardRules.push(this.makeRule(tr.s, tr.o, true));
|
|
1393
|
-
}
|
|
1394
|
-
else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
1395
|
-
backwardRules.push(this.makeRule(tr.s, tr.o, false));
|
|
1396
|
-
}
|
|
1397
|
-
else {
|
|
1398
|
-
triples.push(tr);
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1347
|
+
out += '\\U';
|
|
1401
1348
|
}
|
|
1349
|
+
break;
|
|
1402
1350
|
}
|
|
1351
|
+
default:
|
|
1352
|
+
// preserve unknown escapes
|
|
1353
|
+
out += '\\' + e;
|
|
1403
1354
|
}
|
|
1404
|
-
return [this.prefixes, triples, forwardRules, backwardRules];
|
|
1405
1355
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
this.prefixes.set(prefName, '');
|
|
1417
|
-
}
|
|
1418
|
-
return;
|
|
1419
|
-
}
|
|
1420
|
-
const tok2 = this.next();
|
|
1421
|
-
let iri;
|
|
1422
|
-
if (tok2.typ === 'IriRef') {
|
|
1423
|
-
iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
|
|
1424
|
-
}
|
|
1425
|
-
else if (tok2.typ === 'Ident') {
|
|
1426
|
-
iri = this.prefixes.expandQName(tok2.value || '');
|
|
1427
|
-
}
|
|
1428
|
-
else {
|
|
1429
|
-
this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
|
|
1430
|
-
}
|
|
1431
|
-
this.expectDot();
|
|
1432
|
-
this.prefixes.set(prefName, iri);
|
|
1356
|
+
return out;
|
|
1357
|
+
}
|
|
1358
|
+
function lex(inputText) {
|
|
1359
|
+
const chars = Array.from(inputText);
|
|
1360
|
+
const n = chars.length;
|
|
1361
|
+
let i = 0;
|
|
1362
|
+
const tokens = [];
|
|
1363
|
+
function peek(offset = 0) {
|
|
1364
|
+
const j = i + offset;
|
|
1365
|
+
return j >= 0 && j < n ? chars[j] : null;
|
|
1433
1366
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1367
|
+
while (i < n) {
|
|
1368
|
+
let c = peek();
|
|
1369
|
+
if (c === null)
|
|
1370
|
+
break;
|
|
1371
|
+
// 1) Whitespace
|
|
1372
|
+
if (isWs(c)) {
|
|
1373
|
+
i++;
|
|
1374
|
+
continue;
|
|
1442
1375
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1376
|
+
// 2) Comments starting with '#'
|
|
1377
|
+
if (c === '#') {
|
|
1378
|
+
while (i < n && chars[i] !== '\n' && chars[i] !== '\r')
|
|
1379
|
+
i++;
|
|
1380
|
+
continue;
|
|
1445
1381
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1382
|
+
// 3) Two-character operators: => and <=
|
|
1383
|
+
if (c === '=') {
|
|
1384
|
+
if (peek(1) === '>') {
|
|
1385
|
+
tokens.push(new Token('OpImplies', null, i));
|
|
1386
|
+
i += 2;
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
// N3 syntactic sugar: '=' means owl:sameAs
|
|
1391
|
+
tokens.push(new Token('Equals', null, i));
|
|
1392
|
+
i += 1;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1454
1395
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1396
|
+
if (c === '<') {
|
|
1397
|
+
if (peek(1) === '=') {
|
|
1398
|
+
tokens.push(new Token('OpImpliedBy', null, i));
|
|
1399
|
+
i += 2;
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
// N3 predicate inversion: "<-" (swap subject/object for this predicate)
|
|
1403
|
+
if (peek(1) === '-') {
|
|
1404
|
+
tokens.push(new Token('OpPredInvert', null, i));
|
|
1405
|
+
i += 2;
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
// Otherwise IRIREF <...>
|
|
1409
|
+
const start = i;
|
|
1410
|
+
i++; // skip '<'
|
|
1411
|
+
const iriChars = [];
|
|
1412
|
+
while (i < n && chars[i] !== '>') {
|
|
1413
|
+
iriChars.push(chars[i]);
|
|
1414
|
+
i++;
|
|
1415
|
+
}
|
|
1416
|
+
if (i >= n || chars[i] !== '>') {
|
|
1417
|
+
throw new N3SyntaxError('Unterminated IRI <...>', start);
|
|
1418
|
+
}
|
|
1419
|
+
i++; // skip '>'
|
|
1420
|
+
const iri = iriChars.join('');
|
|
1421
|
+
tokens.push(new Token('IriRef', iri, start));
|
|
1422
|
+
continue;
|
|
1461
1423
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1424
|
+
// 4) Path + datatype operators: !, ^, ^^
|
|
1425
|
+
if (c === '!') {
|
|
1426
|
+
tokens.push(new Token('OpPathFwd', null, i));
|
|
1427
|
+
i += 1;
|
|
1428
|
+
continue;
|
|
1464
1429
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1430
|
+
if (c === '^') {
|
|
1431
|
+
if (peek(1) === '^') {
|
|
1432
|
+
tokens.push(new Token('HatHat', null, i));
|
|
1433
|
+
i += 2;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
tokens.push(new Token('OpPathRev', null, i));
|
|
1437
|
+
i += 1;
|
|
1438
|
+
continue;
|
|
1467
1439
|
}
|
|
1468
|
-
//
|
|
1469
|
-
if (
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1440
|
+
// 5) Single-character punctuation
|
|
1441
|
+
if ('{}()[];,.'.includes(c)) {
|
|
1442
|
+
const mapping = {
|
|
1443
|
+
'{': 'LBrace',
|
|
1444
|
+
'}': 'RBrace',
|
|
1445
|
+
'(': 'LParen',
|
|
1446
|
+
')': 'RParen',
|
|
1447
|
+
'[': 'LBracket',
|
|
1448
|
+
']': 'RBracket',
|
|
1449
|
+
';': 'Semicolon',
|
|
1450
|
+
',': 'Comma',
|
|
1451
|
+
'.': 'Dot',
|
|
1452
|
+
};
|
|
1453
|
+
tokens.push(new Token(mapping[c], null, i));
|
|
1454
|
+
i++;
|
|
1455
|
+
continue;
|
|
1479
1456
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1457
|
+
// String literal: short "..." or long """..."""
|
|
1458
|
+
if (c === '"') {
|
|
1459
|
+
const start = i;
|
|
1460
|
+
// Long string literal """ ... """
|
|
1461
|
+
if (peek(1) === '"' && peek(2) === '"') {
|
|
1462
|
+
i += 3; // consume opening """
|
|
1463
|
+
const sChars = [];
|
|
1464
|
+
let closed = false;
|
|
1465
|
+
while (i < n) {
|
|
1466
|
+
const cc = chars[i];
|
|
1467
|
+
// Preserve escapes verbatim (same behavior as short strings)
|
|
1468
|
+
if (cc === '\\') {
|
|
1469
|
+
i++;
|
|
1470
|
+
if (i < n) {
|
|
1471
|
+
const esc = chars[i];
|
|
1472
|
+
i++;
|
|
1473
|
+
sChars.push('\\');
|
|
1474
|
+
sChars.push(esc);
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
sChars.push('\\');
|
|
1478
|
+
}
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
// In long strings, a run of >= 3 delimiter quotes terminates the literal.
|
|
1482
|
+
// Any extra quotes beyond the final 3 are part of the content.
|
|
1483
|
+
if (cc === '"') {
|
|
1484
|
+
let run = 0;
|
|
1485
|
+
while (i + run < n && chars[i + run] === '"')
|
|
1486
|
+
run++;
|
|
1487
|
+
if (run >= 3) {
|
|
1488
|
+
for (let k = 0; k < run - 3; k++)
|
|
1489
|
+
sChars.push('"');
|
|
1490
|
+
i += run; // consume content quotes (if any) + closing delimiter
|
|
1491
|
+
closed = true;
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
for (let k = 0; k < run; k++)
|
|
1495
|
+
sChars.push('"');
|
|
1496
|
+
i += run;
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
sChars.push(cc);
|
|
1500
|
+
i++;
|
|
1501
|
+
}
|
|
1502
|
+
if (!closed)
|
|
1503
|
+
throw new N3SyntaxError('Unterminated long string literal """..."""', start);
|
|
1504
|
+
const raw = '"""' + sChars.join('') + '"""';
|
|
1505
|
+
const decoded = decodeN3StringEscapes(stripQuotes(raw));
|
|
1506
|
+
const s = JSON.stringify(decoded); // canonical short quoted form
|
|
1507
|
+
tokens.push(new Token('Literal', s, start));
|
|
1508
|
+
continue;
|
|
1524
1509
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1510
|
+
// Short string literal " ... "
|
|
1511
|
+
i++; // consume opening "
|
|
1512
|
+
const sChars = [];
|
|
1513
|
+
while (i < n) {
|
|
1514
|
+
let cc = chars[i];
|
|
1515
|
+
i++;
|
|
1516
|
+
if (cc === '\\') {
|
|
1517
|
+
if (i < n) {
|
|
1518
|
+
const esc = chars[i];
|
|
1519
|
+
i++;
|
|
1520
|
+
sChars.push('\\');
|
|
1521
|
+
sChars.push(esc);
|
|
1522
|
+
}
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
if (cc === '"')
|
|
1526
|
+
break;
|
|
1527
|
+
sChars.push(cc);
|
|
1527
1528
|
}
|
|
1529
|
+
const raw = '"' + sChars.join('') + '"';
|
|
1530
|
+
const decoded = decodeN3StringEscapes(stripQuotes(raw));
|
|
1531
|
+
const s = JSON.stringify(decoded); // canonical short quoted form
|
|
1532
|
+
tokens.push(new Token('Literal', s, start));
|
|
1533
|
+
continue;
|
|
1528
1534
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1535
|
+
// String literal: short '...' or long '''...'''
|
|
1536
|
+
if (c === "'") {
|
|
1537
|
+
const start = i;
|
|
1538
|
+
// Long string literal ''' ... '''
|
|
1539
|
+
if (peek(1) === "'" && peek(2) === "'") {
|
|
1540
|
+
i += 3; // consume opening '''
|
|
1541
|
+
const sChars = [];
|
|
1542
|
+
let closed = false;
|
|
1543
|
+
while (i < n) {
|
|
1544
|
+
const cc = chars[i];
|
|
1545
|
+
// Preserve escapes verbatim (same behavior as short strings)
|
|
1546
|
+
if (cc === '\\') {
|
|
1547
|
+
i++;
|
|
1548
|
+
if (i < n) {
|
|
1549
|
+
const esc = chars[i];
|
|
1550
|
+
i++;
|
|
1551
|
+
sChars.push('\\');
|
|
1552
|
+
sChars.push(esc);
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
sChars.push('\\');
|
|
1556
|
+
}
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
// In long strings, a run of >= 3 delimiter quotes terminates the literal.
|
|
1560
|
+
// Any extra quotes beyond the final 3 are part of the content.
|
|
1561
|
+
if (cc === "'") {
|
|
1562
|
+
let run = 0;
|
|
1563
|
+
while (i + run < n && chars[i + run] === "'")
|
|
1564
|
+
run++;
|
|
1565
|
+
if (run >= 3) {
|
|
1566
|
+
for (let k = 0; k < run - 3; k++)
|
|
1567
|
+
sChars.push("'");
|
|
1568
|
+
i += run; // consume content quotes (if any) + closing delimiter
|
|
1569
|
+
closed = true;
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
for (let k = 0; k < run; k++)
|
|
1573
|
+
sChars.push("'");
|
|
1574
|
+
i += run;
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
sChars.push(cc);
|
|
1578
|
+
i++;
|
|
1543
1579
|
}
|
|
1580
|
+
if (!closed)
|
|
1581
|
+
throw new N3SyntaxError("Unterminated long string literal '''...'''", start);
|
|
1582
|
+
const raw = "'''" + sChars.join('') + "'''";
|
|
1583
|
+
const decoded = decodeN3StringEscapes(stripQuotes(raw));
|
|
1584
|
+
const s = JSON.stringify(decoded); // canonical short quoted form
|
|
1585
|
+
tokens.push(new Token('Literal', s, start));
|
|
1586
|
+
continue;
|
|
1544
1587
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
else {
|
|
1560
|
-
this.fail(`Expected datatype after ^^, got ${dtTok.toString()}`, dtTok);
|
|
1588
|
+
// Short string literal ' ... '
|
|
1589
|
+
i++; // consume opening '
|
|
1590
|
+
const sChars = [];
|
|
1591
|
+
while (i < n) {
|
|
1592
|
+
let cc = chars[i];
|
|
1593
|
+
i++;
|
|
1594
|
+
if (cc === '\\') {
|
|
1595
|
+
if (i < n) {
|
|
1596
|
+
const esc = chars[i];
|
|
1597
|
+
i++;
|
|
1598
|
+
sChars.push('\\');
|
|
1599
|
+
sChars.push(esc);
|
|
1600
|
+
}
|
|
1601
|
+
continue;
|
|
1561
1602
|
}
|
|
1562
|
-
|
|
1603
|
+
if (cc === "'")
|
|
1604
|
+
break;
|
|
1605
|
+
sChars.push(cc);
|
|
1563
1606
|
}
|
|
1564
|
-
|
|
1607
|
+
const raw = "'" + sChars.join('') + "'";
|
|
1608
|
+
const decoded = decodeN3StringEscapes(stripQuotes(raw));
|
|
1609
|
+
const s = JSON.stringify(decoded); // canonical short quoted form
|
|
1610
|
+
tokens.push(new Token('Literal', s, start));
|
|
1611
|
+
continue;
|
|
1565
1612
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
elems.push(this.parseTerm());
|
|
1613
|
+
// Variable ?name
|
|
1614
|
+
if (c === '?') {
|
|
1615
|
+
const start = i;
|
|
1616
|
+
i++;
|
|
1617
|
+
const nameChars = [];
|
|
1618
|
+
let cc;
|
|
1619
|
+
while ((cc = peek()) !== null && isNameChar(cc)) {
|
|
1620
|
+
nameChars.push(cc);
|
|
1621
|
+
i++;
|
|
1622
|
+
}
|
|
1623
|
+
const name = nameChars.join('');
|
|
1624
|
+
tokens.push(new Token('Var', name, start));
|
|
1625
|
+
continue;
|
|
1580
1626
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
const iriTok = this.next(); // consume 'id'
|
|
1595
|
-
const iriTerm = this.parseTerm();
|
|
1596
|
-
// N3 note: 'id' form is not meant to be used with blank node identifiers.
|
|
1597
|
-
if (iriTerm instanceof Blank && iriTerm.label.startsWith('_:')) {
|
|
1598
|
-
this.fail("Cannot use 'id' keyword with a blank node identifier inside [...]", iriTok);
|
|
1599
|
-
}
|
|
1600
|
-
// Optional ';' right after the id IRI (tolerated).
|
|
1601
|
-
if (this.peek().typ === 'Semicolon')
|
|
1602
|
-
this.next();
|
|
1603
|
-
// Empty IRI property list: [ id :iri ]
|
|
1604
|
-
if (this.peek().typ === 'RBracket') {
|
|
1605
|
-
this.next();
|
|
1606
|
-
return iriTerm;
|
|
1607
|
-
}
|
|
1608
|
-
const subj = iriTerm;
|
|
1609
|
-
while (true) {
|
|
1610
|
-
let pred;
|
|
1611
|
-
let invert = false;
|
|
1612
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
1613
|
-
this.next();
|
|
1614
|
-
pred = internIri(RDF_NS + 'type');
|
|
1615
|
-
}
|
|
1616
|
-
else if (this.peek().typ === 'OpPredInvert') {
|
|
1617
|
-
this.next(); // "<-"
|
|
1618
|
-
pred = this.parseTerm();
|
|
1619
|
-
invert = true;
|
|
1627
|
+
// Directives: @prefix, @base (and language tags after string literals)
|
|
1628
|
+
if (c === '@') {
|
|
1629
|
+
const start = i;
|
|
1630
|
+
const prevTok = tokens.length ? tokens[tokens.length - 1] : null;
|
|
1631
|
+
const prevWasQuotedLiteral = prevTok && prevTok.typ === 'Literal' && typeof prevTok.value === 'string' && prevTok.value.startsWith('"');
|
|
1632
|
+
i++; // consume '@'
|
|
1633
|
+
if (prevWasQuotedLiteral) {
|
|
1634
|
+
// N3 grammar production LANGTAG:
|
|
1635
|
+
// "@" [a-zA-Z]+ ("-" [a-zA-Z0-9]+)*
|
|
1636
|
+
const tagChars = [];
|
|
1637
|
+
let cc = peek();
|
|
1638
|
+
if (cc === null || !/[A-Za-z]/.test(cc)) {
|
|
1639
|
+
throw new N3SyntaxError("Invalid language tag (expected [A-Za-z] after '@')", start);
|
|
1620
1640
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1641
|
+
while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
|
|
1642
|
+
tagChars.push(cc);
|
|
1643
|
+
i++;
|
|
1623
1644
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1645
|
+
while (peek() === '-') {
|
|
1646
|
+
tagChars.push('-');
|
|
1647
|
+
i++; // consume '-'
|
|
1648
|
+
const segChars = [];
|
|
1649
|
+
while ((cc = peek()) !== null && /[A-Za-z0-9]/.test(cc)) {
|
|
1650
|
+
segChars.push(cc);
|
|
1651
|
+
i++;
|
|
1652
|
+
}
|
|
1653
|
+
if (!segChars.length) {
|
|
1654
|
+
throw new N3SyntaxError("Invalid language tag (expected [A-Za-z0-9]+ after '-')", start);
|
|
1655
|
+
}
|
|
1656
|
+
tagChars.push(...segChars);
|
|
1628
1657
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1658
|
+
tokens.push(new Token('LangTag', tagChars.join(''), start));
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
// Otherwise, treat as a directive (@prefix, @base)
|
|
1662
|
+
const wordChars = [];
|
|
1663
|
+
let cc;
|
|
1664
|
+
while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
|
|
1665
|
+
wordChars.push(cc);
|
|
1666
|
+
i++;
|
|
1667
|
+
}
|
|
1668
|
+
const word = wordChars.join('');
|
|
1669
|
+
if (word === 'prefix')
|
|
1670
|
+
tokens.push(new Token('AtPrefix', null, start));
|
|
1671
|
+
else if (word === 'base')
|
|
1672
|
+
tokens.push(new Token('AtBase', null, start));
|
|
1673
|
+
else
|
|
1674
|
+
throw new N3SyntaxError(`Unknown directive @${word}`, start);
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
// 6) Numeric literal (integer or float)
|
|
1678
|
+
if (/[0-9]/.test(c) || (c === '-' && peek(1) !== null && /[0-9]/.test(peek(1)))) {
|
|
1679
|
+
const start = i;
|
|
1680
|
+
const numChars = [c];
|
|
1681
|
+
i++;
|
|
1682
|
+
while (i < n) {
|
|
1683
|
+
const cc = chars[i];
|
|
1684
|
+
if (/[0-9]/.test(cc)) {
|
|
1685
|
+
numChars.push(cc);
|
|
1686
|
+
i++;
|
|
1687
|
+
continue;
|
|
1631
1688
|
}
|
|
1632
|
-
if (
|
|
1633
|
-
|
|
1634
|
-
|
|
1689
|
+
if (cc === '.') {
|
|
1690
|
+
if (i + 1 < n && /[0-9]/.test(chars[i + 1])) {
|
|
1691
|
+
numChars.push('.');
|
|
1692
|
+
i++;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
else {
|
|
1635
1696
|
break;
|
|
1636
|
-
|
|
1697
|
+
}
|
|
1637
1698
|
}
|
|
1638
1699
|
break;
|
|
1639
1700
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1701
|
+
// Optional exponent part: e.g., 1e0, 1.1e-3, 1.1E+0
|
|
1702
|
+
if (i < n && (chars[i] === 'e' || chars[i] === 'E')) {
|
|
1703
|
+
let j = i + 1;
|
|
1704
|
+
if (j < n && (chars[j] === '+' || chars[j] === '-'))
|
|
1705
|
+
j++;
|
|
1706
|
+
if (j < n && /[0-9]/.test(chars[j])) {
|
|
1707
|
+
numChars.push(chars[i]); // e/E
|
|
1708
|
+
i++;
|
|
1709
|
+
if (i < n && (chars[i] === '+' || chars[i] === '-')) {
|
|
1710
|
+
numChars.push(chars[i]);
|
|
1711
|
+
i++;
|
|
1712
|
+
}
|
|
1713
|
+
while (i < n && /[0-9]/.test(chars[i])) {
|
|
1714
|
+
numChars.push(chars[i]);
|
|
1715
|
+
i++;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1642
1718
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1719
|
+
tokens.push(new Token('Literal', numChars.join(''), start));
|
|
1720
|
+
continue;
|
|
1645
1721
|
}
|
|
1646
|
-
//
|
|
1647
|
-
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
while (
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
let invert = false;
|
|
1654
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
1655
|
-
this.next();
|
|
1656
|
-
pred = internIri(RDF_NS + 'type');
|
|
1657
|
-
}
|
|
1658
|
-
else if (this.peek().typ === 'OpPredInvert') {
|
|
1659
|
-
this.next(); // consume "<-"
|
|
1660
|
-
pred = this.parseTerm();
|
|
1661
|
-
invert = true;
|
|
1662
|
-
}
|
|
1663
|
-
else {
|
|
1664
|
-
pred = this.parseTerm();
|
|
1665
|
-
}
|
|
1666
|
-
// Object list: o1, o2, ...
|
|
1667
|
-
const objs = [this.parseTerm()];
|
|
1668
|
-
while (this.peek().typ === 'Comma') {
|
|
1669
|
-
this.next();
|
|
1670
|
-
objs.push(this.parseTerm());
|
|
1671
|
-
}
|
|
1672
|
-
for (const o of objs) {
|
|
1673
|
-
this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
|
|
1674
|
-
}
|
|
1675
|
-
if (this.peek().typ === 'Semicolon') {
|
|
1676
|
-
this.next();
|
|
1677
|
-
if (this.peek().typ === 'RBracket')
|
|
1678
|
-
break;
|
|
1679
|
-
continue;
|
|
1680
|
-
}
|
|
1681
|
-
break;
|
|
1722
|
+
// 7) Identifiers / keywords / QNames
|
|
1723
|
+
const start = i;
|
|
1724
|
+
const wordChars = [];
|
|
1725
|
+
let cc;
|
|
1726
|
+
while ((cc = peek()) !== null && isNameChar(cc)) {
|
|
1727
|
+
wordChars.push(cc);
|
|
1728
|
+
i++;
|
|
1682
1729
|
}
|
|
1683
|
-
if (
|
|
1684
|
-
|
|
1730
|
+
if (!wordChars.length) {
|
|
1731
|
+
throw new N3SyntaxError(`Unexpected char: ${JSON.stringify(c)}`, i);
|
|
1685
1732
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1733
|
+
const word = wordChars.join('');
|
|
1734
|
+
if (word === 'true' || word === 'false') {
|
|
1735
|
+
tokens.push(new Token('Literal', word, start));
|
|
1688
1736
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
const left = this.parseTerm();
|
|
1695
|
-
if (this.peek().typ === 'OpImplies') {
|
|
1696
|
-
this.next();
|
|
1697
|
-
const right = this.parseTerm();
|
|
1698
|
-
const pred = internIri(LOG_NS + 'implies');
|
|
1699
|
-
triples.push(new Triple(left, pred, right));
|
|
1700
|
-
if (this.peek().typ === 'Dot')
|
|
1701
|
-
this.next();
|
|
1702
|
-
else if (this.peek().typ === 'RBrace') {
|
|
1703
|
-
// ok
|
|
1704
|
-
}
|
|
1705
|
-
else {
|
|
1706
|
-
this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
else if (this.peek().typ === 'OpImpliedBy') {
|
|
1710
|
-
this.next();
|
|
1711
|
-
const right = this.parseTerm();
|
|
1712
|
-
const pred = internIri(LOG_NS + 'impliedBy');
|
|
1713
|
-
triples.push(new Triple(left, pred, right));
|
|
1714
|
-
if (this.peek().typ === 'Dot')
|
|
1715
|
-
this.next();
|
|
1716
|
-
else if (this.peek().typ === 'RBrace') {
|
|
1717
|
-
// ok
|
|
1718
|
-
}
|
|
1719
|
-
else {
|
|
1720
|
-
this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
else {
|
|
1724
|
-
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
1725
|
-
// So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
|
|
1726
|
-
if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
|
|
1727
|
-
if (this.pendingTriples.length > 0) {
|
|
1728
|
-
triples.push(...this.pendingTriples);
|
|
1729
|
-
this.pendingTriples = [];
|
|
1730
|
-
}
|
|
1731
|
-
if (this.peek().typ === 'Dot')
|
|
1732
|
-
this.next();
|
|
1733
|
-
continue;
|
|
1734
|
-
}
|
|
1735
|
-
triples.push(...this.parsePredicateObjectList(left));
|
|
1736
|
-
if (this.peek().typ === 'Dot')
|
|
1737
|
-
this.next();
|
|
1738
|
-
else if (this.peek().typ === 'RBrace') {
|
|
1739
|
-
// ok
|
|
1740
|
-
}
|
|
1741
|
-
else {
|
|
1742
|
-
this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1737
|
+
else if ([...word].every((ch) => /[0-9.\-]/.test(ch))) {
|
|
1738
|
+
tokens.push(new Token('Literal', word, start));
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
tokens.push(new Token('Ident', word, start));
|
|
1745
1742
|
}
|
|
1746
|
-
this.next(); // consume '}'
|
|
1747
|
-
return new GraphTerm(triples);
|
|
1748
1743
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
1760
|
-
this.next();
|
|
1761
|
-
verb = internIri(RDF_NS + 'type');
|
|
1762
|
-
}
|
|
1763
|
-
else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
1764
|
-
// N3 syntactic sugar: "S has P O." means "S P O."
|
|
1765
|
-
this.next(); // consume "has"
|
|
1766
|
-
verb = this.parseTerm();
|
|
1767
|
-
}
|
|
1768
|
-
else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
1769
|
-
// N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
|
|
1770
|
-
this.next(); // consume "is"
|
|
1771
|
-
verb = this.parseTerm();
|
|
1772
|
-
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
1773
|
-
this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
1774
|
-
}
|
|
1775
|
-
this.next(); // consume "of"
|
|
1776
|
-
invert = true;
|
|
1777
|
-
}
|
|
1778
|
-
else if (this.peek().typ === 'OpPredInvert') {
|
|
1779
|
-
this.next(); // "<-"
|
|
1780
|
-
verb = this.parseTerm();
|
|
1781
|
-
invert = true;
|
|
1782
|
-
}
|
|
1783
|
-
else {
|
|
1784
|
-
verb = this.parseTerm();
|
|
1785
|
-
}
|
|
1786
|
-
const objects = this.parseObjectList();
|
|
1787
|
-
// If VERB or OBJECTS contained paths, their helper triples must come
|
|
1788
|
-
// before the triples that consume the path results (Easter depends on this).
|
|
1789
|
-
if (this.pendingTriples.length > 0) {
|
|
1790
|
-
out.push(...this.pendingTriples);
|
|
1791
|
-
this.pendingTriples = [];
|
|
1792
|
-
}
|
|
1793
|
-
for (const o of objects) {
|
|
1794
|
-
out.push(new Triple(invert ? o : subject, verb, invert ? subject : o));
|
|
1795
|
-
}
|
|
1796
|
-
if (this.peek().typ === 'Semicolon') {
|
|
1797
|
-
this.next();
|
|
1798
|
-
if (this.peek().typ === 'Dot')
|
|
1799
|
-
break;
|
|
1800
|
-
continue;
|
|
1801
|
-
}
|
|
1802
|
-
break;
|
|
1803
|
-
}
|
|
1804
|
-
return out;
|
|
1744
|
+
tokens.push(new Token('EOF', null, n));
|
|
1745
|
+
return tokens;
|
|
1746
|
+
}
|
|
1747
|
+
// ===========================================================================
|
|
1748
|
+
// PREFIX ENVIRONMENT
|
|
1749
|
+
// ===========================================================================
|
|
1750
|
+
class PrefixEnv {
|
|
1751
|
+
constructor(map, baseIri) {
|
|
1752
|
+
this.map = map || {}; // prefix -> IRI (including "" for @prefix :)
|
|
1753
|
+
this.baseIri = baseIri || ''; // base IRI for resolving <relative>
|
|
1805
1754
|
}
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1755
|
+
static newDefault() {
|
|
1756
|
+
const m = {};
|
|
1757
|
+
m['rdf'] = RDF_NS;
|
|
1758
|
+
m['rdfs'] = RDFS_NS;
|
|
1759
|
+
m['xsd'] = XSD_NS;
|
|
1760
|
+
m['log'] = LOG_NS;
|
|
1761
|
+
m['math'] = MATH_NS;
|
|
1762
|
+
m['string'] = STRING_NS;
|
|
1763
|
+
m['list'] = LIST_NS;
|
|
1764
|
+
m['time'] = TIME_NS;
|
|
1765
|
+
m['genid'] = SKOLEM_NS;
|
|
1766
|
+
m[''] = ''; // empty prefix default namespace
|
|
1767
|
+
return new PrefixEnv(m, ''); // base IRI starts empty
|
|
1813
1768
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1769
|
+
set(pref, base) {
|
|
1770
|
+
this.map[pref] = base;
|
|
1771
|
+
}
|
|
1772
|
+
setBase(baseIri) {
|
|
1773
|
+
this.baseIri = baseIri || '';
|
|
1774
|
+
}
|
|
1775
|
+
expandQName(q) {
|
|
1776
|
+
if (q.includes(':')) {
|
|
1777
|
+
const [p, local] = q.split(':', 2);
|
|
1778
|
+
const base = this.map[p] || '';
|
|
1779
|
+
if (base)
|
|
1780
|
+
return base + local;
|
|
1781
|
+
return q;
|
|
1823
1782
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1783
|
+
return q;
|
|
1784
|
+
}
|
|
1785
|
+
shrinkIri(iri) {
|
|
1786
|
+
let best = null; // [prefix, local]
|
|
1787
|
+
for (const [p, base] of Object.entries(this.map)) {
|
|
1788
|
+
if (!base)
|
|
1789
|
+
continue;
|
|
1790
|
+
if (iri.startsWith(base)) {
|
|
1791
|
+
const local = iri.slice(base.length);
|
|
1792
|
+
if (!local)
|
|
1793
|
+
continue;
|
|
1794
|
+
const cand = [p, local];
|
|
1795
|
+
if (best === null || cand[1].length < best[1].length)
|
|
1796
|
+
best = cand;
|
|
1828
1797
|
}
|
|
1829
1798
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1837
|
-
else {
|
|
1838
|
-
rawPremise = [];
|
|
1839
|
-
}
|
|
1840
|
-
let rawConclusion;
|
|
1841
|
-
if (conclTerm instanceof GraphTerm) {
|
|
1842
|
-
rawConclusion = conclTerm.triples;
|
|
1843
|
-
}
|
|
1844
|
-
else if (conclTerm instanceof Literal && conclTerm.value === 'false') {
|
|
1845
|
-
rawConclusion = [];
|
|
1846
|
-
}
|
|
1847
|
-
else {
|
|
1848
|
-
rawConclusion = [];
|
|
1849
|
-
}
|
|
1850
|
-
// Blank nodes that occur explicitly in the head (conclusion)
|
|
1851
|
-
const headBlankLabels = collectBlankLabelsInTriples(rawConclusion);
|
|
1852
|
-
const [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
|
|
1853
|
-
// Reorder constraints for *forward* rules.
|
|
1854
|
-
const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
|
|
1855
|
-
return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
|
|
1799
|
+
if (best === null)
|
|
1800
|
+
return null;
|
|
1801
|
+
const [p, local] = best;
|
|
1802
|
+
if (p === '')
|
|
1803
|
+
return `:${local}`;
|
|
1804
|
+
return `${p}:${local}`;
|
|
1856
1805
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1806
|
+
prefixesUsedForOutput(triples) {
|
|
1807
|
+
const used = new Set();
|
|
1808
|
+
for (const t of triples) {
|
|
1809
|
+
const iris = [];
|
|
1810
|
+
iris.push(...collectIrisInTerm(t.s));
|
|
1811
|
+
if (!isRdfTypePred(t.p)) {
|
|
1812
|
+
iris.push(...collectIrisInTerm(t.p));
|
|
1813
|
+
}
|
|
1814
|
+
iris.push(...collectIrisInTerm(t.o));
|
|
1815
|
+
for (const iri of iris) {
|
|
1816
|
+
for (const [p, base] of Object.entries(this.map)) {
|
|
1817
|
+
if (base && iri.startsWith(base))
|
|
1818
|
+
used.add(p);
|
|
1819
|
+
}
|
|
1868
1820
|
}
|
|
1869
|
-
return new Var(mapping[label]);
|
|
1870
|
-
}
|
|
1871
|
-
if (t instanceof ListTerm) {
|
|
1872
|
-
return new ListTerm(t.elems.map((e) => convertTerm(e, mapping, counter)));
|
|
1873
|
-
}
|
|
1874
|
-
if (t instanceof OpenListTerm) {
|
|
1875
|
-
return new OpenListTerm(t.prefix.map((e) => convertTerm(e, mapping, counter)), t.tailVar);
|
|
1876
1821
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1822
|
+
const v = [];
|
|
1823
|
+
for (const p of used) {
|
|
1824
|
+
if (this.map.hasOwnProperty(p))
|
|
1825
|
+
v.push([p, this.map[p]]);
|
|
1880
1826
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
function convertTriple(tr, mapping, counter) {
|
|
1884
|
-
return new Triple(convertTerm(tr.s, mapping, counter), convertTerm(tr.p, mapping, counter), convertTerm(tr.o, mapping, counter));
|
|
1827
|
+
v.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
1828
|
+
return v;
|
|
1885
1829
|
}
|
|
1886
|
-
const mapping = {};
|
|
1887
|
-
const counter = [0];
|
|
1888
|
-
const newPremise = premise.map((tr) => convertTriple(tr, mapping, counter));
|
|
1889
|
-
return [newPremise, conclusion];
|
|
1890
1830
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
// outer fixpoint iteration (non-termination once we do strict duplicate checks).
|
|
1896
|
-
//
|
|
1897
|
-
// We achieve this by optionally keying head-blank allocations by a "firingKey"
|
|
1898
|
-
// (usually derived from the instantiated premises and rule index) and caching
|
|
1899
|
-
// them in a run-global map.
|
|
1900
|
-
function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
|
|
1901
|
-
if (t instanceof Blank) {
|
|
1902
|
-
const label = t.label;
|
|
1903
|
-
// Only skolemize blanks that occur explicitly in the rule head
|
|
1904
|
-
if (!headBlankLabels || !headBlankLabels.has(label)) {
|
|
1905
|
-
return t; // this is a data blank (e.g. bound via ?X), keep it
|
|
1906
|
-
}
|
|
1907
|
-
if (!mapping.hasOwnProperty(label)) {
|
|
1908
|
-
// If we have a global cache keyed by firingKey, use it to ensure
|
|
1909
|
-
// deterministic blank IDs for the same rule+substitution instance.
|
|
1910
|
-
if (globalMap && firingKey) {
|
|
1911
|
-
const gk = `${firingKey}|${label}`;
|
|
1912
|
-
let sk = globalMap.get(gk);
|
|
1913
|
-
if (!sk) {
|
|
1914
|
-
const idx = skCounter[0];
|
|
1915
|
-
skCounter[0] += 1;
|
|
1916
|
-
sk = `_:sk_${idx}`;
|
|
1917
|
-
globalMap.set(gk, sk);
|
|
1918
|
-
}
|
|
1919
|
-
mapping[label] = sk;
|
|
1920
|
-
}
|
|
1921
|
-
else {
|
|
1922
|
-
const idx = skCounter[0];
|
|
1923
|
-
skCounter[0] += 1;
|
|
1924
|
-
mapping[label] = `_:sk_${idx}`;
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
return new Blank(mapping[label]);
|
|
1831
|
+
function collectIrisInTerm(t) {
|
|
1832
|
+
const out = [];
|
|
1833
|
+
if (t instanceof Iri) {
|
|
1834
|
+
out.push(t.value);
|
|
1928
1835
|
}
|
|
1929
|
-
if (t instanceof
|
|
1930
|
-
|
|
1836
|
+
else if (t instanceof Literal) {
|
|
1837
|
+
const [_lex, dt] = literalParts(t.value);
|
|
1838
|
+
if (dt)
|
|
1839
|
+
out.push(dt); // so rdf/xsd prefixes are emitted when only used in ^^...
|
|
1931
1840
|
}
|
|
1932
|
-
if (t instanceof
|
|
1933
|
-
|
|
1841
|
+
else if (t instanceof ListTerm) {
|
|
1842
|
+
for (const x of t.elems)
|
|
1843
|
+
out.push(...collectIrisInTerm(x));
|
|
1934
1844
|
}
|
|
1935
|
-
if (t instanceof
|
|
1936
|
-
|
|
1845
|
+
else if (t instanceof OpenListTerm) {
|
|
1846
|
+
for (const x of t.prefix)
|
|
1847
|
+
out.push(...collectIrisInTerm(x));
|
|
1937
1848
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
// ===========================================================================
|
|
1944
|
-
// Alpha equivalence helpers
|
|
1945
|
-
// ===========================================================================
|
|
1946
|
-
function termsEqual(a, b) {
|
|
1947
|
-
if (a === b)
|
|
1948
|
-
return true;
|
|
1949
|
-
if (!a || !b)
|
|
1950
|
-
return false;
|
|
1951
|
-
if (a.constructor !== b.constructor)
|
|
1952
|
-
return false;
|
|
1953
|
-
if (a instanceof Iri)
|
|
1954
|
-
return a.value === b.value;
|
|
1955
|
-
if (a instanceof Literal) {
|
|
1956
|
-
if (a.value === b.value)
|
|
1957
|
-
return true;
|
|
1958
|
-
// Plain "abc" == "abc"^^xsd:string (but not language-tagged strings)
|
|
1959
|
-
if (literalsEquivalentAsXsdString(a.value, b.value))
|
|
1960
|
-
return true;
|
|
1961
|
-
// Keep in sync with unifyTerm(): numeric-value equality, datatype-aware.
|
|
1962
|
-
const ai = parseNumericLiteralInfo(a);
|
|
1963
|
-
const bi = parseNumericLiteralInfo(b);
|
|
1964
|
-
if (ai && bi) {
|
|
1965
|
-
// Same datatype => compare values
|
|
1966
|
-
if (ai.dt === bi.dt) {
|
|
1967
|
-
if (ai.kind === 'bigint' && bi.kind === 'bigint')
|
|
1968
|
-
return ai.value === bi.value;
|
|
1969
|
-
const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
|
|
1970
|
-
const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
|
|
1971
|
-
return !Number.isNaN(an) && !Number.isNaN(bn) && an === bn;
|
|
1972
|
-
}
|
|
1849
|
+
else if (t instanceof GraphTerm) {
|
|
1850
|
+
for (const tr of t.triples) {
|
|
1851
|
+
out.push(...collectIrisInTerm(tr.s));
|
|
1852
|
+
out.push(...collectIrisInTerm(tr.p));
|
|
1853
|
+
out.push(...collectIrisInTerm(tr.o));
|
|
1973
1854
|
}
|
|
1974
|
-
return false;
|
|
1975
1855
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
if (a.elems.length !== b.elems.length)
|
|
1982
|
-
return false;
|
|
1983
|
-
for (let i = 0; i < a.elems.length; i++) {
|
|
1984
|
-
if (!termsEqual(a.elems[i], b.elems[i]))
|
|
1985
|
-
return false;
|
|
1986
|
-
}
|
|
1987
|
-
return true;
|
|
1856
|
+
return out;
|
|
1857
|
+
}
|
|
1858
|
+
function collectVarsInTerm(t, acc) {
|
|
1859
|
+
if (t instanceof Var) {
|
|
1860
|
+
acc.add(t.name);
|
|
1988
1861
|
}
|
|
1989
|
-
if (
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
for (
|
|
1995
|
-
|
|
1996
|
-
|
|
1862
|
+
else if (t instanceof ListTerm) {
|
|
1863
|
+
for (const x of t.elems)
|
|
1864
|
+
collectVarsInTerm(x, acc);
|
|
1865
|
+
}
|
|
1866
|
+
else if (t instanceof OpenListTerm) {
|
|
1867
|
+
for (const x of t.prefix)
|
|
1868
|
+
collectVarsInTerm(x, acc);
|
|
1869
|
+
acc.add(t.tailVar);
|
|
1870
|
+
}
|
|
1871
|
+
else if (t instanceof GraphTerm) {
|
|
1872
|
+
for (const tr of t.triples) {
|
|
1873
|
+
collectVarsInTerm(tr.s, acc);
|
|
1874
|
+
collectVarsInTerm(tr.p, acc);
|
|
1875
|
+
collectVarsInTerm(tr.o, acc);
|
|
1997
1876
|
}
|
|
1998
|
-
return true;
|
|
1999
1877
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
1878
|
+
}
|
|
1879
|
+
function varsInRule(rule) {
|
|
1880
|
+
const acc = new Set();
|
|
1881
|
+
for (const tr of rule.premise) {
|
|
1882
|
+
collectVarsInTerm(tr.s, acc);
|
|
1883
|
+
collectVarsInTerm(tr.p, acc);
|
|
1884
|
+
collectVarsInTerm(tr.o, acc);
|
|
2002
1885
|
}
|
|
2003
|
-
|
|
1886
|
+
for (const tr of rule.conclusion) {
|
|
1887
|
+
collectVarsInTerm(tr.s, acc);
|
|
1888
|
+
collectVarsInTerm(tr.p, acc);
|
|
1889
|
+
collectVarsInTerm(tr.o, acc);
|
|
1890
|
+
}
|
|
1891
|
+
return acc;
|
|
2004
1892
|
}
|
|
2005
|
-
function
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2008
|
-
if (!a || !b)
|
|
2009
|
-
return false;
|
|
2010
|
-
if (a.constructor !== b.constructor)
|
|
2011
|
-
return false;
|
|
2012
|
-
if (a instanceof Iri)
|
|
2013
|
-
return a.value === b.value;
|
|
2014
|
-
if (a instanceof Literal) {
|
|
2015
|
-
if (a.value === b.value)
|
|
2016
|
-
return true;
|
|
2017
|
-
// Plain "abc" == "abc"^^xsd:string (but not language-tagged)
|
|
2018
|
-
if (literalsEquivalentAsXsdString(a.value, b.value))
|
|
2019
|
-
return true;
|
|
2020
|
-
// Numeric equality ONLY when datatypes agree (no integer<->decimal here)
|
|
2021
|
-
const ai = parseNumericLiteralInfo(a);
|
|
2022
|
-
const bi = parseNumericLiteralInfo(b);
|
|
2023
|
-
if (ai && bi && ai.dt === bi.dt) {
|
|
2024
|
-
// integer: exact bigint
|
|
2025
|
-
if (ai.kind === 'bigint' && bi.kind === 'bigint')
|
|
2026
|
-
return ai.value === bi.value;
|
|
2027
|
-
// decimal: compare exactly via num/scale if possible
|
|
2028
|
-
if (ai.dt === XSD_NS + 'decimal') {
|
|
2029
|
-
const da = parseXsdDecimalToBigIntScale(ai.lexStr);
|
|
2030
|
-
const db = parseXsdDecimalToBigIntScale(bi.lexStr);
|
|
2031
|
-
if (da && db) {
|
|
2032
|
-
const scale = Math.max(da.scale, db.scale);
|
|
2033
|
-
const na = da.num * pow10n(scale - da.scale);
|
|
2034
|
-
const nb = db.num * pow10n(scale - db.scale);
|
|
2035
|
-
return na === nb;
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
// double/float-ish: JS number (same as your normal same-dt path)
|
|
2039
|
-
const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
|
|
2040
|
-
const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
|
|
2041
|
-
return !Number.isNaN(an) && !Number.isNaN(bn) && an === bn;
|
|
2042
|
-
}
|
|
2043
|
-
return false;
|
|
1893
|
+
function collectBlankLabelsInTerm(t, acc) {
|
|
1894
|
+
if (t instanceof Blank) {
|
|
1895
|
+
acc.add(t.label);
|
|
2044
1896
|
}
|
|
2045
|
-
if (
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
return a.label === b.label;
|
|
2049
|
-
if (a instanceof ListTerm) {
|
|
2050
|
-
if (a.elems.length !== b.elems.length)
|
|
2051
|
-
return false;
|
|
2052
|
-
for (let i = 0; i < a.elems.length; i++) {
|
|
2053
|
-
if (!termsEqualNoIntDecimal(a.elems[i], b.elems[i]))
|
|
2054
|
-
return false;
|
|
2055
|
-
}
|
|
2056
|
-
return true;
|
|
1897
|
+
else if (t instanceof ListTerm) {
|
|
1898
|
+
for (const x of t.elems)
|
|
1899
|
+
collectBlankLabelsInTerm(x, acc);
|
|
2057
1900
|
}
|
|
2058
|
-
if (
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
if (a.prefix.length !== b.prefix.length)
|
|
2062
|
-
return false;
|
|
2063
|
-
for (let i = 0; i < a.prefix.length; i++) {
|
|
2064
|
-
if (!termsEqualNoIntDecimal(a.prefix[i], b.prefix[i]))
|
|
2065
|
-
return false;
|
|
2066
|
-
}
|
|
2067
|
-
return true;
|
|
1901
|
+
else if (t instanceof OpenListTerm) {
|
|
1902
|
+
for (const x of t.prefix)
|
|
1903
|
+
collectBlankLabelsInTerm(x, acc);
|
|
2068
1904
|
}
|
|
2069
|
-
if (
|
|
2070
|
-
|
|
1905
|
+
else if (t instanceof GraphTerm) {
|
|
1906
|
+
for (const tr of t.triples) {
|
|
1907
|
+
collectBlankLabelsInTerm(tr.s, acc);
|
|
1908
|
+
collectBlankLabelsInTerm(tr.p, acc);
|
|
1909
|
+
collectBlankLabelsInTerm(tr.o, acc);
|
|
1910
|
+
}
|
|
2071
1911
|
}
|
|
2072
|
-
return false;
|
|
2073
1912
|
}
|
|
2074
|
-
function
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
for (let i = 0; i < xs.length; i++) {
|
|
2081
|
-
if (!triplesEqual(xs[i], ys[i]))
|
|
2082
|
-
return false;
|
|
1913
|
+
function collectBlankLabelsInTriples(triples) {
|
|
1914
|
+
const acc = new Set();
|
|
1915
|
+
for (const tr of triples) {
|
|
1916
|
+
collectBlankLabelsInTerm(tr.s, acc);
|
|
1917
|
+
collectBlankLabelsInTerm(tr.p, acc);
|
|
1918
|
+
collectBlankLabelsInTerm(tr.o, acc);
|
|
2083
1919
|
}
|
|
2084
|
-
return
|
|
2085
|
-
}
|
|
2086
|
-
// Alpha-equivalence for quoted formulas, up to *variable* and blank-node renaming.
|
|
2087
|
-
// Treats a formula as an unordered set of triples (order-insensitive match).
|
|
2088
|
-
function alphaEqVarName(x, y, vmap) {
|
|
2089
|
-
if (vmap.hasOwnProperty(x))
|
|
2090
|
-
return vmap[x] === y;
|
|
2091
|
-
vmap[x] = y;
|
|
2092
|
-
return true;
|
|
1920
|
+
return acc;
|
|
2093
1921
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
1922
|
+
// ===========================================================================
|
|
1923
|
+
// PARSER
|
|
1924
|
+
// ===========================================================================
|
|
1925
|
+
class Parser {
|
|
1926
|
+
constructor(tokens) {
|
|
1927
|
+
this.toks = tokens;
|
|
1928
|
+
this.pos = 0;
|
|
1929
|
+
this.prefixes = PrefixEnv.newDefault();
|
|
1930
|
+
this.blankCounter = 0;
|
|
1931
|
+
this.pendingTriples = [];
|
|
2103
1932
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
return alphaEqVarName(a.name, b.name, vmap);
|
|
1933
|
+
peek() {
|
|
1934
|
+
return this.toks[this.pos];
|
|
2107
1935
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
return
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
1936
|
+
next() {
|
|
1937
|
+
const tok = this.toks[this.pos];
|
|
1938
|
+
this.pos += 1;
|
|
1939
|
+
return tok;
|
|
1940
|
+
}
|
|
1941
|
+
fail(message, tok = this.peek()) {
|
|
1942
|
+
const off = tok && typeof tok.offset === 'number' ? tok.offset : null;
|
|
1943
|
+
throw new N3SyntaxError(message, off);
|
|
1944
|
+
}
|
|
1945
|
+
expectDot() {
|
|
1946
|
+
const tok = this.next();
|
|
1947
|
+
if (tok.typ !== 'Dot') {
|
|
1948
|
+
this.fail(`Expected '.', got ${tok.toString()}`, tok);
|
|
2118
1949
|
}
|
|
2119
|
-
return true;
|
|
2120
1950
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
1951
|
+
parseDocument() {
|
|
1952
|
+
const triples = [];
|
|
1953
|
+
const forwardRules = [];
|
|
1954
|
+
const backwardRules = [];
|
|
1955
|
+
while (this.peek().typ !== 'EOF') {
|
|
1956
|
+
if (this.peek().typ === 'AtPrefix') {
|
|
1957
|
+
this.next();
|
|
1958
|
+
this.parsePrefixDirective();
|
|
1959
|
+
}
|
|
1960
|
+
else if (this.peek().typ === 'AtBase') {
|
|
1961
|
+
this.next();
|
|
1962
|
+
this.parseBaseDirective();
|
|
1963
|
+
}
|
|
1964
|
+
else if (
|
|
1965
|
+
// SPARQL-style/Turtle-style directives (case-insensitive, no trailing '.')
|
|
1966
|
+
this.peek().typ === 'Ident' &&
|
|
1967
|
+
typeof this.peek().value === 'string' &&
|
|
1968
|
+
this.peek().value.toLowerCase() === 'prefix' &&
|
|
1969
|
+
this.toks[this.pos + 1] &&
|
|
1970
|
+
this.toks[this.pos + 1].typ === 'Ident' &&
|
|
1971
|
+
typeof this.toks[this.pos + 1].value === 'string' &&
|
|
1972
|
+
// Require PNAME_NS form (e.g., "ex:" or ":") to avoid clashing with a normal triple starting with IRI "prefix".
|
|
1973
|
+
this.toks[this.pos + 1].value.endsWith(':') &&
|
|
1974
|
+
this.toks[this.pos + 2] &&
|
|
1975
|
+
(this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')) {
|
|
1976
|
+
this.next(); // consume PREFIX keyword
|
|
1977
|
+
this.parseSparqlPrefixDirective();
|
|
1978
|
+
}
|
|
1979
|
+
else if (this.peek().typ === 'Ident' &&
|
|
1980
|
+
typeof this.peek().value === 'string' &&
|
|
1981
|
+
this.peek().value.toLowerCase() === 'base' &&
|
|
1982
|
+
this.toks[this.pos + 1] &&
|
|
1983
|
+
// SPARQL BASE requires an IRIREF.
|
|
1984
|
+
this.toks[this.pos + 1].typ === 'IriRef') {
|
|
1985
|
+
this.next(); // consume BASE keyword
|
|
1986
|
+
this.parseSparqlBaseDirective();
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
const first = this.parseTerm();
|
|
1990
|
+
if (this.peek().typ === 'OpImplies') {
|
|
1991
|
+
this.next();
|
|
1992
|
+
const second = this.parseTerm();
|
|
1993
|
+
this.expectDot();
|
|
1994
|
+
forwardRules.push(this.makeRule(first, second, true));
|
|
1995
|
+
}
|
|
1996
|
+
else if (this.peek().typ === 'OpImpliedBy') {
|
|
1997
|
+
this.next();
|
|
1998
|
+
const second = this.parseTerm();
|
|
1999
|
+
this.expectDot();
|
|
2000
|
+
backwardRules.push(this.makeRule(first, second, false));
|
|
2001
|
+
}
|
|
2002
|
+
else {
|
|
2003
|
+
let more;
|
|
2004
|
+
if (this.peek().typ === 'Dot') {
|
|
2005
|
+
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
2006
|
+
// So a bare subject followed by '.' is syntactically valid.
|
|
2007
|
+
// If the subject was a path / property-list that generated helper triples,
|
|
2008
|
+
// we emit those; otherwise this statement contributes no triples.
|
|
2009
|
+
more = [];
|
|
2010
|
+
if (this.pendingTriples.length > 0) {
|
|
2011
|
+
more = this.pendingTriples;
|
|
2012
|
+
this.pendingTriples = [];
|
|
2013
|
+
}
|
|
2014
|
+
this.next(); // consume '.'
|
|
2015
|
+
}
|
|
2016
|
+
else {
|
|
2017
|
+
more = this.parsePredicateObjectList(first);
|
|
2018
|
+
this.expectDot();
|
|
2019
|
+
}
|
|
2020
|
+
// normalize explicit log:implies / log:impliedBy at top-level
|
|
2021
|
+
for (const tr of more) {
|
|
2022
|
+
if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
2023
|
+
forwardRules.push(this.makeRule(tr.s, tr.o, true));
|
|
2024
|
+
}
|
|
2025
|
+
else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
2026
|
+
backwardRules.push(this.makeRule(tr.s, tr.o, false));
|
|
2027
|
+
}
|
|
2028
|
+
else {
|
|
2029
|
+
triples.push(tr);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2127
2034
|
}
|
|
2128
|
-
|
|
2129
|
-
return alphaEqVarName(a.tailVar, b.tailVar, vmap);
|
|
2130
|
-
}
|
|
2131
|
-
// Nested formulas: compare with fresh maps (separate scope)
|
|
2132
|
-
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
2133
|
-
return alphaEqGraphTriples(a.triples, b.triples);
|
|
2035
|
+
return [this.prefixes, triples, forwardRules, backwardRules];
|
|
2134
2036
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
|
|
2140
|
-
alphaEqTermInGraph(a.o, b.o, vmap, bmap));
|
|
2141
|
-
}
|
|
2142
|
-
function alphaEqGraphTriples(xs, ys) {
|
|
2143
|
-
if (xs.length !== ys.length)
|
|
2144
|
-
return false;
|
|
2145
|
-
// Fast path: exact same sequence.
|
|
2146
|
-
if (triplesListEqual(xs, ys))
|
|
2147
|
-
return true;
|
|
2148
|
-
// Order-insensitive backtracking match, threading var/blank mappings.
|
|
2149
|
-
const used = new Array(ys.length).fill(false);
|
|
2150
|
-
function step(i, vmap, bmap) {
|
|
2151
|
-
if (i >= xs.length)
|
|
2152
|
-
return true;
|
|
2153
|
-
const x = xs[i];
|
|
2154
|
-
for (let j = 0; j < ys.length; j++) {
|
|
2155
|
-
if (used[j])
|
|
2156
|
-
continue;
|
|
2157
|
-
const y = ys[j];
|
|
2158
|
-
// Cheap pruning when both predicates are IRIs.
|
|
2159
|
-
if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
|
|
2160
|
-
continue;
|
|
2161
|
-
const v2 = { ...vmap };
|
|
2162
|
-
const b2 = { ...bmap };
|
|
2163
|
-
if (!alphaEqTripleInGraph(x, y, v2, b2))
|
|
2164
|
-
continue;
|
|
2165
|
-
used[j] = true;
|
|
2166
|
-
if (step(i + 1, v2, b2))
|
|
2167
|
-
return true;
|
|
2168
|
-
used[j] = false;
|
|
2037
|
+
parsePrefixDirective() {
|
|
2038
|
+
const tok = this.next();
|
|
2039
|
+
if (tok.typ !== 'Ident') {
|
|
2040
|
+
this.fail(`Expected prefix name, got ${tok.toString()}`, tok);
|
|
2169
2041
|
}
|
|
2170
|
-
|
|
2042
|
+
const pref = tok.value || '';
|
|
2043
|
+
const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
|
|
2044
|
+
if (this.peek().typ === 'Dot') {
|
|
2045
|
+
this.next();
|
|
2046
|
+
if (!this.prefixes.map.hasOwnProperty(prefName)) {
|
|
2047
|
+
this.prefixes.set(prefName, '');
|
|
2048
|
+
}
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const tok2 = this.next();
|
|
2052
|
+
let iri;
|
|
2053
|
+
if (tok2.typ === 'IriRef') {
|
|
2054
|
+
iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
|
|
2055
|
+
}
|
|
2056
|
+
else if (tok2.typ === 'Ident') {
|
|
2057
|
+
iri = this.prefixes.expandQName(tok2.value || '');
|
|
2058
|
+
}
|
|
2059
|
+
else {
|
|
2060
|
+
this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
|
|
2061
|
+
}
|
|
2062
|
+
this.expectDot();
|
|
2063
|
+
this.prefixes.set(prefName, iri);
|
|
2171
2064
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
if (
|
|
2179
|
-
|
|
2065
|
+
parseBaseDirective() {
|
|
2066
|
+
const tok = this.next();
|
|
2067
|
+
let iri;
|
|
2068
|
+
if (tok.typ === 'IriRef') {
|
|
2069
|
+
iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
|
|
2070
|
+
}
|
|
2071
|
+
else if (tok.typ === 'Ident') {
|
|
2072
|
+
iri = tok.value || '';
|
|
2180
2073
|
}
|
|
2181
2074
|
else {
|
|
2182
|
-
|
|
2183
|
-
return true;
|
|
2075
|
+
this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
|
|
2184
2076
|
}
|
|
2077
|
+
this.expectDot();
|
|
2078
|
+
this.prefixes.setBase(iri);
|
|
2185
2079
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
return a.name === b.name;
|
|
2192
|
-
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
2193
|
-
if (a.elems.length !== b.elems.length)
|
|
2194
|
-
return false;
|
|
2195
|
-
for (let i = 0; i < a.elems.length; i++) {
|
|
2196
|
-
if (!alphaEqTerm(a.elems[i], b.elems[i], bmap))
|
|
2197
|
-
return false;
|
|
2080
|
+
parseSparqlPrefixDirective() {
|
|
2081
|
+
// SPARQL/Turtle-style PREFIX directive: PREFIX pfx: <iri> (no trailing '.')
|
|
2082
|
+
const tok = this.next();
|
|
2083
|
+
if (tok.typ !== 'Ident') {
|
|
2084
|
+
this.fail(`Expected prefix name after PREFIX, got ${tok.toString()}`, tok);
|
|
2198
2085
|
}
|
|
2199
|
-
|
|
2086
|
+
const pref = tok.value || '';
|
|
2087
|
+
const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
|
|
2088
|
+
const tok2 = this.next();
|
|
2089
|
+
let iri;
|
|
2090
|
+
if (tok2.typ === 'IriRef') {
|
|
2091
|
+
iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
|
|
2092
|
+
}
|
|
2093
|
+
else if (tok2.typ === 'Ident') {
|
|
2094
|
+
iri = this.prefixes.expandQName(tok2.value || '');
|
|
2095
|
+
}
|
|
2096
|
+
else {
|
|
2097
|
+
this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
|
|
2098
|
+
}
|
|
2099
|
+
// N3/Turtle: PREFIX directives do not have a trailing '.', but accept it permissively.
|
|
2100
|
+
if (this.peek().typ === 'Dot')
|
|
2101
|
+
this.next();
|
|
2102
|
+
this.prefixes.set(prefName, iri);
|
|
2200
2103
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2104
|
+
parseSparqlBaseDirective() {
|
|
2105
|
+
// SPARQL/Turtle-style BASE directive: BASE <iri> (no trailing '.')
|
|
2106
|
+
const tok = this.next();
|
|
2107
|
+
let iri;
|
|
2108
|
+
if (tok.typ === 'IriRef') {
|
|
2109
|
+
iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
|
|
2207
2110
|
}
|
|
2208
|
-
|
|
2111
|
+
else if (tok.typ === 'Ident') {
|
|
2112
|
+
iri = tok.value || '';
|
|
2113
|
+
}
|
|
2114
|
+
else {
|
|
2115
|
+
this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
|
|
2116
|
+
}
|
|
2117
|
+
// N3/Turtle: BASE directives do not have a trailing '.', but accept it permissively.
|
|
2118
|
+
if (this.peek().typ === 'Dot')
|
|
2119
|
+
this.next();
|
|
2120
|
+
this.prefixes.setBase(iri);
|
|
2209
2121
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2122
|
+
parseTerm() {
|
|
2123
|
+
let t = this.parsePathItem();
|
|
2124
|
+
while (this.peek().typ === 'OpPathFwd' || this.peek().typ === 'OpPathRev') {
|
|
2125
|
+
const dir = this.next().typ; // OpPathFwd | OpPathRev
|
|
2126
|
+
const pred = this.parsePathItem();
|
|
2127
|
+
this.blankCounter += 1;
|
|
2128
|
+
const bn = new Blank(`_:b${this.blankCounter}`);
|
|
2129
|
+
this.pendingTriples.push(dir === 'OpPathFwd' ? new Triple(t, pred, bn) : new Triple(bn, pred, t));
|
|
2130
|
+
t = bn;
|
|
2131
|
+
}
|
|
2132
|
+
return t;
|
|
2213
2133
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
// ===========================================================================
|
|
2221
|
-
// Indexes (facts + backward rules)
|
|
2222
|
-
// ===========================================================================
|
|
2223
|
-
//
|
|
2224
|
-
// Facts:
|
|
2225
|
-
// - __byPred: Map<predicateIRI, Triple[]>
|
|
2226
|
-
// - __byPO: Map<predicateIRI, Map<objectKey, Triple[]>>
|
|
2227
|
-
// - __keySet: Set<"S\tP\tO"> for IRI/Literal-only triples (fast dup check)
|
|
2228
|
-
//
|
|
2229
|
-
// Backward rules:
|
|
2230
|
-
// - __byHeadPred: Map<headPredicateIRI, Rule[]>
|
|
2231
|
-
// - __wildHeadPred: Rule[] (non-IRI head predicate)
|
|
2232
|
-
function termFastKey(t) {
|
|
2233
|
-
if (t instanceof Iri)
|
|
2234
|
-
return 'I:' + t.value;
|
|
2235
|
-
if (t instanceof Literal)
|
|
2236
|
-
return 'L:' + normalizeLiteralForFastKey(t.value);
|
|
2237
|
-
return null;
|
|
2238
|
-
}
|
|
2239
|
-
function tripleFastKey(tr) {
|
|
2240
|
-
const ks = termFastKey(tr.s);
|
|
2241
|
-
const kp = termFastKey(tr.p);
|
|
2242
|
-
const ko = termFastKey(tr.o);
|
|
2243
|
-
if (ks === null || kp === null || ko === null)
|
|
2244
|
-
return null;
|
|
2245
|
-
return ks + '\t' + kp + '\t' + ko;
|
|
2246
|
-
}
|
|
2247
|
-
function ensureFactIndexes(facts) {
|
|
2248
|
-
if (facts.__byPred && facts.__byPS && facts.__byPO && facts.__keySet)
|
|
2249
|
-
return;
|
|
2250
|
-
Object.defineProperty(facts, '__byPred', {
|
|
2251
|
-
value: new Map(),
|
|
2252
|
-
enumerable: false,
|
|
2253
|
-
writable: true,
|
|
2254
|
-
});
|
|
2255
|
-
Object.defineProperty(facts, '__byPS', {
|
|
2256
|
-
value: new Map(),
|
|
2257
|
-
enumerable: false,
|
|
2258
|
-
writable: true,
|
|
2259
|
-
});
|
|
2260
|
-
Object.defineProperty(facts, '__byPO', {
|
|
2261
|
-
value: new Map(),
|
|
2262
|
-
enumerable: false,
|
|
2263
|
-
writable: true,
|
|
2264
|
-
});
|
|
2265
|
-
Object.defineProperty(facts, '__keySet', {
|
|
2266
|
-
value: new Set(),
|
|
2267
|
-
enumerable: false,
|
|
2268
|
-
writable: true,
|
|
2269
|
-
});
|
|
2270
|
-
for (const f of facts)
|
|
2271
|
-
indexFact(facts, f);
|
|
2272
|
-
}
|
|
2273
|
-
function indexFact(facts, tr) {
|
|
2274
|
-
if (tr.p instanceof Iri) {
|
|
2275
|
-
const pk = tr.p.value;
|
|
2276
|
-
let pb = facts.__byPred.get(pk);
|
|
2277
|
-
if (!pb) {
|
|
2278
|
-
pb = [];
|
|
2279
|
-
facts.__byPred.set(pk, pb);
|
|
2134
|
+
parsePathItem() {
|
|
2135
|
+
const tok = this.next();
|
|
2136
|
+
const typ = tok.typ;
|
|
2137
|
+
const val = tok.value;
|
|
2138
|
+
if (typ === 'Equals') {
|
|
2139
|
+
return internIri(OWL_NS + 'sameAs');
|
|
2280
2140
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2141
|
+
if (typ === 'IriRef') {
|
|
2142
|
+
const base = this.prefixes.baseIri || '';
|
|
2143
|
+
return internIri(resolveIriRef(val || '', base));
|
|
2144
|
+
}
|
|
2145
|
+
if (typ === 'Ident') {
|
|
2146
|
+
const name = val || '';
|
|
2147
|
+
if (name === 'a') {
|
|
2148
|
+
return internIri(RDF_NS + 'type');
|
|
2288
2149
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
psb = [];
|
|
2292
|
-
ps.set(sk, psb);
|
|
2150
|
+
else if (name.startsWith('_:')) {
|
|
2151
|
+
return new Blank(name);
|
|
2293
2152
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
const ok = termFastKey(tr.o);
|
|
2297
|
-
if (ok !== null) {
|
|
2298
|
-
let po = facts.__byPO.get(pk);
|
|
2299
|
-
if (!po) {
|
|
2300
|
-
po = new Map();
|
|
2301
|
-
facts.__byPO.set(pk, po);
|
|
2153
|
+
else if (name.includes(':')) {
|
|
2154
|
+
return internIri(this.prefixes.expandQName(name));
|
|
2302
2155
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
pob = [];
|
|
2306
|
-
po.set(ok, pob);
|
|
2156
|
+
else {
|
|
2157
|
+
return internIri(name);
|
|
2307
2158
|
}
|
|
2308
|
-
pob.push(tr);
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
const key = tripleFastKey(tr);
|
|
2312
|
-
if (key !== null)
|
|
2313
|
-
facts.__keySet.add(key);
|
|
2314
|
-
}
|
|
2315
|
-
function candidateFacts(facts, goal) {
|
|
2316
|
-
ensureFactIndexes(facts);
|
|
2317
|
-
if (goal.p instanceof Iri) {
|
|
2318
|
-
const pk = goal.p.value;
|
|
2319
|
-
const sk = termFastKey(goal.s);
|
|
2320
|
-
const ok = termFastKey(goal.o);
|
|
2321
|
-
/** @type {Triple[] | null} */
|
|
2322
|
-
let byPS = null;
|
|
2323
|
-
if (sk !== null) {
|
|
2324
|
-
const ps = facts.__byPS.get(pk);
|
|
2325
|
-
if (ps)
|
|
2326
|
-
byPS = ps.get(sk) || null;
|
|
2327
2159
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2160
|
+
if (typ === 'Literal') {
|
|
2161
|
+
let s = val || '';
|
|
2162
|
+
// Optional language tag: "..."@en, per N3 LANGTAG production.
|
|
2163
|
+
if (this.peek().typ === 'LangTag') {
|
|
2164
|
+
// Only quoted string literals can carry a language tag.
|
|
2165
|
+
if (!(s.startsWith('"') && s.endsWith('"'))) {
|
|
2166
|
+
this.fail('Language tag is only allowed on quoted string literals', this.peek());
|
|
2167
|
+
}
|
|
2168
|
+
const langTok = this.next();
|
|
2169
|
+
const lang = langTok.value || '';
|
|
2170
|
+
s = `${s}@${lang}`;
|
|
2171
|
+
// N3/Turtle: language tags and datatypes are mutually exclusive.
|
|
2172
|
+
if (this.peek().typ === 'HatHat') {
|
|
2173
|
+
this.fail('A literal cannot have both a language tag (@...) and a datatype (^^...)', this.peek());
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (this.peek().typ === 'HatHat') {
|
|
2177
|
+
this.next();
|
|
2178
|
+
const dtTok = this.next();
|
|
2179
|
+
let dtIri;
|
|
2180
|
+
if (dtTok.typ === 'IriRef') {
|
|
2181
|
+
dtIri = dtTok.value || '';
|
|
2182
|
+
}
|
|
2183
|
+
else if (dtTok.typ === 'Ident') {
|
|
2184
|
+
const qn = dtTok.value || '';
|
|
2185
|
+
if (qn.includes(':'))
|
|
2186
|
+
dtIri = this.prefixes.expandQName(qn);
|
|
2187
|
+
else
|
|
2188
|
+
dtIri = qn;
|
|
2189
|
+
}
|
|
2190
|
+
else {
|
|
2191
|
+
this.fail(`Expected datatype after ^^, got ${dtTok.toString()}`, dtTok);
|
|
2192
|
+
}
|
|
2193
|
+
s = `${s}^^<${dtIri}>`;
|
|
2362
2194
|
}
|
|
2195
|
+
return internLiteral(s);
|
|
2363
2196
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2197
|
+
if (typ === 'Var')
|
|
2198
|
+
return new Var(val || '');
|
|
2199
|
+
if (typ === 'LParen')
|
|
2200
|
+
return this.parseList();
|
|
2201
|
+
if (typ === 'LBracket')
|
|
2202
|
+
return this.parseBlank();
|
|
2203
|
+
if (typ === 'LBrace')
|
|
2204
|
+
return this.parseGraph();
|
|
2205
|
+
this.fail(`Unexpected term token: ${tok.toString()}`, tok);
|
|
2366
2206
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
ensureFactIndexes(facts);
|
|
2372
|
-
facts.push(tr);
|
|
2373
|
-
indexFact(facts, tr);
|
|
2374
|
-
}
|
|
2375
|
-
function ensureBackRuleIndexes(backRules) {
|
|
2376
|
-
if (backRules.__byHeadPred && backRules.__wildHeadPred)
|
|
2377
|
-
return;
|
|
2378
|
-
Object.defineProperty(backRules, '__byHeadPred', {
|
|
2379
|
-
value: new Map(),
|
|
2380
|
-
enumerable: false,
|
|
2381
|
-
writable: true,
|
|
2382
|
-
});
|
|
2383
|
-
Object.defineProperty(backRules, '__wildHeadPred', {
|
|
2384
|
-
value: [],
|
|
2385
|
-
enumerable: false,
|
|
2386
|
-
writable: true,
|
|
2387
|
-
});
|
|
2388
|
-
for (const r of backRules)
|
|
2389
|
-
indexBackRule(backRules, r);
|
|
2390
|
-
}
|
|
2391
|
-
function indexBackRule(backRules, r) {
|
|
2392
|
-
if (!r || !r.conclusion || r.conclusion.length !== 1)
|
|
2393
|
-
return;
|
|
2394
|
-
const head = r.conclusion[0];
|
|
2395
|
-
if (head && head.p instanceof Iri) {
|
|
2396
|
-
const k = head.p.value;
|
|
2397
|
-
let bucket = backRules.__byHeadPred.get(k);
|
|
2398
|
-
if (!bucket) {
|
|
2399
|
-
bucket = [];
|
|
2400
|
-
backRules.__byHeadPred.set(k, bucket);
|
|
2207
|
+
parseList() {
|
|
2208
|
+
const elems = [];
|
|
2209
|
+
while (this.peek().typ !== 'RParen') {
|
|
2210
|
+
elems.push(this.parseTerm());
|
|
2401
2211
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
else {
|
|
2405
|
-
backRules.__wildHeadPred.push(r);
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
// ===========================================================================
|
|
2409
|
-
// Special predicate helpers
|
|
2410
|
-
// ===========================================================================
|
|
2411
|
-
function isRdfTypePred(p) {
|
|
2412
|
-
return p instanceof Iri && p.value === RDF_NS + 'type';
|
|
2413
|
-
}
|
|
2414
|
-
function isOwlSameAsPred(t) {
|
|
2415
|
-
return t instanceof Iri && t.value === OWL_NS + 'sameAs';
|
|
2416
|
-
}
|
|
2417
|
-
function isLogImplies(p) {
|
|
2418
|
-
return p instanceof Iri && p.value === LOG_NS + 'implies';
|
|
2419
|
-
}
|
|
2420
|
-
function isLogImpliedBy(p) {
|
|
2421
|
-
return p instanceof Iri && p.value === LOG_NS + 'impliedBy';
|
|
2422
|
-
}
|
|
2423
|
-
// ===========================================================================
|
|
2424
|
-
// Constraint / "test" builtins
|
|
2425
|
-
// ===========================================================================
|
|
2426
|
-
function isConstraintBuiltin(tr) {
|
|
2427
|
-
if (!(tr.p instanceof Iri))
|
|
2428
|
-
return false;
|
|
2429
|
-
const v = tr.p.value;
|
|
2430
|
-
// math: numeric comparisons (no new bindings, just tests)
|
|
2431
|
-
if (v === MATH_NS + 'equalTo' ||
|
|
2432
|
-
v === MATH_NS + 'greaterThan' ||
|
|
2433
|
-
v === MATH_NS + 'lessThan' ||
|
|
2434
|
-
v === MATH_NS + 'notEqualTo' ||
|
|
2435
|
-
v === MATH_NS + 'notGreaterThan' ||
|
|
2436
|
-
v === MATH_NS + 'notLessThan') {
|
|
2437
|
-
return true;
|
|
2438
|
-
}
|
|
2439
|
-
// list: membership test with no bindings
|
|
2440
|
-
if (v === LIST_NS + 'notMember') {
|
|
2441
|
-
return true;
|
|
2442
|
-
}
|
|
2443
|
-
// log: tests that are purely constraints (no new bindings)
|
|
2444
|
-
if (v === LOG_NS + 'forAllIn' ||
|
|
2445
|
-
v === LOG_NS + 'notEqualTo' ||
|
|
2446
|
-
v === LOG_NS + 'notIncludes' ||
|
|
2447
|
-
v === LOG_NS + 'outputString') {
|
|
2448
|
-
return true;
|
|
2449
|
-
}
|
|
2450
|
-
// string: relational / membership style tests (no bindings)
|
|
2451
|
-
if (v === STRING_NS + 'contains' ||
|
|
2452
|
-
v === STRING_NS + 'containsIgnoringCase' ||
|
|
2453
|
-
v === STRING_NS + 'endsWith' ||
|
|
2454
|
-
v === STRING_NS + 'equalIgnoringCase' ||
|
|
2455
|
-
v === STRING_NS + 'greaterThan' ||
|
|
2456
|
-
v === STRING_NS + 'lessThan' ||
|
|
2457
|
-
v === STRING_NS + 'matches' ||
|
|
2458
|
-
v === STRING_NS + 'notEqualIgnoringCase' ||
|
|
2459
|
-
v === STRING_NS + 'notGreaterThan' ||
|
|
2460
|
-
v === STRING_NS + 'notLessThan' ||
|
|
2461
|
-
v === STRING_NS + 'notMatches' ||
|
|
2462
|
-
v === STRING_NS + 'startsWith') {
|
|
2463
|
-
return true;
|
|
2464
|
-
}
|
|
2465
|
-
return false;
|
|
2466
|
-
}
|
|
2467
|
-
// Move constraint builtins to the end of the rule premise.
|
|
2468
|
-
// This is a simple "delaying" strategy similar in spirit to Prolog's when/2:
|
|
2469
|
-
// - normal goals first (can bind variables),
|
|
2470
|
-
// - pure test / constraint builtins last (checked once bindings are in place).
|
|
2471
|
-
function reorderPremiseForConstraints(premise) {
|
|
2472
|
-
if (!premise || premise.length === 0)
|
|
2473
|
-
return premise;
|
|
2474
|
-
const normal = [];
|
|
2475
|
-
const delayed = [];
|
|
2476
|
-
for (const tr of premise) {
|
|
2477
|
-
if (isConstraintBuiltin(tr))
|
|
2478
|
-
delayed.push(tr);
|
|
2479
|
-
else
|
|
2480
|
-
normal.push(tr);
|
|
2481
|
-
}
|
|
2482
|
-
return normal.concat(delayed);
|
|
2483
|
-
}
|
|
2484
|
-
// @ts-nocheck
|
|
2485
|
-
/* eslint-disable */
|
|
2486
|
-
// ===========================================================================
|
|
2487
|
-
// Unification + substitution
|
|
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;
|
|
2499
|
-
}
|
|
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)
|
|
2504
|
-
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)
|
|
2517
|
-
return false;
|
|
2518
|
-
if (t instanceof ListTerm)
|
|
2519
|
-
return t.elems.every((e) => isGroundTerm(e));
|
|
2520
|
-
if (t instanceof OpenListTerm)
|
|
2521
|
-
return false;
|
|
2522
|
-
if (t instanceof GraphTerm)
|
|
2523
|
-
return t.triples.every((tr) => isGroundTripleInGraph(tr));
|
|
2524
|
-
return true;
|
|
2525
|
-
}
|
|
2526
|
-
function isGroundTriple(tr) {
|
|
2527
|
-
return isGroundTerm(tr.s) && isGroundTerm(tr.p) && isGroundTerm(tr.o);
|
|
2528
|
-
}
|
|
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)];
|
|
2212
|
+
this.next(); // consume ')'
|
|
2213
|
+
return new ListTerm(elems);
|
|
2549
2214
|
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
const first = s[t.name];
|
|
2557
|
-
if (first === undefined) {
|
|
2558
|
-
return t;
|
|
2215
|
+
parseBlank() {
|
|
2216
|
+
// [] or [ ... ] property list
|
|
2217
|
+
if (this.peek().typ === 'RBracket') {
|
|
2218
|
+
this.next();
|
|
2219
|
+
this.blankCounter += 1;
|
|
2220
|
+
return new Blank(`_:b${this.blankCounter}`);
|
|
2559
2221
|
}
|
|
2560
|
-
//
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
const
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2222
|
+
// IRI property list: [ id <IRI> predicateObjectList? ]
|
|
2223
|
+
// Lets you embed descriptions of an IRI directly in object position.
|
|
2224
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'id') {
|
|
2225
|
+
const iriTok = this.next(); // consume 'id'
|
|
2226
|
+
const iriTerm = this.parseTerm();
|
|
2227
|
+
// N3 note: 'id' form is not meant to be used with blank node identifiers.
|
|
2228
|
+
if (iriTerm instanceof Blank && iriTerm.label.startsWith('_:')) {
|
|
2229
|
+
this.fail("Cannot use 'id' keyword with a blank node identifier inside [...]", iriTok);
|
|
2230
|
+
}
|
|
2231
|
+
// Optional ';' right after the id IRI (tolerated).
|
|
2232
|
+
if (this.peek().typ === 'Semicolon')
|
|
2233
|
+
this.next();
|
|
2234
|
+
// Empty IRI property list: [ id :iri ]
|
|
2235
|
+
if (this.peek().typ === 'RBracket') {
|
|
2236
|
+
this.next();
|
|
2237
|
+
return iriTerm;
|
|
2238
|
+
}
|
|
2239
|
+
const subj = iriTerm;
|
|
2240
|
+
while (true) {
|
|
2241
|
+
let pred;
|
|
2242
|
+
let invert = false;
|
|
2243
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
2244
|
+
this.next();
|
|
2245
|
+
pred = internIri(RDF_NS + 'type');
|
|
2246
|
+
}
|
|
2247
|
+
else if (this.peek().typ === 'OpPredInvert') {
|
|
2248
|
+
this.next(); // "<-"
|
|
2249
|
+
pred = this.parseTerm();
|
|
2250
|
+
invert = true;
|
|
2251
|
+
}
|
|
2252
|
+
else {
|
|
2253
|
+
pred = this.parseTerm();
|
|
2254
|
+
}
|
|
2255
|
+
const objs = [this.parseTerm()];
|
|
2256
|
+
while (this.peek().typ === 'Comma') {
|
|
2257
|
+
this.next();
|
|
2258
|
+
objs.push(this.parseTerm());
|
|
2259
|
+
}
|
|
2260
|
+
for (const o of objs) {
|
|
2261
|
+
this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
|
|
2262
|
+
}
|
|
2263
|
+
if (this.peek().typ === 'Semicolon') {
|
|
2264
|
+
this.next();
|
|
2265
|
+
if (this.peek().typ === 'RBracket')
|
|
2266
|
+
break;
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2570
2269
|
break;
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2270
|
+
}
|
|
2271
|
+
if (this.peek().typ !== 'RBracket') {
|
|
2272
|
+
this.fail(`Expected ']' at end of IRI property list, got ${this.peek().toString()}`);
|
|
2273
|
+
}
|
|
2274
|
+
this.next();
|
|
2275
|
+
return iriTerm;
|
|
2576
2276
|
}
|
|
2577
|
-
//
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
if (tailTerm !== undefined) {
|
|
2589
|
-
const tailApplied = applySubstTerm(tailTerm, s);
|
|
2590
|
-
if (tailApplied instanceof ListTerm) {
|
|
2591
|
-
return new ListTerm(newPrefix.concat(tailApplied.elems));
|
|
2277
|
+
// [ predicateObjectList ]
|
|
2278
|
+
this.blankCounter += 1;
|
|
2279
|
+
const id = `_:b${this.blankCounter}`;
|
|
2280
|
+
const subj = new Blank(id);
|
|
2281
|
+
while (true) {
|
|
2282
|
+
// Verb (can also be 'a')
|
|
2283
|
+
let pred;
|
|
2284
|
+
let invert = false;
|
|
2285
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
2286
|
+
this.next();
|
|
2287
|
+
pred = internIri(RDF_NS + 'type');
|
|
2592
2288
|
}
|
|
2593
|
-
else if (
|
|
2594
|
-
|
|
2289
|
+
else if (this.peek().typ === 'OpPredInvert') {
|
|
2290
|
+
this.next(); // consume "<-"
|
|
2291
|
+
pred = this.parseTerm();
|
|
2292
|
+
invert = true;
|
|
2595
2293
|
}
|
|
2596
2294
|
else {
|
|
2597
|
-
|
|
2295
|
+
pred = this.parseTerm();
|
|
2296
|
+
}
|
|
2297
|
+
// Object list: o1, o2, ...
|
|
2298
|
+
const objs = [this.parseTerm()];
|
|
2299
|
+
while (this.peek().typ === 'Comma') {
|
|
2300
|
+
this.next();
|
|
2301
|
+
objs.push(this.parseTerm());
|
|
2302
|
+
}
|
|
2303
|
+
for (const o of objs) {
|
|
2304
|
+
this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
|
|
2598
2305
|
}
|
|
2306
|
+
if (this.peek().typ === 'Semicolon') {
|
|
2307
|
+
this.next();
|
|
2308
|
+
if (this.peek().typ === 'RBracket')
|
|
2309
|
+
break;
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
break;
|
|
2599
2313
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2314
|
+
if (this.peek().typ === 'RBracket') {
|
|
2315
|
+
this.next();
|
|
2602
2316
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
return new GraphTerm(t.triples.map((tr) => applySubstTriple(tr, s)));
|
|
2606
|
-
}
|
|
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;
|
|
2614
|
-
}
|
|
2615
|
-
function unifyOpenWithList(prefix, tailv, ys, subst) {
|
|
2616
|
-
if (ys.length < prefix.length)
|
|
2617
|
-
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)
|
|
2627
|
-
return null;
|
|
2628
|
-
return s2;
|
|
2629
|
-
}
|
|
2630
|
-
function unifyGraphTriples(xs, ys, subst) {
|
|
2631
|
-
if (xs.length !== ys.length)
|
|
2632
|
-
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;
|
|
2317
|
+
else {
|
|
2318
|
+
this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
|
|
2657
2319
|
}
|
|
2658
|
-
return
|
|
2659
|
-
}
|
|
2660
|
-
return step(0, { ...subst }); // IMPORTANT: start from the incoming subst
|
|
2661
|
-
}
|
|
2662
|
-
function unifyTerm(a, b, subst) {
|
|
2663
|
-
return unifyTermWithOptions(a, b, subst, {
|
|
2664
|
-
boolValueEq: true,
|
|
2665
|
-
intDecimalEq: false,
|
|
2666
|
-
});
|
|
2667
|
-
}
|
|
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
|
-
});
|
|
2675
|
-
}
|
|
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))
|
|
2686
|
-
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);
|
|
2693
|
-
}
|
|
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 };
|
|
2705
|
-
}
|
|
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 };
|
|
2320
|
+
return new Blank(id);
|
|
2712
2321
|
}
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2322
|
+
parseGraph() {
|
|
2323
|
+
const triples = [];
|
|
2324
|
+
while (this.peek().typ !== 'RBrace') {
|
|
2325
|
+
const left = this.parseTerm();
|
|
2326
|
+
if (this.peek().typ === 'OpImplies') {
|
|
2327
|
+
this.next();
|
|
2328
|
+
const right = this.parseTerm();
|
|
2329
|
+
const pred = internIri(LOG_NS + 'implies');
|
|
2330
|
+
triples.push(new Triple(left, pred, right));
|
|
2331
|
+
if (this.peek().typ === 'Dot')
|
|
2332
|
+
this.next();
|
|
2333
|
+
else if (this.peek().typ === 'RBrace') {
|
|
2334
|
+
// ok
|
|
2724
2335
|
}
|
|
2725
2336
|
else {
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2337
|
+
this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
else if (this.peek().typ === 'OpImpliedBy') {
|
|
2341
|
+
this.next();
|
|
2342
|
+
const right = this.parseTerm();
|
|
2343
|
+
const pred = internIri(LOG_NS + 'impliedBy');
|
|
2344
|
+
triples.push(new Triple(left, pred, right));
|
|
2345
|
+
if (this.peek().typ === 'Dot')
|
|
2346
|
+
this.next();
|
|
2347
|
+
else if (this.peek().typ === 'RBrace') {
|
|
2348
|
+
// ok
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
else {
|
|
2355
|
+
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
2356
|
+
// So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
|
|
2357
|
+
if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
|
|
2358
|
+
if (this.pendingTriples.length > 0) {
|
|
2359
|
+
triples.push(...this.pendingTriples);
|
|
2360
|
+
this.pendingTriples = [];
|
|
2361
|
+
}
|
|
2362
|
+
if (this.peek().typ === 'Dot')
|
|
2363
|
+
this.next();
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
triples.push(...this.parsePredicateObjectList(left));
|
|
2367
|
+
if (this.peek().typ === 'Dot')
|
|
2368
|
+
this.next();
|
|
2369
|
+
else if (this.peek().typ === 'RBrace') {
|
|
2370
|
+
// ok
|
|
2730
2371
|
}
|
|
2372
|
+
else {
|
|
2373
|
+
this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
this.next(); // consume '}'
|
|
2378
|
+
return new GraphTerm(triples);
|
|
2379
|
+
}
|
|
2380
|
+
parsePredicateObjectList(subject) {
|
|
2381
|
+
const out = [];
|
|
2382
|
+
// If the SUBJECT was a path, emit its helper triples first
|
|
2383
|
+
if (this.pendingTriples.length > 0) {
|
|
2384
|
+
out.push(...this.pendingTriples);
|
|
2385
|
+
this.pendingTriples = [];
|
|
2386
|
+
}
|
|
2387
|
+
while (true) {
|
|
2388
|
+
let verb;
|
|
2389
|
+
let invert = false;
|
|
2390
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
2391
|
+
this.next();
|
|
2392
|
+
verb = internIri(RDF_NS + 'type');
|
|
2393
|
+
}
|
|
2394
|
+
else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
2395
|
+
// N3 syntactic sugar: "S has P O." means "S P O."
|
|
2396
|
+
this.next(); // consume "has"
|
|
2397
|
+
verb = this.parseTerm();
|
|
2731
2398
|
}
|
|
2732
|
-
if (
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
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
|
-
}
|
|
2399
|
+
else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
2400
|
+
// N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
|
|
2401
|
+
this.next(); // consume "is"
|
|
2402
|
+
verb = this.parseTerm();
|
|
2403
|
+
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
2404
|
+
this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
2744
2405
|
}
|
|
2406
|
+
this.next(); // consume "of"
|
|
2407
|
+
invert = true;
|
|
2408
|
+
}
|
|
2409
|
+
else if (this.peek().typ === 'OpPredInvert') {
|
|
2410
|
+
this.next(); // "<-"
|
|
2411
|
+
verb = this.parseTerm();
|
|
2412
|
+
invert = true;
|
|
2413
|
+
}
|
|
2414
|
+
else {
|
|
2415
|
+
verb = this.parseTerm();
|
|
2416
|
+
}
|
|
2417
|
+
const objects = this.parseObjectList();
|
|
2418
|
+
// If VERB or OBJECTS contained paths, their helper triples must come
|
|
2419
|
+
// before the triples that consume the path results (Easter depends on this).
|
|
2420
|
+
if (this.pendingTriples.length > 0) {
|
|
2421
|
+
out.push(...this.pendingTriples);
|
|
2422
|
+
this.pendingTriples = [];
|
|
2423
|
+
}
|
|
2424
|
+
for (const o of objects) {
|
|
2425
|
+
out.push(new Triple(invert ? o : subject, verb, invert ? subject : o));
|
|
2745
2426
|
}
|
|
2427
|
+
if (this.peek().typ === 'Semicolon') {
|
|
2428
|
+
this.next();
|
|
2429
|
+
if (this.peek().typ === 'Dot')
|
|
2430
|
+
break;
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
break;
|
|
2746
2434
|
}
|
|
2435
|
+
return out;
|
|
2747
2436
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
|
|
2754
|
-
}
|
|
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)
|
|
2758
|
-
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)
|
|
2763
|
-
return null;
|
|
2437
|
+
parseObjectList() {
|
|
2438
|
+
const objs = [this.parseTerm()];
|
|
2439
|
+
while (this.peek().typ === 'Comma') {
|
|
2440
|
+
this.next();
|
|
2441
|
+
objs.push(this.parseTerm());
|
|
2764
2442
|
}
|
|
2765
|
-
return
|
|
2443
|
+
return objs;
|
|
2766
2444
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
if (
|
|
2770
|
-
|
|
2771
|
-
|
|
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)
|
|
2775
|
-
return null;
|
|
2445
|
+
makeRule(left, right, isForward) {
|
|
2446
|
+
let premiseTerm, conclTerm;
|
|
2447
|
+
if (isForward) {
|
|
2448
|
+
premiseTerm = left;
|
|
2449
|
+
conclTerm = right;
|
|
2776
2450
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
if (s2 === null)
|
|
2794
|
-
return null;
|
|
2795
|
-
const s3 = unifyTerm(pat.o, fact.o, s2);
|
|
2796
|
-
return s3;
|
|
2797
|
-
}
|
|
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;
|
|
2451
|
+
else {
|
|
2452
|
+
premiseTerm = right;
|
|
2453
|
+
conclTerm = left;
|
|
2454
|
+
}
|
|
2455
|
+
let isFuse = false;
|
|
2456
|
+
if (isForward) {
|
|
2457
|
+
if (conclTerm instanceof Literal && conclTerm.value === 'false') {
|
|
2458
|
+
isFuse = true;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
let rawPremise;
|
|
2462
|
+
if (premiseTerm instanceof GraphTerm) {
|
|
2463
|
+
rawPremise = premiseTerm.triples;
|
|
2464
|
+
}
|
|
2465
|
+
else if (premiseTerm instanceof Literal && premiseTerm.value === 'true') {
|
|
2466
|
+
rawPremise = [];
|
|
2807
2467
|
}
|
|
2808
2468
|
else {
|
|
2809
|
-
|
|
2469
|
+
rawPremise = [];
|
|
2470
|
+
}
|
|
2471
|
+
let rawConclusion;
|
|
2472
|
+
if (conclTerm instanceof GraphTerm) {
|
|
2473
|
+
rawConclusion = conclTerm.triples;
|
|
2474
|
+
}
|
|
2475
|
+
else if (conclTerm instanceof Literal && conclTerm.value === 'false') {
|
|
2476
|
+
rawConclusion = [];
|
|
2810
2477
|
}
|
|
2478
|
+
else {
|
|
2479
|
+
rawConclusion = [];
|
|
2480
|
+
}
|
|
2481
|
+
// Blank nodes that occur explicitly in the head (conclusion)
|
|
2482
|
+
const headBlankLabels = collectBlankLabelsInTriples(rawConclusion);
|
|
2483
|
+
const [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
|
|
2484
|
+
// Reorder constraints for *forward* rules.
|
|
2485
|
+
const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
|
|
2486
|
+
return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
|
|
2811
2487
|
}
|
|
2812
|
-
return out;
|
|
2813
2488
|
}
|
|
2489
|
+
// @ts-nocheck
|
|
2490
|
+
/* eslint-disable */
|
|
2814
2491
|
// ===========================================================================
|
|
2815
2492
|
// BUILTINS
|
|
2816
2493
|
// ===========================================================================
|
|
@@ -5916,20 +5593,119 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
5916
5593
|
if (pv === STRING_NS + 'format') {
|
|
5917
5594
|
if (!(g.s instanceof ListTerm) || g.s.elems.length < 1)
|
|
5918
5595
|
return [];
|
|
5919
|
-
const fmtStr = termToJsString(g.s.elems[0]);
|
|
5920
|
-
if (fmtStr === null)
|
|
5596
|
+
const fmtStr = termToJsString(g.s.elems[0]);
|
|
5597
|
+
if (fmtStr === null)
|
|
5598
|
+
return [];
|
|
5599
|
+
const args = [];
|
|
5600
|
+
for (let i = 1; i < g.s.elems.length; i++) {
|
|
5601
|
+
const aStr = termToJsString(g.s.elems[i]);
|
|
5602
|
+
if (aStr === null)
|
|
5603
|
+
return [];
|
|
5604
|
+
args.push(aStr);
|
|
5605
|
+
}
|
|
5606
|
+
const formatted = simpleStringFormat(fmtStr, args);
|
|
5607
|
+
if (formatted === null)
|
|
5608
|
+
return []; // unsupported format specifier(s)
|
|
5609
|
+
const lit = makeStringLiteral(formatted);
|
|
5610
|
+
if (g.o instanceof Var) {
|
|
5611
|
+
const s2 = { ...subst };
|
|
5612
|
+
s2[g.o.name] = lit;
|
|
5613
|
+
return [s2];
|
|
5614
|
+
}
|
|
5615
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
5616
|
+
return s2 !== null ? [s2] : [];
|
|
5617
|
+
}
|
|
5618
|
+
// string:jsonPointer
|
|
5619
|
+
// Schema: ( $jsonText $pointer ) string:jsonPointer $value
|
|
5620
|
+
if (pv === STRING_NS + 'jsonPointer') {
|
|
5621
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2)
|
|
5622
|
+
return [];
|
|
5623
|
+
const jsonText = termToJsonText(g.s.elems[0]);
|
|
5624
|
+
const ptr = termToJsStringDecoded(g.s.elems[1]);
|
|
5625
|
+
if (jsonText === null || ptr === null)
|
|
5626
|
+
return [];
|
|
5627
|
+
const valTerm = jsonPointerLookup(jsonText, ptr);
|
|
5628
|
+
if (valTerm === null)
|
|
5629
|
+
return [];
|
|
5630
|
+
const s2 = unifyTerm(g.o, valTerm, subst);
|
|
5631
|
+
return s2 !== null ? [s2] : [];
|
|
5632
|
+
}
|
|
5633
|
+
// string:greaterThan
|
|
5634
|
+
if (pv === STRING_NS + 'greaterThan') {
|
|
5635
|
+
const sStr = termToJsString(g.s);
|
|
5636
|
+
const oStr = termToJsString(g.o);
|
|
5637
|
+
if (sStr === null || oStr === null)
|
|
5638
|
+
return [];
|
|
5639
|
+
return sStr > oStr ? [{ ...subst }] : [];
|
|
5640
|
+
}
|
|
5641
|
+
// string:lessThan
|
|
5642
|
+
if (pv === STRING_NS + 'lessThan') {
|
|
5643
|
+
const sStr = termToJsString(g.s);
|
|
5644
|
+
const oStr = termToJsString(g.o);
|
|
5645
|
+
if (sStr === null || oStr === null)
|
|
5646
|
+
return [];
|
|
5647
|
+
return sStr < oStr ? [{ ...subst }] : [];
|
|
5648
|
+
}
|
|
5649
|
+
// string:matches
|
|
5650
|
+
if (pv === STRING_NS + 'matches') {
|
|
5651
|
+
const sStr = termToJsString(g.s);
|
|
5652
|
+
const pattern = termToJsString(g.o);
|
|
5653
|
+
if (sStr === null || pattern === null)
|
|
5654
|
+
return [];
|
|
5655
|
+
const re = compileSwapRegex(pattern, '');
|
|
5656
|
+
if (!re)
|
|
5657
|
+
return [];
|
|
5658
|
+
return re.test(sStr) ? [{ ...subst }] : [];
|
|
5659
|
+
}
|
|
5660
|
+
// string:notEqualIgnoringCase
|
|
5661
|
+
if (pv === STRING_NS + 'notEqualIgnoringCase') {
|
|
5662
|
+
const sStr = termToJsString(g.s);
|
|
5663
|
+
const oStr = termToJsString(g.o);
|
|
5664
|
+
if (sStr === null || oStr === null)
|
|
5665
|
+
return [];
|
|
5666
|
+
return sStr.toLowerCase() !== oStr.toLowerCase() ? [{ ...subst }] : [];
|
|
5667
|
+
}
|
|
5668
|
+
// string:notGreaterThan (≤ in Unicode code order)
|
|
5669
|
+
if (pv === STRING_NS + 'notGreaterThan') {
|
|
5670
|
+
const sStr = termToJsString(g.s);
|
|
5671
|
+
const oStr = termToJsString(g.o);
|
|
5672
|
+
if (sStr === null || oStr === null)
|
|
5673
|
+
return [];
|
|
5674
|
+
return sStr <= oStr ? [{ ...subst }] : [];
|
|
5675
|
+
}
|
|
5676
|
+
// string:notLessThan (≥ in Unicode code order)
|
|
5677
|
+
if (pv === STRING_NS + 'notLessThan') {
|
|
5678
|
+
const sStr = termToJsString(g.s);
|
|
5679
|
+
const oStr = termToJsString(g.o);
|
|
5680
|
+
if (sStr === null || oStr === null)
|
|
5681
|
+
return [];
|
|
5682
|
+
return sStr >= oStr ? [{ ...subst }] : [];
|
|
5683
|
+
}
|
|
5684
|
+
// string:notMatches
|
|
5685
|
+
if (pv === STRING_NS + 'notMatches') {
|
|
5686
|
+
const sStr = termToJsString(g.s);
|
|
5687
|
+
const pattern = termToJsString(g.o);
|
|
5688
|
+
if (sStr === null || pattern === null)
|
|
5689
|
+
return [];
|
|
5690
|
+
const re = compileSwapRegex(pattern, '');
|
|
5691
|
+
if (!re)
|
|
5692
|
+
return [];
|
|
5693
|
+
return re.test(sStr) ? [] : [{ ...subst }];
|
|
5694
|
+
}
|
|
5695
|
+
// string:replace
|
|
5696
|
+
if (pv === STRING_NS + 'replace') {
|
|
5697
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3)
|
|
5698
|
+
return [];
|
|
5699
|
+
const dataStr = termToJsString(g.s.elems[0]);
|
|
5700
|
+
const searchStr = termToJsString(g.s.elems[1]);
|
|
5701
|
+
const replStr = termToJsString(g.s.elems[2]);
|
|
5702
|
+
if (dataStr === null || searchStr === null || replStr === null)
|
|
5703
|
+
return [];
|
|
5704
|
+
const re = compileSwapRegex(searchStr, 'g');
|
|
5705
|
+
if (!re)
|
|
5921
5706
|
return [];
|
|
5922
|
-
const
|
|
5923
|
-
|
|
5924
|
-
const aStr = termToJsString(g.s.elems[i]);
|
|
5925
|
-
if (aStr === null)
|
|
5926
|
-
return [];
|
|
5927
|
-
args.push(aStr);
|
|
5928
|
-
}
|
|
5929
|
-
const formatted = simpleStringFormat(fmtStr, args);
|
|
5930
|
-
if (formatted === null)
|
|
5931
|
-
return []; // unsupported format specifier(s)
|
|
5932
|
-
const lit = makeStringLiteral(formatted);
|
|
5707
|
+
const outStr = dataStr.replace(re, replStr);
|
|
5708
|
+
const lit = makeStringLiteral(outStr);
|
|
5933
5709
|
if (g.o instanceof Var) {
|
|
5934
5710
|
const s2 = { ...subst };
|
|
5935
5711
|
s2[g.o.name] = lit;
|
|
@@ -5938,161 +5714,392 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
5938
5714
|
const s2 = unifyTerm(g.o, lit, subst);
|
|
5939
5715
|
return s2 !== null ? [s2] : [];
|
|
5940
5716
|
}
|
|
5941
|
-
// string:
|
|
5942
|
-
|
|
5943
|
-
if (pv === STRING_NS + 'jsonPointer') {
|
|
5717
|
+
// string:scrape
|
|
5718
|
+
if (pv === STRING_NS + 'scrape') {
|
|
5944
5719
|
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2)
|
|
5945
5720
|
return [];
|
|
5946
|
-
const
|
|
5947
|
-
const
|
|
5948
|
-
if (
|
|
5721
|
+
const dataStr = termToJsString(g.s.elems[0]);
|
|
5722
|
+
const pattern = termToJsString(g.s.elems[1]);
|
|
5723
|
+
if (dataStr === null || pattern === null)
|
|
5949
5724
|
return [];
|
|
5950
|
-
const
|
|
5951
|
-
if (
|
|
5725
|
+
const re = compileSwapRegex(pattern, '');
|
|
5726
|
+
if (!re)
|
|
5952
5727
|
return [];
|
|
5953
|
-
const
|
|
5728
|
+
const m = re.exec(dataStr);
|
|
5729
|
+
// Spec says “exactly 1 group”; we just use the first capturing group if present.
|
|
5730
|
+
if (!m || m.length < 2)
|
|
5731
|
+
return [];
|
|
5732
|
+
const group = m[1];
|
|
5733
|
+
const lit = makeStringLiteral(group);
|
|
5734
|
+
if (g.o instanceof Var) {
|
|
5735
|
+
const s2 = { ...subst };
|
|
5736
|
+
s2[g.o.name] = lit;
|
|
5737
|
+
return [s2];
|
|
5738
|
+
}
|
|
5739
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
5954
5740
|
return s2 !== null ? [s2] : [];
|
|
5955
5741
|
}
|
|
5956
|
-
// string:
|
|
5957
|
-
if (pv === STRING_NS + '
|
|
5742
|
+
// string:startsWith
|
|
5743
|
+
if (pv === STRING_NS + 'startsWith') {
|
|
5958
5744
|
const sStr = termToJsString(g.s);
|
|
5959
5745
|
const oStr = termToJsString(g.o);
|
|
5960
5746
|
if (sStr === null || oStr === null)
|
|
5961
5747
|
return [];
|
|
5962
|
-
return sStr
|
|
5748
|
+
return sStr.startsWith(oStr) ? [{ ...subst }] : [];
|
|
5749
|
+
}
|
|
5750
|
+
// Unknown builtin
|
|
5751
|
+
return [];
|
|
5752
|
+
}
|
|
5753
|
+
function isBuiltinPred(p) {
|
|
5754
|
+
if (!(p instanceof Iri))
|
|
5755
|
+
return false;
|
|
5756
|
+
const v = p.value;
|
|
5757
|
+
// Super restricted mode: only treat => / <= as builtins.
|
|
5758
|
+
// Everything else should be handled as ordinary predicates (and thus must be
|
|
5759
|
+
// provided explicitly as facts/rules, without builtin evaluation).
|
|
5760
|
+
if (superRestrictedMode) {
|
|
5761
|
+
return v === LOG_NS + 'implies' || v === LOG_NS + 'impliedBy';
|
|
5762
|
+
}
|
|
5763
|
+
// Treat RDF Collections as list-term builtins too.
|
|
5764
|
+
if (v === RDF_NS + 'first' || v === RDF_NS + 'rest') {
|
|
5765
|
+
return true;
|
|
5766
|
+
}
|
|
5767
|
+
return (v.startsWith(CRYPTO_NS) ||
|
|
5768
|
+
v.startsWith(MATH_NS) ||
|
|
5769
|
+
v.startsWith(LOG_NS) ||
|
|
5770
|
+
v.startsWith(STRING_NS) ||
|
|
5771
|
+
v.startsWith(TIME_NS) ||
|
|
5772
|
+
v.startsWith(LIST_NS));
|
|
5773
|
+
}
|
|
5774
|
+
// @ts-nocheck
|
|
5775
|
+
/* eslint-disable */
|
|
5776
|
+
// ===========================================================================
|
|
5777
|
+
// Unification + substitution
|
|
5778
|
+
// ===========================================================================
|
|
5779
|
+
function containsVarTerm(t, v) {
|
|
5780
|
+
if (t instanceof Var)
|
|
5781
|
+
return t.name === v;
|
|
5782
|
+
if (t instanceof ListTerm)
|
|
5783
|
+
return t.elems.some((e) => containsVarTerm(e, v));
|
|
5784
|
+
if (t instanceof OpenListTerm)
|
|
5785
|
+
return t.prefix.some((e) => containsVarTerm(e, v)) || t.tailVar === v;
|
|
5786
|
+
if (t instanceof GraphTerm)
|
|
5787
|
+
return t.triples.some((tr) => containsVarTerm(tr.s, v) || containsVarTerm(tr.p, v) || containsVarTerm(tr.o, v));
|
|
5788
|
+
return false;
|
|
5789
|
+
}
|
|
5790
|
+
function isGroundTermInGraph(t) {
|
|
5791
|
+
// variables inside graph terms are treated as local placeholders,
|
|
5792
|
+
// so they don't make the *surrounding triple* non-ground.
|
|
5793
|
+
if (t instanceof OpenListTerm)
|
|
5794
|
+
return false;
|
|
5795
|
+
if (t instanceof ListTerm)
|
|
5796
|
+
return t.elems.every((e) => isGroundTermInGraph(e));
|
|
5797
|
+
if (t instanceof GraphTerm)
|
|
5798
|
+
return t.triples.every((tr) => isGroundTripleInGraph(tr));
|
|
5799
|
+
// Iri/Literal/Blank/Var are all OK inside formulas
|
|
5800
|
+
return true;
|
|
5801
|
+
}
|
|
5802
|
+
function isGroundTripleInGraph(tr) {
|
|
5803
|
+
return isGroundTermInGraph(tr.s) && isGroundTermInGraph(tr.p) && isGroundTermInGraph(tr.o);
|
|
5804
|
+
}
|
|
5805
|
+
function isGroundTerm(t) {
|
|
5806
|
+
if (t instanceof Var)
|
|
5807
|
+
return false;
|
|
5808
|
+
if (t instanceof ListTerm)
|
|
5809
|
+
return t.elems.every((e) => isGroundTerm(e));
|
|
5810
|
+
if (t instanceof OpenListTerm)
|
|
5811
|
+
return false;
|
|
5812
|
+
if (t instanceof GraphTerm)
|
|
5813
|
+
return t.triples.every((tr) => isGroundTripleInGraph(tr));
|
|
5814
|
+
return true;
|
|
5815
|
+
}
|
|
5816
|
+
function isGroundTriple(tr) {
|
|
5817
|
+
return isGroundTerm(tr.s) && isGroundTerm(tr.p) && isGroundTerm(tr.o);
|
|
5818
|
+
}
|
|
5819
|
+
// Canonical JSON-ish encoding for use as a Skolem cache key.
|
|
5820
|
+
// We only *call* this on ground terms in log:skolem, but it is
|
|
5821
|
+
// robust to seeing vars/open lists anyway.
|
|
5822
|
+
function skolemKeyFromTerm(t) {
|
|
5823
|
+
function enc(u) {
|
|
5824
|
+
if (u instanceof Iri)
|
|
5825
|
+
return ['I', u.value];
|
|
5826
|
+
if (u instanceof Literal)
|
|
5827
|
+
return ['L', u.value];
|
|
5828
|
+
if (u instanceof Blank)
|
|
5829
|
+
return ['B', u.label];
|
|
5830
|
+
if (u instanceof Var)
|
|
5831
|
+
return ['V', u.name];
|
|
5832
|
+
if (u instanceof ListTerm)
|
|
5833
|
+
return ['List', u.elems.map(enc)];
|
|
5834
|
+
if (u instanceof OpenListTerm)
|
|
5835
|
+
return ['OpenList', u.prefix.map(enc), u.tailVar];
|
|
5836
|
+
if (u instanceof GraphTerm)
|
|
5837
|
+
return ['Graph', u.triples.map((tr) => [enc(tr.s), enc(tr.p), enc(tr.o)])];
|
|
5838
|
+
return ['Other', String(u)];
|
|
5839
|
+
}
|
|
5840
|
+
return JSON.stringify(enc(t));
|
|
5841
|
+
}
|
|
5842
|
+
function applySubstTerm(t, s) {
|
|
5843
|
+
// Common case: variable
|
|
5844
|
+
if (t instanceof Var) {
|
|
5845
|
+
// Fast path: unbound variable → no change
|
|
5846
|
+
const first = s[t.name];
|
|
5847
|
+
if (first === undefined) {
|
|
5848
|
+
return t;
|
|
5849
|
+
}
|
|
5850
|
+
// Follow chains X -> Y -> ... until we hit a non-var or a cycle.
|
|
5851
|
+
let cur = first;
|
|
5852
|
+
const seen = new Set([t.name]);
|
|
5853
|
+
while (cur instanceof Var) {
|
|
5854
|
+
const name = cur.name;
|
|
5855
|
+
if (seen.has(name))
|
|
5856
|
+
break; // cycle
|
|
5857
|
+
seen.add(name);
|
|
5858
|
+
const nxt = s[name];
|
|
5859
|
+
if (!nxt)
|
|
5860
|
+
break;
|
|
5861
|
+
cur = nxt;
|
|
5862
|
+
}
|
|
5863
|
+
if (cur instanceof Var) {
|
|
5864
|
+
// Still a var: keep it as is (no need to clone)
|
|
5865
|
+
return cur;
|
|
5866
|
+
}
|
|
5867
|
+
// Bound to a non-var term: apply substitution recursively in case it
|
|
5868
|
+
// contains variables inside.
|
|
5869
|
+
return applySubstTerm(cur, s);
|
|
5870
|
+
}
|
|
5871
|
+
// Non-variable terms
|
|
5872
|
+
if (t instanceof ListTerm) {
|
|
5873
|
+
return new ListTerm(t.elems.map((e) => applySubstTerm(e, s)));
|
|
5874
|
+
}
|
|
5875
|
+
if (t instanceof OpenListTerm) {
|
|
5876
|
+
const newPrefix = t.prefix.map((e) => applySubstTerm(e, s));
|
|
5877
|
+
const tailTerm = s[t.tailVar];
|
|
5878
|
+
if (tailTerm !== undefined) {
|
|
5879
|
+
const tailApplied = applySubstTerm(tailTerm, s);
|
|
5880
|
+
if (tailApplied instanceof ListTerm) {
|
|
5881
|
+
return new ListTerm(newPrefix.concat(tailApplied.elems));
|
|
5882
|
+
}
|
|
5883
|
+
else if (tailApplied instanceof OpenListTerm) {
|
|
5884
|
+
return new OpenListTerm(newPrefix.concat(tailApplied.prefix), tailApplied.tailVar);
|
|
5885
|
+
}
|
|
5886
|
+
else {
|
|
5887
|
+
return new OpenListTerm(newPrefix, t.tailVar);
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
else {
|
|
5891
|
+
return new OpenListTerm(newPrefix, t.tailVar);
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5894
|
+
if (t instanceof GraphTerm) {
|
|
5895
|
+
return new GraphTerm(t.triples.map((tr) => applySubstTriple(tr, s)));
|
|
5896
|
+
}
|
|
5897
|
+
return t;
|
|
5898
|
+
}
|
|
5899
|
+
function applySubstTriple(tr, s) {
|
|
5900
|
+
return new Triple(applySubstTerm(tr.s, s), applySubstTerm(tr.p, s), applySubstTerm(tr.o, s));
|
|
5901
|
+
}
|
|
5902
|
+
function iriValue(t) {
|
|
5903
|
+
return t instanceof Iri ? t.value : null;
|
|
5904
|
+
}
|
|
5905
|
+
function unifyOpenWithList(prefix, tailv, ys, subst) {
|
|
5906
|
+
if (ys.length < prefix.length)
|
|
5907
|
+
return null;
|
|
5908
|
+
let s2 = { ...subst };
|
|
5909
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
5910
|
+
s2 = unifyTerm(prefix[i], ys[i], s2);
|
|
5911
|
+
if (s2 === null)
|
|
5912
|
+
return null;
|
|
5913
|
+
}
|
|
5914
|
+
const rest = new ListTerm(ys.slice(prefix.length));
|
|
5915
|
+
s2 = unifyTerm(new Var(tailv), rest, s2);
|
|
5916
|
+
if (s2 === null)
|
|
5917
|
+
return null;
|
|
5918
|
+
return s2;
|
|
5919
|
+
}
|
|
5920
|
+
function unifyGraphTriples(xs, ys, subst) {
|
|
5921
|
+
if (xs.length !== ys.length)
|
|
5922
|
+
return null;
|
|
5923
|
+
// Fast path: exact same sequence.
|
|
5924
|
+
if (triplesListEqual(xs, ys))
|
|
5925
|
+
return { ...subst };
|
|
5926
|
+
// Backtracking match (order-insensitive), *threading* the substitution through.
|
|
5927
|
+
const used = new Array(ys.length).fill(false);
|
|
5928
|
+
function step(i, s) {
|
|
5929
|
+
if (i >= xs.length)
|
|
5930
|
+
return s;
|
|
5931
|
+
const x = xs[i];
|
|
5932
|
+
for (let j = 0; j < ys.length; j++) {
|
|
5933
|
+
if (used[j])
|
|
5934
|
+
continue;
|
|
5935
|
+
const y = ys[j];
|
|
5936
|
+
// Cheap pruning when both predicates are IRIs.
|
|
5937
|
+
if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
|
|
5938
|
+
continue;
|
|
5939
|
+
const s2 = unifyTriple(x, y, s); // IMPORTANT: use `s`, not {}
|
|
5940
|
+
if (s2 === null)
|
|
5941
|
+
continue;
|
|
5942
|
+
used[j] = true;
|
|
5943
|
+
const s3 = step(i + 1, s2);
|
|
5944
|
+
if (s3 !== null)
|
|
5945
|
+
return s3;
|
|
5946
|
+
used[j] = false;
|
|
5947
|
+
}
|
|
5948
|
+
return null;
|
|
5949
|
+
}
|
|
5950
|
+
return step(0, { ...subst }); // IMPORTANT: start from the incoming subst
|
|
5951
|
+
}
|
|
5952
|
+
function unifyTerm(a, b, subst) {
|
|
5953
|
+
return unifyTermWithOptions(a, b, subst, {
|
|
5954
|
+
boolValueEq: true,
|
|
5955
|
+
intDecimalEq: false,
|
|
5956
|
+
});
|
|
5957
|
+
}
|
|
5958
|
+
function unifyTermListAppend(a, b, subst) {
|
|
5959
|
+
// Keep list:append behavior: allow integer<->decimal exact equality,
|
|
5960
|
+
// but do NOT add boolean-value equivalence (preserves current semantics).
|
|
5961
|
+
return unifyTermWithOptions(a, b, subst, {
|
|
5962
|
+
boolValueEq: false,
|
|
5963
|
+
intDecimalEq: true,
|
|
5964
|
+
});
|
|
5965
|
+
}
|
|
5966
|
+
function unifyTermWithOptions(a, b, subst, opts) {
|
|
5967
|
+
a = applySubstTerm(a, subst);
|
|
5968
|
+
b = applySubstTerm(b, subst);
|
|
5969
|
+
// Variable binding
|
|
5970
|
+
if (a instanceof Var) {
|
|
5971
|
+
const v = a.name;
|
|
5972
|
+
const t = b;
|
|
5973
|
+
if (t instanceof Var && t.name === v)
|
|
5974
|
+
return { ...subst };
|
|
5975
|
+
if (containsVarTerm(t, v))
|
|
5976
|
+
return null;
|
|
5977
|
+
const s2 = { ...subst };
|
|
5978
|
+
s2[v] = t;
|
|
5979
|
+
return s2;
|
|
5963
5980
|
}
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
const sStr = termToJsString(g.s);
|
|
5967
|
-
const oStr = termToJsString(g.o);
|
|
5968
|
-
if (sStr === null || oStr === null)
|
|
5969
|
-
return [];
|
|
5970
|
-
return sStr < oStr ? [{ ...subst }] : [];
|
|
5981
|
+
if (b instanceof Var) {
|
|
5982
|
+
return unifyTermWithOptions(b, a, subst, opts);
|
|
5971
5983
|
}
|
|
5972
|
-
//
|
|
5973
|
-
if (
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5984
|
+
// Exact matches
|
|
5985
|
+
if (a instanceof Iri && b instanceof Iri && a.value === b.value)
|
|
5986
|
+
return { ...subst };
|
|
5987
|
+
if (a instanceof Literal && b instanceof Literal && a.value === b.value)
|
|
5988
|
+
return { ...subst };
|
|
5989
|
+
if (a instanceof Blank && b instanceof Blank && a.label === b.label)
|
|
5990
|
+
return { ...subst };
|
|
5991
|
+
// Plain string vs xsd:string equivalence
|
|
5992
|
+
if (a instanceof Literal && b instanceof Literal) {
|
|
5993
|
+
if (literalsEquivalentAsXsdString(a.value, b.value))
|
|
5994
|
+
return { ...subst };
|
|
5982
5995
|
}
|
|
5983
|
-
//
|
|
5984
|
-
if (
|
|
5985
|
-
const
|
|
5986
|
-
const
|
|
5987
|
-
if (
|
|
5988
|
-
return
|
|
5989
|
-
return sStr.toLowerCase() !== oStr.toLowerCase() ? [{ ...subst }] : [];
|
|
5996
|
+
// Boolean-value equivalence (ONLY for normal unifyTerm)
|
|
5997
|
+
if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
|
|
5998
|
+
const ai = parseBooleanLiteralInfo(a);
|
|
5999
|
+
const bi = parseBooleanLiteralInfo(b);
|
|
6000
|
+
if (ai && bi && ai.value === bi.value)
|
|
6001
|
+
return { ...subst };
|
|
5990
6002
|
}
|
|
5991
|
-
//
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
6003
|
+
// Numeric-value match:
|
|
6004
|
+
// - always allow equality when datatype matches (existing behavior)
|
|
6005
|
+
// - optionally allow integer<->decimal exact equality (list:append only)
|
|
6006
|
+
if (a instanceof Literal && b instanceof Literal) {
|
|
6007
|
+
const ai = parseNumericLiteralInfo(a);
|
|
6008
|
+
const bi = parseNumericLiteralInfo(b);
|
|
6009
|
+
if (ai && bi) {
|
|
6010
|
+
if (ai.dt === bi.dt) {
|
|
6011
|
+
if (ai.kind === 'bigint' && bi.kind === 'bigint') {
|
|
6012
|
+
if (ai.value === bi.value)
|
|
6013
|
+
return { ...subst };
|
|
6014
|
+
}
|
|
6015
|
+
else {
|
|
6016
|
+
const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
|
|
6017
|
+
const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
|
|
6018
|
+
if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn)
|
|
6019
|
+
return { ...subst };
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
if (opts.intDecimalEq) {
|
|
6023
|
+
const intDt = XSD_NS + 'integer';
|
|
6024
|
+
const decDt = XSD_NS + 'decimal';
|
|
6025
|
+
if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
|
|
6026
|
+
const intInfo = ai.dt === intDt ? ai : bi; // bigint
|
|
6027
|
+
const decInfo = ai.dt === decDt ? ai : bi; // number + lexStr
|
|
6028
|
+
const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
|
|
6029
|
+
if (dec) {
|
|
6030
|
+
const scaledInt = intInfo.value * pow10n(dec.scale);
|
|
6031
|
+
if (scaledInt === dec.num)
|
|
6032
|
+
return { ...subst };
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
5998
6037
|
}
|
|
5999
|
-
//
|
|
6000
|
-
if (
|
|
6001
|
-
|
|
6002
|
-
const oStr = termToJsString(g.o);
|
|
6003
|
-
if (sStr === null || oStr === null)
|
|
6004
|
-
return [];
|
|
6005
|
-
return sStr >= oStr ? [{ ...subst }] : [];
|
|
6038
|
+
// Open list vs concrete list
|
|
6039
|
+
if (a instanceof OpenListTerm && b instanceof ListTerm) {
|
|
6040
|
+
return unifyOpenWithList(a.prefix, a.tailVar, b.elems, subst);
|
|
6006
6041
|
}
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
const sStr = termToJsString(g.s);
|
|
6010
|
-
const pattern = termToJsString(g.o);
|
|
6011
|
-
if (sStr === null || pattern === null)
|
|
6012
|
-
return [];
|
|
6013
|
-
const re = compileSwapRegex(pattern, '');
|
|
6014
|
-
if (!re)
|
|
6015
|
-
return [];
|
|
6016
|
-
return re.test(sStr) ? [] : [{ ...subst }];
|
|
6042
|
+
if (a instanceof ListTerm && b instanceof OpenListTerm) {
|
|
6043
|
+
return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
|
|
6017
6044
|
}
|
|
6018
|
-
//
|
|
6019
|
-
if (
|
|
6020
|
-
if (
|
|
6021
|
-
return
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
const re = compileSwapRegex(searchStr, 'g');
|
|
6028
|
-
if (!re)
|
|
6029
|
-
return [];
|
|
6030
|
-
const outStr = dataStr.replace(re, replStr);
|
|
6031
|
-
const lit = makeStringLiteral(outStr);
|
|
6032
|
-
if (g.o instanceof Var) {
|
|
6033
|
-
const s2 = { ...subst };
|
|
6034
|
-
s2[g.o.name] = lit;
|
|
6035
|
-
return [s2];
|
|
6045
|
+
// Open list vs open list
|
|
6046
|
+
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
6047
|
+
if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
|
|
6048
|
+
return null;
|
|
6049
|
+
let s2 = { ...subst };
|
|
6050
|
+
for (let i = 0; i < a.prefix.length; i++) {
|
|
6051
|
+
s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
|
|
6052
|
+
if (s2 === null)
|
|
6053
|
+
return null;
|
|
6036
6054
|
}
|
|
6037
|
-
|
|
6038
|
-
return s2 !== null ? [s2] : [];
|
|
6055
|
+
return s2;
|
|
6039
6056
|
}
|
|
6040
|
-
//
|
|
6041
|
-
if (
|
|
6042
|
-
if (
|
|
6043
|
-
return
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
if (!re)
|
|
6050
|
-
return [];
|
|
6051
|
-
const m = re.exec(dataStr);
|
|
6052
|
-
// Spec says “exactly 1 group”; we just use the first capturing group if present.
|
|
6053
|
-
if (!m || m.length < 2)
|
|
6054
|
-
return [];
|
|
6055
|
-
const group = m[1];
|
|
6056
|
-
const lit = makeStringLiteral(group);
|
|
6057
|
-
if (g.o instanceof Var) {
|
|
6058
|
-
const s2 = { ...subst };
|
|
6059
|
-
s2[g.o.name] = lit;
|
|
6060
|
-
return [s2];
|
|
6057
|
+
// List terms
|
|
6058
|
+
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
6059
|
+
if (a.elems.length !== b.elems.length)
|
|
6060
|
+
return null;
|
|
6061
|
+
let s2 = { ...subst };
|
|
6062
|
+
for (let i = 0; i < a.elems.length; i++) {
|
|
6063
|
+
s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
|
|
6064
|
+
if (s2 === null)
|
|
6065
|
+
return null;
|
|
6061
6066
|
}
|
|
6062
|
-
|
|
6063
|
-
return s2 !== null ? [s2] : [];
|
|
6067
|
+
return s2;
|
|
6064
6068
|
}
|
|
6065
|
-
//
|
|
6066
|
-
if (
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
return [];
|
|
6071
|
-
return sStr.startsWith(oStr) ? [{ ...subst }] : [];
|
|
6069
|
+
// Graphs
|
|
6070
|
+
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
6071
|
+
if (alphaEqGraphTriples(a.triples, b.triples))
|
|
6072
|
+
return { ...subst };
|
|
6073
|
+
return unifyGraphTriples(a.triples, b.triples, subst);
|
|
6072
6074
|
}
|
|
6073
|
-
|
|
6074
|
-
return [];
|
|
6075
|
+
return null;
|
|
6075
6076
|
}
|
|
6076
|
-
function
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6077
|
+
function unifyTriple(pat, fact, subst) {
|
|
6078
|
+
// Predicates are usually the cheapest and most selective
|
|
6079
|
+
const s1 = unifyTerm(pat.p, fact.p, subst);
|
|
6080
|
+
if (s1 === null)
|
|
6081
|
+
return null;
|
|
6082
|
+
const s2 = unifyTerm(pat.s, fact.s, s1);
|
|
6083
|
+
if (s2 === null)
|
|
6084
|
+
return null;
|
|
6085
|
+
const s3 = unifyTerm(pat.o, fact.o, s2);
|
|
6086
|
+
return s3;
|
|
6087
|
+
}
|
|
6088
|
+
function composeSubst(outer, delta) {
|
|
6089
|
+
if (!delta || Object.keys(delta).length === 0) {
|
|
6090
|
+
return { ...outer };
|
|
6085
6091
|
}
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6092
|
+
const out = { ...outer };
|
|
6093
|
+
for (const [k, v] of Object.entries(delta)) {
|
|
6094
|
+
if (out.hasOwnProperty(k)) {
|
|
6095
|
+
if (!termsEqual(out[k], v))
|
|
6096
|
+
return null;
|
|
6097
|
+
}
|
|
6098
|
+
else {
|
|
6099
|
+
out[k] = v;
|
|
6100
|
+
}
|
|
6089
6101
|
}
|
|
6090
|
-
return
|
|
6091
|
-
v.startsWith(MATH_NS) ||
|
|
6092
|
-
v.startsWith(LOG_NS) ||
|
|
6093
|
-
v.startsWith(STRING_NS) ||
|
|
6094
|
-
v.startsWith(TIME_NS) ||
|
|
6095
|
-
v.startsWith(LIST_NS));
|
|
6102
|
+
return out;
|
|
6096
6103
|
}
|
|
6097
6104
|
// ===========================================================================
|
|
6098
6105
|
// Backward proof (SLD-style)
|