eyeling 1.9.2 → 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 +1709 -1704
- package/package.json +1 -1
- package/src/eyeling-core.ts +0 -1260
- package/src/eyeling-n3.ts +1267 -0
package/eyeling.js
CHANGED
|
@@ -624,1862 +624,1867 @@ 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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
continue;
|
|
804
|
-
}
|
|
805
|
-
tokens.push(new Token('OpPathRev', null, i));
|
|
806
|
-
i += 1;
|
|
807
|
-
continue;
|
|
808
|
-
}
|
|
809
|
-
// 5) Single-character punctuation
|
|
810
|
-
if ('{}()[];,.'.includes(c)) {
|
|
811
|
-
const mapping = {
|
|
812
|
-
'{': 'LBrace',
|
|
813
|
-
'}': 'RBrace',
|
|
814
|
-
'(': 'LParen',
|
|
815
|
-
')': 'RParen',
|
|
816
|
-
'[': 'LBracket',
|
|
817
|
-
']': 'RBracket',
|
|
818
|
-
';': 'Semicolon',
|
|
819
|
-
',': 'Comma',
|
|
820
|
-
'.': 'Dot',
|
|
821
|
-
};
|
|
822
|
-
tokens.push(new Token(mapping[c], null, i));
|
|
823
|
-
i++;
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
// String literal: short "..." or long """..."""
|
|
827
|
-
if (c === '"') {
|
|
828
|
-
const start = i;
|
|
829
|
-
// Long string literal """ ... """
|
|
830
|
-
if (peek(1) === '"' && peek(2) === '"') {
|
|
831
|
-
i += 3; // consume opening """
|
|
832
|
-
const sChars = [];
|
|
833
|
-
let closed = false;
|
|
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;
|
|
903
|
-
}
|
|
904
|
-
// String literal: short '...' or long '''...'''
|
|
905
|
-
if (c === "'") {
|
|
906
|
-
const start = i;
|
|
907
|
-
// Long string literal ''' ... '''
|
|
908
|
-
if (peek(1) === "'" && peek(2) === "'") {
|
|
909
|
-
i += 3; // consume opening '''
|
|
910
|
-
const sChars = [];
|
|
911
|
-
let closed = false;
|
|
912
|
-
while (i < n) {
|
|
913
|
-
const cc = chars[i];
|
|
914
|
-
// Preserve escapes verbatim (same behavior as short strings)
|
|
915
|
-
if (cc === '\\') {
|
|
916
|
-
i++;
|
|
917
|
-
if (i < n) {
|
|
918
|
-
const esc = chars[i];
|
|
919
|
-
i++;
|
|
920
|
-
sChars.push('\\');
|
|
921
|
-
sChars.push(esc);
|
|
922
|
-
}
|
|
923
|
-
else {
|
|
924
|
-
sChars.push('\\');
|
|
925
|
-
}
|
|
926
|
-
continue;
|
|
927
|
-
}
|
|
928
|
-
// In long strings, a run of >= 3 delimiter quotes terminates the literal.
|
|
929
|
-
// Any extra quotes beyond the final 3 are part of the content.
|
|
930
|
-
if (cc === "'") {
|
|
931
|
-
let run = 0;
|
|
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));
|
|
955
|
-
continue;
|
|
956
|
-
}
|
|
957
|
-
// Short string literal ' ... '
|
|
958
|
-
i++; // consume opening '
|
|
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));
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
// Otherwise, treat as a directive (@prefix, @base)
|
|
1031
|
-
const wordChars = [];
|
|
1032
|
-
let cc;
|
|
1033
|
-
while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
|
|
1034
|
-
wordChars.push(cc);
|
|
1035
|
-
i++;
|
|
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
|
-
}
|
|
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;
|
|
1086
804
|
}
|
|
1087
805
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
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;
|
|
1090
810
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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;
|
|
1098
823
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
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;
|
|
1101
834
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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;
|
|
1105
886
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
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;
|
|
1108
895
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
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])
|
|
924
|
+
continue;
|
|
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)
|
|
928
|
+
continue;
|
|
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;
|
|
1111
937
|
}
|
|
938
|
+
return false;
|
|
1112
939
|
}
|
|
1113
|
-
|
|
1114
|
-
return tokens;
|
|
940
|
+
return step(0, {}, {});
|
|
1115
941
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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;
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
bmap[x] = y;
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
1123
953
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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;
|
|
1095
|
+
}
|
|
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;
|
|
1223
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) || [];
|
|
1224
1110
|
}
|
|
1225
|
-
return
|
|
1111
|
+
return facts;
|
|
1226
1112
|
}
|
|
1227
|
-
function
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
+
}
|
|
1245
1131
|
}
|
|
1132
|
+
const pb = facts.__byPred.get(pk) || [];
|
|
1133
|
+
return pb.some((t) => triplesEqual(t, tr));
|
|
1246
1134
|
}
|
|
1135
|
+
// Non-IRI predicate: fall back to strict triple equality.
|
|
1136
|
+
return facts.some((t) => triplesEqual(t, tr));
|
|
1247
1137
|
}
|
|
1248
|
-
function
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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);
|
|
1254
1171
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
collectVarsInTerm(tr.p, acc);
|
|
1258
|
-
collectVarsInTerm(tr.o, acc);
|
|
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));
|
|
1364
|
-
}
|
|
1365
|
-
else if (this.peek().typ === 'OpImpliedBy') {
|
|
1366
|
-
this.next();
|
|
1367
|
-
const second = this.parseTerm();
|
|
1368
|
-
this.expectDot();
|
|
1369
|
-
backwardRules.push(this.makeRule(first, second, false));
|
|
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;
|
|
1370
1330
|
}
|
|
1371
1331
|
else {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
}
|
|
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;
|
|
1401
1345
|
}
|
|
1346
|
+
else {
|
|
1347
|
+
out += '\\U';
|
|
1348
|
+
}
|
|
1349
|
+
break;
|
|
1402
1350
|
}
|
|
1351
|
+
default:
|
|
1352
|
+
// preserve unknown escapes
|
|
1353
|
+
out += '\\' + e;
|
|
1403
1354
|
}
|
|
1404
|
-
return [this.prefixes, triples, forwardRules, backwardRules];
|
|
1405
|
-
}
|
|
1406
|
-
parsePrefixDirective() {
|
|
1407
|
-
const tok = this.next();
|
|
1408
|
-
if (tok.typ !== 'Ident') {
|
|
1409
|
-
this.fail(`Expected prefix name, got ${tok.toString()}`, tok);
|
|
1410
|
-
}
|
|
1411
|
-
const pref = tok.value || '';
|
|
1412
|
-
const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
|
|
1413
|
-
if (this.peek().typ === 'Dot') {
|
|
1414
|
-
this.next();
|
|
1415
|
-
if (!this.prefixes.map.hasOwnProperty(prefName)) {
|
|
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);
|
|
1433
1355
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
|
|
1445
|
-
}
|
|
1446
|
-
this.expectDot();
|
|
1447
|
-
this.prefixes.setBase(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;
|
|
1448
1366
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const tok2 = this.next();
|
|
1458
|
-
let iri;
|
|
1459
|
-
if (tok2.typ === 'IriRef') {
|
|
1460
|
-
iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
|
|
1461
|
-
}
|
|
1462
|
-
else if (tok2.typ === 'Ident') {
|
|
1463
|
-
iri = this.prefixes.expandQName(tok2.value || '');
|
|
1464
|
-
}
|
|
1465
|
-
else {
|
|
1466
|
-
this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
|
|
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;
|
|
1467
1375
|
}
|
|
1468
|
-
//
|
|
1469
|
-
if (
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
parseSparqlBaseDirective() {
|
|
1474
|
-
// SPARQL/Turtle-style BASE directive: BASE <iri> (no trailing '.')
|
|
1475
|
-
const tok = this.next();
|
|
1476
|
-
let iri;
|
|
1477
|
-
if (tok.typ === 'IriRef') {
|
|
1478
|
-
iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
|
|
1376
|
+
// 2) Comments starting with '#'
|
|
1377
|
+
if (c === '#') {
|
|
1378
|
+
while (i < n && chars[i] !== '\n' && chars[i] !== '\r')
|
|
1379
|
+
i++;
|
|
1380
|
+
continue;
|
|
1479
1381
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
+
}
|
|
1482
1395
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
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;
|
|
1485
1423
|
}
|
|
1486
|
-
//
|
|
1487
|
-
if (
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
parseTerm() {
|
|
1492
|
-
let t = this.parsePathItem();
|
|
1493
|
-
while (this.peek().typ === 'OpPathFwd' || this.peek().typ === 'OpPathRev') {
|
|
1494
|
-
const dir = this.next().typ; // OpPathFwd | OpPathRev
|
|
1495
|
-
const pred = this.parsePathItem();
|
|
1496
|
-
this.blankCounter += 1;
|
|
1497
|
-
const bn = new Blank(`_:b${this.blankCounter}`);
|
|
1498
|
-
this.pendingTriples.push(dir === 'OpPathFwd' ? new Triple(t, pred, bn) : new Triple(bn, pred, t));
|
|
1499
|
-
t = bn;
|
|
1424
|
+
// 4) Path + datatype operators: !, ^, ^^
|
|
1425
|
+
if (c === '!') {
|
|
1426
|
+
tokens.push(new Token('OpPathFwd', null, i));
|
|
1427
|
+
i += 1;
|
|
1428
|
+
continue;
|
|
1500
1429
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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;
|
|
1509
1439
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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;
|
|
1513
1456
|
}
|
|
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
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
return this.parseList();
|
|
1570
|
-
if (typ === 'LBracket')
|
|
1571
|
-
return this.parseBlank();
|
|
1572
|
-
if (typ === 'LBrace')
|
|
1573
|
-
return this.parseGraph();
|
|
1574
|
-
this.fail(`Unexpected term token: ${tok.toString()}`, tok);
|
|
1575
|
-
}
|
|
1576
|
-
parseList() {
|
|
1577
|
-
const elems = [];
|
|
1578
|
-
while (this.peek().typ !== 'RParen') {
|
|
1579
|
-
elems.push(this.parseTerm());
|
|
1580
|
-
}
|
|
1581
|
-
this.next(); // consume ')'
|
|
1582
|
-
return new ListTerm(elems);
|
|
1583
|
-
}
|
|
1584
|
-
parseBlank() {
|
|
1585
|
-
// [] or [ ... ] property list
|
|
1586
|
-
if (this.peek().typ === 'RBracket') {
|
|
1587
|
-
this.next();
|
|
1588
|
-
this.blankCounter += 1;
|
|
1589
|
-
return new Blank(`_:b${this.blankCounter}`);
|
|
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;
|
|
1590
1612
|
}
|
|
1591
|
-
//
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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;
|
|
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++;
|
|
1607
1622
|
}
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1623
|
+
const name = nameChars.join('');
|
|
1624
|
+
tokens.push(new Token('Var', name, start));
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
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
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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++;
|
|
1729
|
+
}
|
|
1730
|
+
if (!wordChars.length) {
|
|
1731
|
+
throw new N3SyntaxError(`Unexpected char: ${JSON.stringify(c)}`, i);
|
|
1732
|
+
}
|
|
1733
|
+
const word = wordChars.join('');
|
|
1734
|
+
if (word === 'true' || word === 'false') {
|
|
1735
|
+
tokens.push(new Token('Literal', word, start));
|
|
1736
|
+
}
|
|
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));
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
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>
|
|
1754
|
+
}
|
|
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
|
|
1768
|
+
}
|
|
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;
|
|
1782
|
+
}
|
|
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;
|
|
1671
1797
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1798
|
+
}
|
|
1799
|
+
if (best === null)
|
|
1800
|
+
return null;
|
|
1801
|
+
const [p, local] = best;
|
|
1802
|
+
if (p === '')
|
|
1803
|
+
return `:${local}`;
|
|
1804
|
+
return `${p}:${local}`;
|
|
1805
|
+
}
|
|
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));
|
|
1674
1813
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
+
}
|
|
1680
1820
|
}
|
|
1681
|
-
break;
|
|
1682
1821
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1822
|
+
const v = [];
|
|
1823
|
+
for (const p of used) {
|
|
1824
|
+
if (this.map.hasOwnProperty(p))
|
|
1825
|
+
v.push([p, this.map[p]]);
|
|
1826
|
+
}
|
|
1827
|
+
v.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
1828
|
+
return v;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
function collectIrisInTerm(t) {
|
|
1832
|
+
const out = [];
|
|
1833
|
+
if (t instanceof Iri) {
|
|
1834
|
+
out.push(t.value);
|
|
1835
|
+
}
|
|
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 ^^...
|
|
1840
|
+
}
|
|
1841
|
+
else if (t instanceof ListTerm) {
|
|
1842
|
+
for (const x of t.elems)
|
|
1843
|
+
out.push(...collectIrisInTerm(x));
|
|
1844
|
+
}
|
|
1845
|
+
else if (t instanceof OpenListTerm) {
|
|
1846
|
+
for (const x of t.prefix)
|
|
1847
|
+
out.push(...collectIrisInTerm(x));
|
|
1848
|
+
}
|
|
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));
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return out;
|
|
1857
|
+
}
|
|
1858
|
+
function collectVarsInTerm(t, acc) {
|
|
1859
|
+
if (t instanceof Var) {
|
|
1860
|
+
acc.add(t.name);
|
|
1861
|
+
}
|
|
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);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
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);
|
|
1885
|
+
}
|
|
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;
|
|
1892
|
+
}
|
|
1893
|
+
function collectBlankLabelsInTerm(t, acc) {
|
|
1894
|
+
if (t instanceof Blank) {
|
|
1895
|
+
acc.add(t.label);
|
|
1896
|
+
}
|
|
1897
|
+
else if (t instanceof ListTerm) {
|
|
1898
|
+
for (const x of t.elems)
|
|
1899
|
+
collectBlankLabelsInTerm(x, acc);
|
|
1900
|
+
}
|
|
1901
|
+
else if (t instanceof OpenListTerm) {
|
|
1902
|
+
for (const x of t.prefix)
|
|
1903
|
+
collectBlankLabelsInTerm(x, acc);
|
|
1904
|
+
}
|
|
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);
|
|
1685
1910
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
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);
|
|
1919
|
+
}
|
|
1920
|
+
return acc;
|
|
1921
|
+
}
|
|
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 = [];
|
|
1932
|
+
}
|
|
1933
|
+
peek() {
|
|
1934
|
+
return this.toks[this.pos];
|
|
1935
|
+
}
|
|
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);
|
|
1688
1949
|
}
|
|
1689
|
-
return new Blank(id);
|
|
1690
1950
|
}
|
|
1691
|
-
|
|
1951
|
+
parseDocument() {
|
|
1692
1952
|
const triples = [];
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1953
|
+
const forwardRules = [];
|
|
1954
|
+
const backwardRules = [];
|
|
1955
|
+
while (this.peek().typ !== 'EOF') {
|
|
1956
|
+
if (this.peek().typ === 'AtPrefix') {
|
|
1696
1957
|
this.next();
|
|
1697
|
-
|
|
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
|
-
}
|
|
1958
|
+
this.parsePrefixDirective();
|
|
1708
1959
|
}
|
|
1709
|
-
else if (this.peek().typ === '
|
|
1960
|
+
else if (this.peek().typ === 'AtBase') {
|
|
1710
1961
|
this.next();
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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();
|
|
1722
1987
|
}
|
|
1723
1988
|
else {
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1731
|
-
if (this.peek().typ === 'Dot')
|
|
1732
|
-
this.next();
|
|
1733
|
-
continue;
|
|
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));
|
|
1734
1995
|
}
|
|
1735
|
-
|
|
1736
|
-
if (this.peek().typ === 'Dot')
|
|
1996
|
+
else if (this.peek().typ === 'OpImpliedBy') {
|
|
1737
1997
|
this.next();
|
|
1738
|
-
|
|
1739
|
-
|
|
1998
|
+
const second = this.parseTerm();
|
|
1999
|
+
this.expectDot();
|
|
2000
|
+
backwardRules.push(this.makeRule(first, second, false));
|
|
1740
2001
|
}
|
|
1741
2002
|
else {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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;
|
|
1805
|
-
}
|
|
1806
|
-
parseObjectList() {
|
|
1807
|
-
const objs = [this.parseTerm()];
|
|
1808
|
-
while (this.peek().typ === 'Comma') {
|
|
1809
|
-
this.next();
|
|
1810
|
-
objs.push(this.parseTerm());
|
|
1811
|
-
}
|
|
1812
|
-
return objs;
|
|
1813
|
-
}
|
|
1814
|
-
makeRule(left, right, isForward) {
|
|
1815
|
-
let premiseTerm, conclTerm;
|
|
1816
|
-
if (isForward) {
|
|
1817
|
-
premiseTerm = left;
|
|
1818
|
-
conclTerm = right;
|
|
1819
|
-
}
|
|
1820
|
-
else {
|
|
1821
|
-
premiseTerm = right;
|
|
1822
|
-
conclTerm = left;
|
|
1823
|
-
}
|
|
1824
|
-
let isFuse = false;
|
|
1825
|
-
if (isForward) {
|
|
1826
|
-
if (conclTerm instanceof Literal && conclTerm.value === 'false') {
|
|
1827
|
-
isFuse = true;
|
|
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
|
+
}
|
|
1828
2033
|
}
|
|
1829
2034
|
}
|
|
1830
|
-
|
|
1831
|
-
if (premiseTerm instanceof GraphTerm) {
|
|
1832
|
-
rawPremise = premiseTerm.triples;
|
|
1833
|
-
}
|
|
1834
|
-
else if (premiseTerm instanceof Literal && premiseTerm.value === 'true') {
|
|
1835
|
-
rawPremise = [];
|
|
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);
|
|
2035
|
+
return [this.prefixes, triples, forwardRules, backwardRules];
|
|
1856
2036
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
function liftBlankRuleVars(premise, conclusion) {
|
|
1862
|
-
function convertTerm(t, mapping, counter) {
|
|
1863
|
-
if (t instanceof Blank) {
|
|
1864
|
-
const label = t.label;
|
|
1865
|
-
if (!mapping.hasOwnProperty(label)) {
|
|
1866
|
-
counter[0] += 1;
|
|
1867
|
-
mapping[label] = `_b${counter[0]}`;
|
|
1868
|
-
}
|
|
1869
|
-
return new Var(mapping[label]);
|
|
1870
|
-
}
|
|
1871
|
-
if (t instanceof ListTerm) {
|
|
1872
|
-
return new ListTerm(t.elems.map((e) => convertTerm(e, mapping, counter)));
|
|
2037
|
+
parsePrefixDirective() {
|
|
2038
|
+
const tok = this.next();
|
|
2039
|
+
if (tok.typ !== 'Ident') {
|
|
2040
|
+
this.fail(`Expected prefix name, got ${tok.toString()}`, tok);
|
|
1873
2041
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
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;
|
|
1876
2050
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2051
|
+
const tok2 = this.next();
|
|
2052
|
+
let iri;
|
|
2053
|
+
if (tok2.typ === 'IriRef') {
|
|
2054
|
+
iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
|
|
1880
2055
|
}
|
|
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));
|
|
1885
|
-
}
|
|
1886
|
-
const mapping = {};
|
|
1887
|
-
const counter = [0];
|
|
1888
|
-
const newPremise = premise.map((tr) => convertTriple(tr, mapping, counter));
|
|
1889
|
-
return [newPremise, conclusion];
|
|
1890
|
-
}
|
|
1891
|
-
// Skolemization for blank nodes that occur explicitly in a rule head.
|
|
1892
|
-
//
|
|
1893
|
-
// IMPORTANT: we must be *stable per rule firing*, otherwise a rule whose
|
|
1894
|
-
// premises stay true would keep generating fresh _:sk_N blank nodes on every
|
|
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
|
|
2056
|
+
else if (tok2.typ === 'Ident') {
|
|
2057
|
+
iri = this.prefixes.expandQName(tok2.value || '');
|
|
1906
2058
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
-
}
|
|
2059
|
+
else {
|
|
2060
|
+
this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
|
|
1926
2061
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
if (t instanceof ListTerm) {
|
|
1930
|
-
return new ListTerm(t.elems.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter, firingKey, globalMap)));
|
|
1931
|
-
}
|
|
1932
|
-
if (t instanceof OpenListTerm) {
|
|
1933
|
-
return new OpenListTerm(t.prefix.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter, firingKey, globalMap)), t.tailVar);
|
|
1934
|
-
}
|
|
1935
|
-
if (t instanceof GraphTerm) {
|
|
1936
|
-
return new GraphTerm(t.triples.map((tr) => skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap)));
|
|
2062
|
+
this.expectDot();
|
|
2063
|
+
this.prefixes.set(prefName, iri);
|
|
1937
2064
|
}
|
|
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
|
-
}
|
|
2065
|
+
parseBaseDirective() {
|
|
2066
|
+
const tok = this.next();
|
|
2067
|
+
let iri;
|
|
2068
|
+
if (tok.typ === 'IriRef') {
|
|
2069
|
+
iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
|
|
1973
2070
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
if (a instanceof Var)
|
|
1977
|
-
return a.name === b.name;
|
|
1978
|
-
if (a instanceof Blank)
|
|
1979
|
-
return a.label === b.label;
|
|
1980
|
-
if (a instanceof ListTerm) {
|
|
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;
|
|
2071
|
+
else if (tok.typ === 'Ident') {
|
|
2072
|
+
iri = tok.value || '';
|
|
1986
2073
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
if (a instanceof OpenListTerm) {
|
|
1990
|
-
if (a.tailVar !== b.tailVar)
|
|
1991
|
-
return false;
|
|
1992
|
-
if (a.prefix.length !== b.prefix.length)
|
|
1993
|
-
return false;
|
|
1994
|
-
for (let i = 0; i < a.prefix.length; i++) {
|
|
1995
|
-
if (!termsEqual(a.prefix[i], b.prefix[i]))
|
|
1996
|
-
return false;
|
|
2074
|
+
else {
|
|
2075
|
+
this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
|
|
1997
2076
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
if (a instanceof GraphTerm) {
|
|
2001
|
-
return alphaEqGraphTriples(a.triples, b.triples);
|
|
2077
|
+
this.expectDot();
|
|
2078
|
+
this.prefixes.setBase(iri);
|
|
2002
2079
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
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;
|
|
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);
|
|
2042
2085
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
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;
|
|
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 || '');
|
|
2055
2092
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
if (a instanceof OpenListTerm) {
|
|
2059
|
-
if (a.tailVar !== b.tailVar)
|
|
2060
|
-
return false;
|
|
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;
|
|
2093
|
+
else if (tok2.typ === 'Ident') {
|
|
2094
|
+
iri = this.prefixes.expandQName(tok2.value || '');
|
|
2066
2095
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
function triplesEqual(a, b) {
|
|
2075
|
-
return termsEqual(a.s, b.s) && termsEqual(a.p, b.p) && termsEqual(a.o, b.o);
|
|
2076
|
-
}
|
|
2077
|
-
function triplesListEqual(xs, ys) {
|
|
2078
|
-
if (xs.length !== ys.length)
|
|
2079
|
-
return false;
|
|
2080
|
-
for (let i = 0; i < xs.length; i++) {
|
|
2081
|
-
if (!triplesEqual(xs[i], ys[i]))
|
|
2082
|
-
return false;
|
|
2083
|
-
}
|
|
2084
|
-
return true;
|
|
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;
|
|
2093
|
-
}
|
|
2094
|
-
function alphaEqTermInGraph(a, b, vmap, bmap) {
|
|
2095
|
-
// Blank nodes: renamable
|
|
2096
|
-
if (a instanceof Blank && b instanceof Blank) {
|
|
2097
|
-
const x = a.label;
|
|
2098
|
-
const y = b.label;
|
|
2099
|
-
if (bmap.hasOwnProperty(x))
|
|
2100
|
-
return bmap[x] === y;
|
|
2101
|
-
bmap[x] = y;
|
|
2102
|
-
return true;
|
|
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);
|
|
2103
2103
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
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 || '');
|
|
2110
|
+
}
|
|
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);
|
|
2107
2121
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
return false;
|
|
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;
|
|
2118
2131
|
}
|
|
2119
|
-
return
|
|
2132
|
+
return t;
|
|
2120
2133
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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');
|
|
2127
2140
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
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');
|
|
2149
|
+
}
|
|
2150
|
+
else if (name.startsWith('_:')) {
|
|
2151
|
+
return new Blank(name);
|
|
2152
|
+
}
|
|
2153
|
+
else if (name.includes(':')) {
|
|
2154
|
+
return internIri(this.prefixes.expandQName(name));
|
|
2155
|
+
}
|
|
2156
|
+
else {
|
|
2157
|
+
return internIri(name);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
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}>`;
|
|
2194
|
+
}
|
|
2195
|
+
return internLiteral(s);
|
|
2196
|
+
}
|
|
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);
|
|
2130
2206
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2207
|
+
parseList() {
|
|
2208
|
+
const elems = [];
|
|
2209
|
+
while (this.peek().typ !== 'RParen') {
|
|
2210
|
+
elems.push(this.parseTerm());
|
|
2211
|
+
}
|
|
2212
|
+
this.next(); // consume ')'
|
|
2213
|
+
return new ListTerm(elems);
|
|
2134
2214
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
if (
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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}`);
|
|
2221
|
+
}
|
|
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
|
+
}
|
|
2269
|
+
break;
|
|
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;
|
|
2276
|
+
}
|
|
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');
|
|
2288
|
+
}
|
|
2289
|
+
else if (this.peek().typ === 'OpPredInvert') {
|
|
2290
|
+
this.next(); // consume "<-"
|
|
2291
|
+
pred = this.parseTerm();
|
|
2292
|
+
invert = true;
|
|
2293
|
+
}
|
|
2294
|
+
else {
|
|
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));
|
|
2305
|
+
}
|
|
2306
|
+
if (this.peek().typ === 'Semicolon') {
|
|
2307
|
+
this.next();
|
|
2308
|
+
if (this.peek().typ === 'RBracket')
|
|
2309
|
+
break;
|
|
2164
2310
|
continue;
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
return true;
|
|
2168
|
-
used[j] = false;
|
|
2311
|
+
}
|
|
2312
|
+
break;
|
|
2169
2313
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
return step(0, {}, {});
|
|
2173
|
-
}
|
|
2174
|
-
function alphaEqTerm(a, b, bmap) {
|
|
2175
|
-
if (a instanceof Blank && b instanceof Blank) {
|
|
2176
|
-
const x = a.label;
|
|
2177
|
-
const y = b.label;
|
|
2178
|
-
if (bmap.hasOwnProperty(x)) {
|
|
2179
|
-
return bmap[x] === y;
|
|
2314
|
+
if (this.peek().typ === 'RBracket') {
|
|
2315
|
+
this.next();
|
|
2180
2316
|
}
|
|
2181
2317
|
else {
|
|
2182
|
-
|
|
2183
|
-
return true;
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
if (a instanceof Iri && b instanceof Iri)
|
|
2187
|
-
return a.value === b.value;
|
|
2188
|
-
if (a instanceof Literal && b instanceof Literal)
|
|
2189
|
-
return a.value === b.value;
|
|
2190
|
-
if (a instanceof Var && b instanceof Var)
|
|
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;
|
|
2318
|
+
this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
|
|
2198
2319
|
}
|
|
2199
|
-
return
|
|
2320
|
+
return new Blank(id);
|
|
2200
2321
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
if (
|
|
2206
|
-
|
|
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
|
|
2335
|
+
}
|
|
2336
|
+
else {
|
|
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
|
|
2371
|
+
}
|
|
2372
|
+
else {
|
|
2373
|
+
this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2207
2376
|
}
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
2211
|
-
// formulas are alpha-equivalent up to var/blank renaming
|
|
2212
|
-
return alphaEqGraphTriples(a.triples, b.triples);
|
|
2377
|
+
this.next(); // consume '}'
|
|
2378
|
+
return new GraphTerm(triples);
|
|
2213
2379
|
}
|
|
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);
|
|
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 = [];
|
|
2280
2386
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
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();
|
|
2398
|
+
}
|
|
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()}`);
|
|
2405
|
+
}
|
|
2406
|
+
this.next(); // consume "of"
|
|
2407
|
+
invert = true;
|
|
2288
2408
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2409
|
+
else if (this.peek().typ === 'OpPredInvert') {
|
|
2410
|
+
this.next(); // "<-"
|
|
2411
|
+
verb = this.parseTerm();
|
|
2412
|
+
invert = true;
|
|
2293
2413
|
}
|
|
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);
|
|
2414
|
+
else {
|
|
2415
|
+
verb = this.parseTerm();
|
|
2302
2416
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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 = [];
|
|
2307
2423
|
}
|
|
2308
|
-
|
|
2424
|
+
for (const o of objects) {
|
|
2425
|
+
out.push(new Triple(invert ? o : subject, verb, invert ? subject : o));
|
|
2426
|
+
}
|
|
2427
|
+
if (this.peek().typ === 'Semicolon') {
|
|
2428
|
+
this.next();
|
|
2429
|
+
if (this.peek().typ === 'Dot')
|
|
2430
|
+
break;
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
break;
|
|
2309
2434
|
}
|
|
2435
|
+
return out;
|
|
2310
2436
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
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
|
-
}
|
|
2328
|
-
/** @type {Triple[] | null} */
|
|
2329
|
-
let byPO = null;
|
|
2330
|
-
if (ok !== null) {
|
|
2331
|
-
const po = facts.__byPO.get(pk);
|
|
2332
|
-
if (po)
|
|
2333
|
-
byPO = po.get(ok) || null;
|
|
2437
|
+
parseObjectList() {
|
|
2438
|
+
const objs = [this.parseTerm()];
|
|
2439
|
+
while (this.peek().typ === 'Comma') {
|
|
2440
|
+
this.next();
|
|
2441
|
+
objs.push(this.parseTerm());
|
|
2334
2442
|
}
|
|
2335
|
-
|
|
2336
|
-
return byPS.length <= byPO.length ? byPS : byPO;
|
|
2337
|
-
if (byPS)
|
|
2338
|
-
return byPS;
|
|
2339
|
-
if (byPO)
|
|
2340
|
-
return byPO;
|
|
2341
|
-
return facts.__byPred.get(pk) || [];
|
|
2443
|
+
return objs;
|
|
2342
2444
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
if (
|
|
2356
|
-
|
|
2357
|
-
// Facts are all in the same graph. Different blank node labels represent
|
|
2358
|
-
// different existentials unless explicitly connected. Do NOT treat
|
|
2359
|
-
// triples as duplicates modulo blank renaming, or you'll incorrectly
|
|
2360
|
-
// drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
|
|
2361
|
-
return pob.some((t) => triplesEqual(t, tr));
|
|
2445
|
+
makeRule(left, right, isForward) {
|
|
2446
|
+
let premiseTerm, conclTerm;
|
|
2447
|
+
if (isForward) {
|
|
2448
|
+
premiseTerm = left;
|
|
2449
|
+
conclTerm = right;
|
|
2450
|
+
}
|
|
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;
|
|
2362
2459
|
}
|
|
2363
2460
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
// Non-IRI predicate: fall back to strict triple equality.
|
|
2368
|
-
return facts.some((t) => triplesEqual(t, tr));
|
|
2369
|
-
}
|
|
2370
|
-
function pushFactIndexed(facts, tr) {
|
|
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);
|
|
2461
|
+
let rawPremise;
|
|
2462
|
+
if (premiseTerm instanceof GraphTerm) {
|
|
2463
|
+
rawPremise = premiseTerm.triples;
|
|
2401
2464
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
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);
|
|
2465
|
+
else if (premiseTerm instanceof Literal && premiseTerm.value === 'true') {
|
|
2466
|
+
rawPremise = [];
|
|
2467
|
+
}
|
|
2468
|
+
else {
|
|
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 = [];
|
|
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);
|
|
2481
2487
|
}
|
|
2482
|
-
return normal.concat(delayed);
|
|
2483
2488
|
}
|
|
2484
2489
|
// @ts-nocheck
|
|
2485
2490
|
/* eslint-disable */
|