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 CHANGED
@@ -624,1862 +624,1867 @@ class DerivedFact {
624
624
  }
625
625
  }
626
626
  // ===========================================================================
627
- // LEXER
627
+ // Blank-node lifting and Skolemization
628
628
  // ===========================================================================
629
- class Token {
630
- constructor(typ, value = null, offset = null) {
631
- this.typ = typ;
632
- this.value = value;
633
- // Codepoint offset in the original source (Array.from(text) index).
634
- this.offset = offset;
635
- }
636
- toString() {
637
- const loc = typeof this.offset === 'number' ? `@${this.offset}` : '';
638
- if (this.value == null)
639
- return `Token(${this.typ}${loc})`;
640
- return `Token(${this.typ}${loc}, ${JSON.stringify(this.value)})`;
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
- class N3SyntaxError extends SyntaxError {
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
- function isWs(c) {
651
- return /\s/.test(c);
652
- }
653
- function isNameChar(c) {
654
- return /[0-9A-Za-z_\-:]/.test(c);
655
- }
656
- function decodeN3StringEscapes(s) {
657
- let out = '';
658
- for (let i = 0; i < s.length; i++) {
659
- const c = s[i];
660
- if (c !== '\\') {
661
- out += c;
662
- continue;
663
- }
664
- if (i + 1 >= s.length) {
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
- const e = s[++i];
669
- switch (e) {
670
- case 't':
671
- out += '\t';
672
- break;
673
- case 'n':
674
- out += '\n';
675
- break;
676
- case 'r':
677
- out += '\r';
678
- break;
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
- break;
687
+ mapping[label] = sk;
704
688
  }
705
- case 'U': {
706
- const hex = s.slice(i + 1, i + 9);
707
- if (/^[0-9A-Fa-f]{8}$/.test(hex)) {
708
- const cp = parseInt(hex, 16);
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
- return out;
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
- while (i < n) {
737
- let c = peek();
738
- if (c === null)
739
- break;
740
- // 1) Whitespace
741
- if (isWs(c)) {
742
- i++;
743
- continue;
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
- // 2) Comments starting with '#'
746
- if (c === '#') {
747
- while (i < n && chars[i] !== '\n' && chars[i] !== '\r')
748
- i++;
749
- continue;
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
- // 3) Two-character operators: => and <=
752
- if (c === '=') {
753
- if (peek(1) === '>') {
754
- tokens.push(new Token('OpImplies', null, i));
755
- i += 2;
756
- continue;
757
- }
758
- else {
759
- // N3 syntactic sugar: '=' means owl:sameAs
760
- tokens.push(new Token('Equals', null, i));
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
- if (c === '<') {
766
- if (peek(1) === '=') {
767
- tokens.push(new Token('OpImpliedBy', null, i));
768
- i += 2;
769
- continue;
770
- }
771
- // N3 predicate inversion: "<-" (swap subject/object for this predicate)
772
- if (peek(1) === '-') {
773
- tokens.push(new Token('OpPredInvert', null, i));
774
- i += 2;
775
- continue;
776
- }
777
- // Otherwise IRIREF <...>
778
- const start = i;
779
- i++; // skip '<'
780
- const iriChars = [];
781
- while (i < n && chars[i] !== '>') {
782
- iriChars.push(chars[i]);
783
- i++;
784
- }
785
- if (i >= n || chars[i] !== '>') {
786
- throw new N3SyntaxError('Unterminated IRI <...>', start);
787
- }
788
- i++; // skip '>'
789
- const iri = iriChars.join('');
790
- tokens.push(new Token('IriRef', iri, start));
791
- continue;
792
- }
793
- // 4) Path + datatype operators: !, ^, ^^
794
- if (c === '!') {
795
- tokens.push(new Token('OpPathFwd', null, i));
796
- i += 1;
797
- continue;
798
- }
799
- if (c === '^') {
800
- if (peek(1) === '^') {
801
- tokens.push(new Token('HatHat', null, i));
802
- i += 2;
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
- tokens.push(new Token('Literal', numChars.join(''), start));
1089
- continue;
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
- // 7) Identifiers / keywords / QNames
1092
- const start = i;
1093
- const wordChars = [];
1094
- let cc;
1095
- while ((cc = peek()) !== null && isNameChar(cc)) {
1096
- wordChars.push(cc);
1097
- i++;
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
- if (!wordChars.length) {
1100
- throw new N3SyntaxError(`Unexpected char: ${JSON.stringify(c)}`, i);
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
- const word = wordChars.join('');
1103
- if (word === 'true' || word === 'false') {
1104
- tokens.push(new Token('Literal', word, start));
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
- else if ([...word].every((ch) => /[0-9.\-]/.test(ch))) {
1107
- tokens.push(new Token('Literal', word, start));
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
- else {
1110
- tokens.push(new Token('Ident', word, start));
896
+ // tailVar is a var-name string, so treat it as renamable too
897
+ return alphaEqVarName(a.tailVar, b.tailVar, vmap);
898
+ }
899
+ // Nested formulas: compare with fresh maps (separate scope)
900
+ if (a instanceof GraphTerm && b instanceof GraphTerm) {
901
+ return alphaEqGraphTriples(a.triples, b.triples);
902
+ }
903
+ return false;
904
+ }
905
+ function alphaEqTripleInGraph(a, b, vmap, bmap) {
906
+ return (alphaEqTermInGraph(a.s, b.s, vmap, bmap) &&
907
+ alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
908
+ alphaEqTermInGraph(a.o, b.o, vmap, bmap));
909
+ }
910
+ function alphaEqGraphTriples(xs, ys) {
911
+ if (xs.length !== ys.length)
912
+ return false;
913
+ // Fast path: exact same sequence.
914
+ if (triplesListEqual(xs, ys))
915
+ return true;
916
+ // Order-insensitive backtracking match, threading var/blank mappings.
917
+ const used = new Array(ys.length).fill(false);
918
+ function step(i, vmap, bmap) {
919
+ if (i >= xs.length)
920
+ return true;
921
+ const x = xs[i];
922
+ for (let j = 0; j < ys.length; j++) {
923
+ if (used[j])
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
- tokens.push(new Token('EOF', null, n));
1114
- return tokens;
940
+ return step(0, {}, {});
1115
941
  }
1116
- // ===========================================================================
1117
- // PREFIX ENVIRONMENT
1118
- // ===========================================================================
1119
- class PrefixEnv {
1120
- constructor(map, baseIri) {
1121
- this.map = map || {}; // prefix -> IRI (including "" for @prefix :)
1122
- this.baseIri = baseIri || ''; // base IRI for resolving <relative>
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
- static newDefault() {
1125
- const m = {};
1126
- m['rdf'] = RDF_NS;
1127
- m['rdfs'] = RDFS_NS;
1128
- m['xsd'] = XSD_NS;
1129
- m['log'] = LOG_NS;
1130
- m['math'] = MATH_NS;
1131
- m['string'] = STRING_NS;
1132
- m['list'] = LIST_NS;
1133
- m['time'] = TIME_NS;
1134
- m['genid'] = SKOLEM_NS;
1135
- m[''] = ''; // empty prefix default namespace
1136
- return new PrefixEnv(m, ''); // base IRI starts empty
954
+ if (a instanceof Iri && b instanceof Iri)
955
+ return a.value === b.value;
956
+ if (a instanceof Literal && b instanceof Literal)
957
+ return a.value === b.value;
958
+ if (a instanceof Var && b instanceof Var)
959
+ return a.name === b.name;
960
+ if (a instanceof ListTerm && b instanceof ListTerm) {
961
+ if (a.elems.length !== b.elems.length)
962
+ return false;
963
+ for (let i = 0; i < a.elems.length; i++) {
964
+ if (!alphaEqTerm(a.elems[i], b.elems[i], bmap))
965
+ return false;
966
+ }
967
+ return true;
1137
968
  }
1138
- set(pref, base) {
1139
- this.map[pref] = base;
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
- setBase(baseIri) {
1142
- this.baseIri = baseIri || '';
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
- expandQName(q) {
1145
- if (q.includes(':')) {
1146
- const [p, local] = q.split(':', 2);
1147
- const base = this.map[p] || '';
1148
- if (base)
1149
- return base + local;
1150
- return q;
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
- return q;
1153
- }
1154
- shrinkIri(iri) {
1155
- let best = null; // [prefix, local]
1156
- for (const [p, base] of Object.entries(this.map)) {
1157
- if (!base)
1158
- continue;
1159
- if (iri.startsWith(base)) {
1160
- const local = iri.slice(base.length);
1161
- if (!local)
1162
- continue;
1163
- const cand = [p, local];
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
- if (best === null)
1169
- return null;
1170
- const [p, local] = best;
1171
- if (p === '')
1172
- return `:${local}`;
1173
- return `${p}:${local}`;
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
- iris.push(...collectIrisInTerm(t.o));
1184
- for (const iri of iris) {
1185
- for (const [p, base] of Object.entries(this.map)) {
1186
- if (base && iri.startsWith(base))
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 collectIrisInTerm(t) {
1201
- const out = [];
1202
- if (t instanceof Iri) {
1203
- out.push(t.value);
1204
- }
1205
- else if (t instanceof Literal) {
1206
- const [_lex, dt] = literalParts(t.value);
1207
- if (dt)
1208
- out.push(dt); // so rdf/xsd prefixes are emitted when only used in ^^...
1209
- }
1210
- else if (t instanceof ListTerm) {
1211
- for (const x of t.elems)
1212
- out.push(...collectIrisInTerm(x));
1213
- }
1214
- else if (t instanceof OpenListTerm) {
1215
- for (const x of t.prefix)
1216
- out.push(...collectIrisInTerm(x));
1217
- }
1218
- else if (t instanceof GraphTerm) {
1219
- for (const tr of t.triples) {
1220
- out.push(...collectIrisInTerm(tr.s));
1221
- out.push(...collectIrisInTerm(tr.p));
1222
- out.push(...collectIrisInTerm(tr.o));
1083
+ function candidateFacts(facts, goal) {
1084
+ ensureFactIndexes(facts);
1085
+ if (goal.p instanceof Iri) {
1086
+ const pk = goal.p.value;
1087
+ const sk = termFastKey(goal.s);
1088
+ const ok = termFastKey(goal.o);
1089
+ /** @type {Triple[] | null} */
1090
+ let byPS = null;
1091
+ if (sk !== null) {
1092
+ const ps = facts.__byPS.get(pk);
1093
+ if (ps)
1094
+ byPS = ps.get(sk) || null;
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 out;
1111
+ return facts;
1226
1112
  }
1227
- function collectVarsInTerm(t, acc) {
1228
- if (t instanceof Var) {
1229
- acc.add(t.name);
1230
- }
1231
- else if (t instanceof ListTerm) {
1232
- for (const x of t.elems)
1233
- collectVarsInTerm(x, acc);
1234
- }
1235
- else if (t instanceof OpenListTerm) {
1236
- for (const x of t.prefix)
1237
- collectVarsInTerm(x, acc);
1238
- acc.add(t.tailVar);
1239
- }
1240
- else if (t instanceof GraphTerm) {
1241
- for (const tr of t.triples) {
1242
- collectVarsInTerm(tr.s, acc);
1243
- collectVarsInTerm(tr.p, acc);
1244
- collectVarsInTerm(tr.o, acc);
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 varsInRule(rule) {
1249
- const acc = new Set();
1250
- for (const tr of rule.premise) {
1251
- collectVarsInTerm(tr.s, acc);
1252
- collectVarsInTerm(tr.p, acc);
1253
- collectVarsInTerm(tr.o, acc);
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
- for (const tr of rule.conclusion) {
1256
- collectVarsInTerm(tr.s, acc);
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
- function collectBlankLabelsInTerm(t, acc) {
1263
- if (t instanceof Blank) {
1264
- acc.add(t.label);
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
- else if (t instanceof ListTerm) {
1267
- for (const x of t.elems)
1268
- collectBlankLabelsInTerm(x, acc);
1207
+ // list: membership test with no bindings
1208
+ if (v === LIST_NS + 'notMember') {
1209
+ return true;
1269
1210
  }
1270
- else if (t instanceof OpenListTerm) {
1271
- for (const x of t.prefix)
1272
- collectBlankLabelsInTerm(x, acc);
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
- else if (t instanceof GraphTerm) {
1275
- for (const tr of t.triples) {
1276
- collectBlankLabelsInTerm(tr.s, acc);
1277
- collectBlankLabelsInTerm(tr.p, acc);
1278
- collectBlankLabelsInTerm(tr.o, acc);
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
- function collectBlankLabelsInTriples(triples) {
1283
- const acc = new Set();
1284
- for (const tr of triples) {
1285
- collectBlankLabelsInTerm(tr.s, acc);
1286
- collectBlankLabelsInTerm(tr.p, acc);
1287
- collectBlankLabelsInTerm(tr.o, acc);
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 acc;
1250
+ return normal.concat(delayed);
1290
1251
  }
1252
+ // @ts-nocheck
1253
+ /* eslint-disable */
1291
1254
  // ===========================================================================
1292
- // PARSER
1255
+ // N3 lexer + parser
1293
1256
  // ===========================================================================
1294
- class Parser {
1295
- constructor(tokens) {
1296
- this.toks = tokens;
1297
- this.pos = 0;
1298
- this.prefixes = PrefixEnv.newDefault();
1299
- this.blankCounter = 0;
1300
- this.pendingTriples = [];
1301
- }
1302
- peek() {
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
- next() {
1306
- const tok = this.toks[this.pos];
1307
- this.pos += 1;
1308
- return tok;
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
- fail(message, tok = this.peek()) {
1311
- const off = tok && typeof tok.offset === 'number' ? tok.offset : null;
1312
- throw new N3SyntaxError(message, off);
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
- expectDot() {
1315
- const tok = this.next();
1316
- if (tok.typ !== 'Dot') {
1317
- this.fail(`Expected '.', got ${tok.toString()}`, tok);
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
- parseDocument() {
1321
- const triples = [];
1322
- const forwardRules = [];
1323
- const backwardRules = [];
1324
- while (this.peek().typ !== 'EOF') {
1325
- if (this.peek().typ === 'AtPrefix') {
1326
- this.next();
1327
- this.parsePrefixDirective();
1328
- }
1329
- else if (this.peek().typ === 'AtBase') {
1330
- this.next();
1331
- this.parseBaseDirective();
1332
- }
1333
- else if (
1334
- // SPARQL-style/Turtle-style directives (case-insensitive, no trailing '.')
1335
- this.peek().typ === 'Ident' &&
1336
- typeof this.peek().value === 'string' &&
1337
- this.peek().value.toLowerCase() === 'prefix' &&
1338
- this.toks[this.pos + 1] &&
1339
- this.toks[this.pos + 1].typ === 'Ident' &&
1340
- typeof this.toks[this.pos + 1].value === 'string' &&
1341
- // Require PNAME_NS form (e.g., "ex:" or ":") to avoid clashing with a normal triple starting with IRI "prefix".
1342
- this.toks[this.pos + 1].value.endsWith(':') &&
1343
- this.toks[this.pos + 2] &&
1344
- (this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')) {
1345
- this.next(); // consume PREFIX keyword
1346
- this.parseSparqlPrefixDirective();
1347
- }
1348
- else if (this.peek().typ === 'Ident' &&
1349
- typeof this.peek().value === 'string' &&
1350
- this.peek().value.toLowerCase() === 'base' &&
1351
- this.toks[this.pos + 1] &&
1352
- // SPARQL BASE requires an IRIREF.
1353
- this.toks[this.pos + 1].typ === 'IriRef') {
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
- let more;
1373
- if (this.peek().typ === 'Dot') {
1374
- // N3 grammar allows: triples ::= subject predicateObjectList?
1375
- // So a bare subject followed by '.' is syntactically valid.
1376
- // If the subject was a path / property-list that generated helper triples,
1377
- // we emit those; otherwise this statement contributes no triples.
1378
- more = [];
1379
- if (this.pendingTriples.length > 0) {
1380
- more = this.pendingTriples;
1381
- this.pendingTriples = [];
1382
- }
1383
- this.next(); // consume '.'
1384
- }
1385
- else {
1386
- more = this.parsePredicateObjectList(first);
1387
- this.expectDot();
1388
- }
1389
- // normalize explicit log:implies / log:impliedBy at top-level
1390
- for (const tr of more) {
1391
- if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
1392
- forwardRules.push(this.makeRule(tr.s, tr.o, true));
1393
- }
1394
- else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
1395
- backwardRules.push(this.makeRule(tr.s, tr.o, false));
1396
- }
1397
- else {
1398
- triples.push(tr);
1399
- }
1400
- }
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
- parseBaseDirective() {
1435
- const tok = this.next();
1436
- let iri;
1437
- if (tok.typ === 'IriRef') {
1438
- iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
1439
- }
1440
- else if (tok.typ === 'Ident') {
1441
- iri = tok.value || '';
1442
- }
1443
- else {
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
- parseSparqlPrefixDirective() {
1450
- // SPARQL/Turtle-style PREFIX directive: PREFIX pfx: <iri> (no trailing '.')
1451
- const tok = this.next();
1452
- if (tok.typ !== 'Ident') {
1453
- this.fail(`Expected prefix name after PREFIX, got ${tok.toString()}`, tok);
1454
- }
1455
- const pref = tok.value || '';
1456
- const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
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
- // N3/Turtle: PREFIX directives do not have a trailing '.', but accept it permissively.
1469
- if (this.peek().typ === 'Dot')
1470
- this.next();
1471
- this.prefixes.set(prefName, iri);
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
- else if (tok.typ === 'Ident') {
1481
- iri = tok.value || '';
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
- else {
1484
- this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
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
- // N3/Turtle: BASE directives do not have a trailing '.', but accept it permissively.
1487
- if (this.peek().typ === 'Dot')
1488
- this.next();
1489
- this.prefixes.setBase(iri);
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
- return t;
1502
- }
1503
- parsePathItem() {
1504
- const tok = this.next();
1505
- const typ = tok.typ;
1506
- const val = tok.value;
1507
- if (typ === 'Equals') {
1508
- return internIri(OWL_NS + 'sameAs');
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
- if (typ === 'IriRef') {
1511
- const base = this.prefixes.baseIri || '';
1512
- return internIri(resolveIriRef(val || '', base));
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
- if (typ === 'Ident') {
1515
- const name = val || '';
1516
- if (name === 'a') {
1517
- return internIri(RDF_NS + 'type');
1518
- }
1519
- else if (name.startsWith('_:')) {
1520
- return new Blank(name);
1521
- }
1522
- else if (name.includes(':')) {
1523
- return internIri(this.prefixes.expandQName(name));
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
- else {
1526
- return internIri(name);
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
- if (typ === 'Literal') {
1530
- let s = val || '';
1531
- // Optional language tag: "..."@en, per N3 LANGTAG production.
1532
- if (this.peek().typ === 'LangTag') {
1533
- // Only quoted string literals can carry a language tag.
1534
- if (!(s.startsWith('"') && s.endsWith('"'))) {
1535
- this.fail('Language tag is only allowed on quoted string literals', this.peek());
1536
- }
1537
- const langTok = this.next();
1538
- const lang = langTok.value || '';
1539
- s = `${s}@${lang}`;
1540
- // N3/Turtle: language tags and datatypes are mutually exclusive.
1541
- if (this.peek().typ === 'HatHat') {
1542
- this.fail('A literal cannot have both a language tag (@...) and a datatype (^^...)', this.peek());
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
- if (this.peek().typ === 'HatHat') {
1546
- this.next();
1547
- const dtTok = this.next();
1548
- let dtIri;
1549
- if (dtTok.typ === 'IriRef') {
1550
- dtIri = dtTok.value || '';
1551
- }
1552
- else if (dtTok.typ === 'Ident') {
1553
- const qn = dtTok.value || '';
1554
- if (qn.includes(':'))
1555
- dtIri = this.prefixes.expandQName(qn);
1556
- else
1557
- dtIri = qn;
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
- s = `${s}^^<${dtIri}>`;
1603
+ if (cc === "'")
1604
+ break;
1605
+ sChars.push(cc);
1563
1606
  }
1564
- return internLiteral(s);
1565
- }
1566
- if (typ === 'Var')
1567
- return new Var(val || '');
1568
- if (typ === 'LParen')
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
- // IRI property list: [ id <IRI> predicateObjectList? ]
1592
- // Lets you embed descriptions of an IRI directly in object position.
1593
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'id') {
1594
- const iriTok = this.next(); // consume 'id'
1595
- const iriTerm = this.parseTerm();
1596
- // N3 note: 'id' form is not meant to be used with blank node identifiers.
1597
- if (iriTerm instanceof Blank && iriTerm.label.startsWith('_:')) {
1598
- this.fail("Cannot use 'id' keyword with a blank node identifier inside [...]", iriTok);
1599
- }
1600
- // Optional ';' right after the id IRI (tolerated).
1601
- if (this.peek().typ === 'Semicolon')
1602
- this.next();
1603
- // Empty IRI property list: [ id :iri ]
1604
- if (this.peek().typ === 'RBracket') {
1605
- this.next();
1606
- return iriTerm;
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 subj = iriTerm;
1609
- while (true) {
1610
- let pred;
1611
- let invert = false;
1612
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
1613
- this.next();
1614
- pred = internIri(RDF_NS + 'type');
1615
- }
1616
- else if (this.peek().typ === 'OpPredInvert') {
1617
- this.next(); // "<-"
1618
- pred = this.parseTerm();
1619
- invert = true;
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
- else {
1622
- pred = this.parseTerm();
1641
+ while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
1642
+ tagChars.push(cc);
1643
+ i++;
1623
1644
  }
1624
- const objs = [this.parseTerm()];
1625
- while (this.peek().typ === 'Comma') {
1626
- this.next();
1627
- objs.push(this.parseTerm());
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
- for (const o of objs) {
1630
- this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
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 (this.peek().typ === 'Semicolon') {
1633
- this.next();
1634
- if (this.peek().typ === 'RBracket')
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
- continue;
1697
+ }
1637
1698
  }
1638
1699
  break;
1639
1700
  }
1640
- if (this.peek().typ !== 'RBracket') {
1641
- this.fail(`Expected ']' at end of IRI property list, got ${this.peek().toString()}`);
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
- this.next();
1644
- return iriTerm;
1719
+ tokens.push(new Token('Literal', numChars.join(''), start));
1720
+ continue;
1645
1721
  }
1646
- // [ predicateObjectList ]
1647
- this.blankCounter += 1;
1648
- const id = `_:b${this.blankCounter}`;
1649
- const subj = new Blank(id);
1650
- while (true) {
1651
- // Verb (can also be 'a')
1652
- let pred;
1653
- let invert = false;
1654
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
1655
- this.next();
1656
- pred = internIri(RDF_NS + 'type');
1657
- }
1658
- else if (this.peek().typ === 'OpPredInvert') {
1659
- this.next(); // consume "<-"
1660
- pred = this.parseTerm();
1661
- invert = true;
1662
- }
1663
- else {
1664
- pred = this.parseTerm();
1665
- }
1666
- // Object list: o1, o2, ...
1667
- const objs = [this.parseTerm()];
1668
- while (this.peek().typ === 'Comma') {
1669
- this.next();
1670
- objs.push(this.parseTerm());
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
- for (const o of objs) {
1673
- this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
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
- if (this.peek().typ === 'Semicolon') {
1676
- this.next();
1677
- if (this.peek().typ === 'RBracket')
1678
- break;
1679
- continue;
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
- if (this.peek().typ === 'RBracket') {
1684
- this.next();
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
- else {
1687
- this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
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
- parseGraph() {
1951
+ parseDocument() {
1692
1952
  const triples = [];
1693
- while (this.peek().typ !== 'RBrace') {
1694
- const left = this.parseTerm();
1695
- if (this.peek().typ === 'OpImplies') {
1953
+ const forwardRules = [];
1954
+ const backwardRules = [];
1955
+ while (this.peek().typ !== 'EOF') {
1956
+ if (this.peek().typ === 'AtPrefix') {
1696
1957
  this.next();
1697
- const right = this.parseTerm();
1698
- const pred = internIri(LOG_NS + 'implies');
1699
- triples.push(new Triple(left, pred, right));
1700
- if (this.peek().typ === 'Dot')
1701
- this.next();
1702
- else if (this.peek().typ === 'RBrace') {
1703
- // ok
1704
- }
1705
- else {
1706
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1707
- }
1958
+ this.parsePrefixDirective();
1708
1959
  }
1709
- else if (this.peek().typ === 'OpImpliedBy') {
1960
+ else if (this.peek().typ === 'AtBase') {
1710
1961
  this.next();
1711
- const right = this.parseTerm();
1712
- const pred = internIri(LOG_NS + 'impliedBy');
1713
- triples.push(new Triple(left, pred, right));
1714
- if (this.peek().typ === 'Dot')
1715
- this.next();
1716
- else if (this.peek().typ === 'RBrace') {
1717
- // ok
1718
- }
1719
- else {
1720
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1721
- }
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
- // N3 grammar allows: triples ::= subject predicateObjectList?
1725
- // So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
1726
- if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
1727
- if (this.pendingTriples.length > 0) {
1728
- triples.push(...this.pendingTriples);
1729
- this.pendingTriples = [];
1730
- }
1731
- if (this.peek().typ === 'Dot')
1732
- this.next();
1733
- continue;
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
- triples.push(...this.parsePredicateObjectList(left));
1736
- if (this.peek().typ === 'Dot')
1996
+ else if (this.peek().typ === 'OpImpliedBy') {
1737
1997
  this.next();
1738
- else if (this.peek().typ === 'RBrace') {
1739
- // ok
1998
+ const second = this.parseTerm();
1999
+ this.expectDot();
2000
+ backwardRules.push(this.makeRule(first, second, false));
1740
2001
  }
1741
2002
  else {
1742
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1743
- }
1744
- }
1745
- }
1746
- this.next(); // consume '}'
1747
- return new GraphTerm(triples);
1748
- }
1749
- parsePredicateObjectList(subject) {
1750
- const out = [];
1751
- // If the SUBJECT was a path, emit its helper triples first
1752
- if (this.pendingTriples.length > 0) {
1753
- out.push(...this.pendingTriples);
1754
- this.pendingTriples = [];
1755
- }
1756
- while (true) {
1757
- let verb;
1758
- let invert = false;
1759
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
1760
- this.next();
1761
- verb = internIri(RDF_NS + 'type');
1762
- }
1763
- else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
1764
- // N3 syntactic sugar: "S has P O." means "S P O."
1765
- this.next(); // consume "has"
1766
- verb = this.parseTerm();
1767
- }
1768
- else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
1769
- // N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
1770
- this.next(); // consume "is"
1771
- verb = this.parseTerm();
1772
- if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
1773
- this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
1774
- }
1775
- this.next(); // consume "of"
1776
- invert = true;
1777
- }
1778
- else if (this.peek().typ === 'OpPredInvert') {
1779
- this.next(); // "<-"
1780
- verb = this.parseTerm();
1781
- invert = true;
1782
- }
1783
- else {
1784
- verb = this.parseTerm();
1785
- }
1786
- const objects = this.parseObjectList();
1787
- // If VERB or OBJECTS contained paths, their helper triples must come
1788
- // before the triples that consume the path results (Easter depends on this).
1789
- if (this.pendingTriples.length > 0) {
1790
- out.push(...this.pendingTriples);
1791
- this.pendingTriples = [];
1792
- }
1793
- for (const o of objects) {
1794
- out.push(new Triple(invert ? o : subject, verb, invert ? subject : o));
1795
- }
1796
- if (this.peek().typ === 'Semicolon') {
1797
- this.next();
1798
- if (this.peek().typ === 'Dot')
1799
- break;
1800
- continue;
1801
- }
1802
- break;
1803
- }
1804
- return out;
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
- let rawPremise;
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
- // Blank-node lifting and Skolemization
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
- if (t instanceof OpenListTerm) {
1875
- return new OpenListTerm(t.prefix.map((e) => convertTerm(e, mapping, counter)), t.tailVar);
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
- if (t instanceof GraphTerm) {
1878
- const triples = t.triples.map((tr) => new Triple(convertTerm(tr.s, mapping, counter), convertTerm(tr.p, mapping, counter), convertTerm(tr.o, mapping, counter)));
1879
- return new GraphTerm(triples);
2051
+ const tok2 = this.next();
2052
+ let iri;
2053
+ if (tok2.typ === 'IriRef') {
2054
+ iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
1880
2055
  }
1881
- return t;
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
- if (!mapping.hasOwnProperty(label)) {
1908
- // If we have a global cache keyed by firingKey, use it to ensure
1909
- // deterministic blank IDs for the same rule+substitution instance.
1910
- if (globalMap && firingKey) {
1911
- const gk = `${firingKey}|${label}`;
1912
- let sk = globalMap.get(gk);
1913
- if (!sk) {
1914
- const idx = skCounter[0];
1915
- skCounter[0] += 1;
1916
- sk = `_:sk_${idx}`;
1917
- globalMap.set(gk, sk);
1918
- }
1919
- mapping[label] = sk;
1920
- }
1921
- else {
1922
- const idx = skCounter[0];
1923
- skCounter[0] += 1;
1924
- mapping[label] = `_:sk_${idx}`;
1925
- }
2059
+ else {
2060
+ this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
1926
2061
  }
1927
- return new Blank(mapping[label]);
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
- return t;
1939
- }
1940
- function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
1941
- 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));
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
- return false;
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
- return true;
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
- return true;
1999
- }
2000
- if (a instanceof GraphTerm) {
2001
- return alphaEqGraphTriples(a.triples, b.triples);
2077
+ this.expectDot();
2078
+ this.prefixes.setBase(iri);
2002
2079
  }
2003
- return false;
2004
- }
2005
- function termsEqualNoIntDecimal(a, b) {
2006
- if (a === b)
2007
- return true;
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
- return false;
2044
- }
2045
- if (a instanceof Var)
2046
- return a.name === b.name;
2047
- if (a instanceof Blank)
2048
- return a.label === b.label;
2049
- if (a instanceof ListTerm) {
2050
- if (a.elems.length !== b.elems.length)
2051
- return false;
2052
- for (let i = 0; i < a.elems.length; i++) {
2053
- if (!termsEqualNoIntDecimal(a.elems[i], b.elems[i]))
2054
- return false;
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
- return true;
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
- return true;
2068
- }
2069
- if (a instanceof GraphTerm) {
2070
- return alphaEqGraphTriples(a.triples, b.triples);
2071
- }
2072
- return false;
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
- // Variables: renamable (ONLY inside quoted formulas)
2105
- if (a instanceof Var && b instanceof Var) {
2106
- return alphaEqVarName(a.name, b.name, vmap);
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
- if (a instanceof Iri && b instanceof Iri)
2109
- return a.value === b.value;
2110
- if (a instanceof Literal && b instanceof Literal)
2111
- return a.value === b.value;
2112
- if (a instanceof ListTerm && b instanceof ListTerm) {
2113
- if (a.elems.length !== b.elems.length)
2114
- return false;
2115
- for (let i = 0; i < a.elems.length; i++) {
2116
- if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap))
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 true;
2132
+ return t;
2120
2133
  }
2121
- if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
2122
- if (a.prefix.length !== b.prefix.length)
2123
- return false;
2124
- for (let i = 0; i < a.prefix.length; i++) {
2125
- if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap))
2126
- return false;
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
- // tailVar is a var-name string, so treat it as renamable too
2129
- return alphaEqVarName(a.tailVar, b.tailVar, vmap);
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
- // Nested formulas: compare with fresh maps (separate scope)
2132
- if (a instanceof GraphTerm && b instanceof GraphTerm) {
2133
- return alphaEqGraphTriples(a.triples, b.triples);
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
- return false;
2136
- }
2137
- function alphaEqTripleInGraph(a, b, vmap, bmap) {
2138
- return (alphaEqTermInGraph(a.s, b.s, vmap, bmap) &&
2139
- alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
2140
- alphaEqTermInGraph(a.o, b.o, vmap, bmap));
2141
- }
2142
- function alphaEqGraphTriples(xs, ys) {
2143
- if (xs.length !== ys.length)
2144
- return false;
2145
- // Fast path: exact same sequence.
2146
- if (triplesListEqual(xs, ys))
2147
- return true;
2148
- // Order-insensitive backtracking match, threading var/blank mappings.
2149
- const used = new Array(ys.length).fill(false);
2150
- function step(i, vmap, bmap) {
2151
- if (i >= xs.length)
2152
- return true;
2153
- const x = xs[i];
2154
- for (let j = 0; j < ys.length; j++) {
2155
- if (used[j])
2156
- continue;
2157
- const y = ys[j];
2158
- // Cheap pruning when both predicates are IRIs.
2159
- if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
2160
- continue;
2161
- const v2 = { ...vmap };
2162
- const b2 = { ...bmap };
2163
- if (!alphaEqTripleInGraph(x, y, v2, b2))
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
- used[j] = true;
2166
- if (step(i + 1, v2, b2))
2167
- return true;
2168
- used[j] = false;
2311
+ }
2312
+ break;
2169
2313
  }
2170
- return false;
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
- bmap[x] = y;
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 true;
2320
+ return new Blank(id);
2200
2321
  }
2201
- if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
2202
- if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
2203
- return false;
2204
- for (let i = 0; i < a.prefix.length; i++) {
2205
- if (!alphaEqTerm(a.prefix[i], b.prefix[i], bmap))
2206
- return false;
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
- return true;
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
- return false;
2215
- }
2216
- function alphaEqTriple(a, b) {
2217
- const bmap = {};
2218
- return alphaEqTerm(a.s, b.s, bmap) && alphaEqTerm(a.p, b.p, bmap) && alphaEqTerm(a.o, b.o, bmap);
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
- pb.push(tr);
2282
- const sk = termFastKey(tr.s);
2283
- if (sk !== null) {
2284
- let ps = facts.__byPS.get(pk);
2285
- if (!ps) {
2286
- ps = new Map();
2287
- facts.__byPS.set(pk, ps);
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
- let psb = ps.get(sk);
2290
- if (!psb) {
2291
- psb = [];
2292
- ps.set(sk, psb);
2409
+ else if (this.peek().typ === 'OpPredInvert') {
2410
+ this.next(); // "<-"
2411
+ verb = this.parseTerm();
2412
+ invert = true;
2293
2413
  }
2294
- psb.push(tr);
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
- let pob = po.get(ok);
2304
- if (!pob) {
2305
- pob = [];
2306
- po.set(ok, pob);
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
- pob.push(tr);
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
- const key = tripleFastKey(tr);
2312
- if (key !== null)
2313
- facts.__keySet.add(key);
2314
- }
2315
- function candidateFacts(facts, goal) {
2316
- ensureFactIndexes(facts);
2317
- if (goal.p instanceof Iri) {
2318
- const pk = goal.p.value;
2319
- const sk = termFastKey(goal.s);
2320
- const ok = termFastKey(goal.o);
2321
- /** @type {Triple[] | null} */
2322
- let byPS = null;
2323
- if (sk !== null) {
2324
- const ps = facts.__byPS.get(pk);
2325
- if (ps)
2326
- byPS = ps.get(sk) || null;
2327
- }
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
- if (byPS && byPO)
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
- return facts;
2344
- }
2345
- function hasFactIndexed(facts, tr) {
2346
- ensureFactIndexes(facts);
2347
- const key = tripleFastKey(tr);
2348
- if (key !== null)
2349
- return facts.__keySet.has(key);
2350
- if (tr.p instanceof Iri) {
2351
- const pk = tr.p.value;
2352
- const ok = termFastKey(tr.o);
2353
- if (ok !== null) {
2354
- const po = facts.__byPO.get(pk);
2355
- if (po) {
2356
- const pob = po.get(ok) || [];
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
- const pb = facts.__byPred.get(pk) || [];
2365
- return pb.some((t) => triplesEqual(t, tr));
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
- bucket.push(r);
2403
- }
2404
- else {
2405
- backRules.__wildHeadPred.push(r);
2406
- }
2407
- }
2408
- // ===========================================================================
2409
- // Special predicate helpers
2410
- // ===========================================================================
2411
- function isRdfTypePred(p) {
2412
- return p instanceof Iri && p.value === RDF_NS + 'type';
2413
- }
2414
- function isOwlSameAsPred(t) {
2415
- return t instanceof Iri && t.value === OWL_NS + 'sameAs';
2416
- }
2417
- function isLogImplies(p) {
2418
- return p instanceof Iri && p.value === LOG_NS + 'implies';
2419
- }
2420
- function isLogImpliedBy(p) {
2421
- return p instanceof Iri && p.value === LOG_NS + 'impliedBy';
2422
- }
2423
- // ===========================================================================
2424
- // Constraint / "test" builtins
2425
- // ===========================================================================
2426
- function isConstraintBuiltin(tr) {
2427
- if (!(tr.p instanceof Iri))
2428
- return false;
2429
- const v = tr.p.value;
2430
- // math: numeric comparisons (no new bindings, just tests)
2431
- if (v === MATH_NS + 'equalTo' ||
2432
- v === MATH_NS + 'greaterThan' ||
2433
- v === MATH_NS + 'lessThan' ||
2434
- v === MATH_NS + 'notEqualTo' ||
2435
- v === MATH_NS + 'notGreaterThan' ||
2436
- v === MATH_NS + 'notLessThan') {
2437
- return true;
2438
- }
2439
- // list: membership test with no bindings
2440
- if (v === LIST_NS + 'notMember') {
2441
- return true;
2442
- }
2443
- // log: tests that are purely constraints (no new bindings)
2444
- if (v === LOG_NS + 'forAllIn' ||
2445
- v === LOG_NS + 'notEqualTo' ||
2446
- v === LOG_NS + 'notIncludes' ||
2447
- v === LOG_NS + 'outputString') {
2448
- return true;
2449
- }
2450
- // string: relational / membership style tests (no bindings)
2451
- if (v === STRING_NS + 'contains' ||
2452
- v === STRING_NS + 'containsIgnoringCase' ||
2453
- v === STRING_NS + 'endsWith' ||
2454
- v === STRING_NS + 'equalIgnoringCase' ||
2455
- v === STRING_NS + 'greaterThan' ||
2456
- v === STRING_NS + 'lessThan' ||
2457
- v === STRING_NS + 'matches' ||
2458
- v === STRING_NS + 'notEqualIgnoringCase' ||
2459
- v === STRING_NS + 'notGreaterThan' ||
2460
- v === STRING_NS + 'notLessThan' ||
2461
- v === STRING_NS + 'notMatches' ||
2462
- v === STRING_NS + 'startsWith') {
2463
- return true;
2464
- }
2465
- return false;
2466
- }
2467
- // Move constraint builtins to the end of the rule premise.
2468
- // This is a simple "delaying" strategy similar in spirit to Prolog's when/2:
2469
- // - normal goals first (can bind variables),
2470
- // - pure test / constraint builtins last (checked once bindings are in place).
2471
- function reorderPremiseForConstraints(premise) {
2472
- if (!premise || premise.length === 0)
2473
- return premise;
2474
- const normal = [];
2475
- const delayed = [];
2476
- for (const tr of premise) {
2477
- if (isConstraintBuiltin(tr))
2478
- delayed.push(tr);
2479
- else
2480
- normal.push(tr);
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 */