eyeling 1.9.1 → 1.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/eyeling.js CHANGED
@@ -624,2193 +624,1870 @@ 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);
766
+ return true;
767
+ }
768
+ if (a instanceof GraphTerm) {
769
+ return alphaEqGraphTriples(a.triples, b.triples);
770
+ }
771
+ return false;
772
+ }
773
+ function termsEqualNoIntDecimal(a, b) {
774
+ if (a === b)
775
+ return true;
776
+ if (!a || !b)
777
+ return false;
778
+ if (a.constructor !== b.constructor)
779
+ return false;
780
+ if (a instanceof Iri)
781
+ return a.value === b.value;
782
+ if (a instanceof Literal) {
783
+ if (a.value === b.value)
784
+ return true;
785
+ // Plain "abc" == "abc"^^xsd:string (but not language-tagged)
786
+ if (literalsEquivalentAsXsdString(a.value, b.value))
787
+ return true;
788
+ // Numeric equality ONLY when datatypes agree (no integer<->decimal here)
789
+ const ai = parseNumericLiteralInfo(a);
790
+ const bi = parseNumericLiteralInfo(b);
791
+ if (ai && bi && ai.dt === bi.dt) {
792
+ // integer: exact bigint
793
+ if (ai.kind === 'bigint' && bi.kind === 'bigint')
794
+ return ai.value === bi.value;
795
+ // decimal: compare exactly via num/scale if possible
796
+ if (ai.dt === XSD_NS + 'decimal') {
797
+ const da = parseXsdDecimalToBigIntScale(ai.lexStr);
798
+ const db = parseXsdDecimalToBigIntScale(bi.lexStr);
799
+ if (da && db) {
800
+ const scale = Math.max(da.scale, db.scale);
801
+ const na = da.num * pow10n(scale - da.scale);
802
+ const nb = db.num * pow10n(scale - db.scale);
803
+ return na === nb;
804
+ }
787
805
  }
788
- i++; // skip '>'
789
- const iri = iriChars.join('');
790
- tokens.push(new Token('IriRef', iri, start));
791
- 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;
792
810
  }
793
- // 4) Path + datatype operators: !, ^, ^^
794
- if (c === '!') {
795
- tokens.push(new Token('OpPathFwd', null, i));
796
- i += 1;
797
- continue;
811
+ return false;
812
+ }
813
+ if (a instanceof Var)
814
+ return a.name === b.name;
815
+ if (a instanceof Blank)
816
+ return a.label === b.label;
817
+ if (a instanceof ListTerm) {
818
+ if (a.elems.length !== b.elems.length)
819
+ return false;
820
+ for (let i = 0; i < a.elems.length; i++) {
821
+ if (!termsEqualNoIntDecimal(a.elems[i], b.elems[i]))
822
+ return false;
798
823
  }
799
- 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;
824
+ return true;
825
+ }
826
+ if (a instanceof OpenListTerm) {
827
+ if (a.tailVar !== b.tailVar)
828
+ return false;
829
+ if (a.prefix.length !== b.prefix.length)
830
+ return false;
831
+ for (let i = 0; i < a.prefix.length; i++) {
832
+ if (!termsEqualNoIntDecimal(a.prefix[i], b.prefix[i]))
833
+ return false;
808
834
  }
809
- // 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;
835
+ return true;
836
+ }
837
+ if (a instanceof GraphTerm) {
838
+ return alphaEqGraphTriples(a.triples, b.triples);
839
+ }
840
+ return false;
841
+ }
842
+ function triplesEqual(a, b) {
843
+ return termsEqual(a.s, b.s) && termsEqual(a.p, b.p) && termsEqual(a.o, b.o);
844
+ }
845
+ function triplesListEqual(xs, ys) {
846
+ if (xs.length !== ys.length)
847
+ return false;
848
+ for (let i = 0; i < xs.length; i++) {
849
+ if (!triplesEqual(xs[i], ys[i]))
850
+ return false;
851
+ }
852
+ return true;
853
+ }
854
+ // Alpha-equivalence for quoted formulas, up to *variable* and blank-node renaming.
855
+ // Treats a formula as an unordered set of triples (order-insensitive match).
856
+ function alphaEqVarName(x, y, vmap) {
857
+ if (vmap.hasOwnProperty(x))
858
+ return vmap[x] === y;
859
+ vmap[x] = y;
860
+ return true;
861
+ }
862
+ function alphaEqTermInGraph(a, b, vmap, bmap) {
863
+ // Blank nodes: renamable
864
+ if (a instanceof Blank && b instanceof Blank) {
865
+ const x = a.label;
866
+ const y = b.label;
867
+ if (bmap.hasOwnProperty(x))
868
+ return bmap[x] === y;
869
+ bmap[x] = y;
870
+ return true;
871
+ }
872
+ // Variables: renamable (ONLY inside quoted formulas)
873
+ if (a instanceof Var && b instanceof Var) {
874
+ return alphaEqVarName(a.name, b.name, vmap);
875
+ }
876
+ if (a instanceof Iri && b instanceof Iri)
877
+ return a.value === b.value;
878
+ if (a instanceof Literal && b instanceof Literal)
879
+ return a.value === b.value;
880
+ if (a instanceof ListTerm && b instanceof ListTerm) {
881
+ if (a.elems.length !== b.elems.length)
882
+ return false;
883
+ for (let i = 0; i < a.elems.length; i++) {
884
+ if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap))
885
+ return false;
825
886
  }
826
- // 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;
887
+ return true;
888
+ }
889
+ if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
890
+ if (a.prefix.length !== b.prefix.length)
891
+ return false;
892
+ for (let i = 0; i < a.prefix.length; i++) {
893
+ if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap))
894
+ return false;
903
895
  }
904
- // 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));
896
+ // tailVar is a var-name string, so treat it as renamable too
897
+ return alphaEqVarName(a.tailVar, b.tailVar, vmap);
898
+ }
899
+ // Nested formulas: compare with fresh maps (separate scope)
900
+ if (a instanceof GraphTerm && b instanceof GraphTerm) {
901
+ return alphaEqGraphTriples(a.triples, b.triples);
902
+ }
903
+ return false;
904
+ }
905
+ function alphaEqTripleInGraph(a, b, vmap, bmap) {
906
+ return (alphaEqTermInGraph(a.s, b.s, vmap, bmap) &&
907
+ alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
908
+ alphaEqTermInGraph(a.o, b.o, vmap, bmap));
909
+ }
910
+ function alphaEqGraphTriples(xs, ys) {
911
+ if (xs.length !== ys.length)
912
+ return false;
913
+ // Fast path: exact same sequence.
914
+ if (triplesListEqual(xs, ys))
915
+ return true;
916
+ // Order-insensitive backtracking match, threading var/blank mappings.
917
+ const used = new Array(ys.length).fill(false);
918
+ function step(i, vmap, bmap) {
919
+ if (i >= xs.length)
920
+ return true;
921
+ const x = xs[i];
922
+ for (let j = 0; j < ys.length; j++) {
923
+ if (used[j])
955
924
  continue;
956
- }
957
- // 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));
925
+ const y = ys[j];
926
+ // Cheap pruning when both predicates are IRIs.
927
+ if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
1028
928
  continue;
1029
- }
1030
- // 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
- }
1086
- }
1087
- }
1088
- tokens.push(new Token('Literal', numChars.join(''), start));
1089
- continue;
1090
- }
1091
- // 7) Identifiers / keywords / QNames
1092
- const start = i;
1093
- const wordChars = [];
1094
- let cc;
1095
- while ((cc = peek()) !== null && isNameChar(cc)) {
1096
- wordChars.push(cc);
1097
- i++;
1098
- }
1099
- if (!wordChars.length) {
1100
- throw new N3SyntaxError(`Unexpected char: ${JSON.stringify(c)}`, i);
1101
- }
1102
- const word = wordChars.join('');
1103
- if (word === 'true' || word === 'false') {
1104
- tokens.push(new Token('Literal', word, start));
929
+ const v2 = { ...vmap };
930
+ const b2 = { ...bmap };
931
+ if (!alphaEqTripleInGraph(x, y, v2, b2))
932
+ continue;
933
+ used[j] = true;
934
+ if (step(i + 1, v2, b2))
935
+ return true;
936
+ used[j] = false;
1105
937
  }
1106
- else if ([...word].every((ch) => /[0-9.\-]/.test(ch))) {
1107
- tokens.push(new Token('Literal', word, start));
938
+ return false;
939
+ }
940
+ return step(0, {}, {});
941
+ }
942
+ function alphaEqTerm(a, b, bmap) {
943
+ if (a instanceof Blank && b instanceof Blank) {
944
+ const x = a.label;
945
+ const y = b.label;
946
+ if (bmap.hasOwnProperty(x)) {
947
+ return bmap[x] === y;
1108
948
  }
1109
949
  else {
1110
- tokens.push(new Token('Ident', word, start));
950
+ bmap[x] = y;
951
+ return true;
1111
952
  }
1112
953
  }
1113
- tokens.push(new Token('EOF', null, n));
1114
- return tokens;
1115
- }
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>
1123
- }
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;
1223
1095
  }
1224
- }
1225
- return out;
1226
- }
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);
1096
+ /** @type {Triple[] | null} */
1097
+ let byPO = null;
1098
+ if (ok !== null) {
1099
+ const po = facts.__byPO.get(pk);
1100
+ if (po)
1101
+ byPO = po.get(ok) || null;
1245
1102
  }
1103
+ if (byPS && byPO)
1104
+ return byPS.length <= byPO.length ? byPS : byPO;
1105
+ if (byPS)
1106
+ return byPS;
1107
+ if (byPO)
1108
+ return byPO;
1109
+ return facts.__byPred.get(pk) || [];
1246
1110
  }
1111
+ return facts;
1247
1112
  }
1248
- function 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);
1113
+ function hasFactIndexed(facts, tr) {
1114
+ ensureFactIndexes(facts);
1115
+ const key = tripleFastKey(tr);
1116
+ if (key !== null)
1117
+ return facts.__keySet.has(key);
1118
+ if (tr.p instanceof Iri) {
1119
+ const pk = tr.p.value;
1120
+ const ok = termFastKey(tr.o);
1121
+ if (ok !== null) {
1122
+ const po = facts.__byPO.get(pk);
1123
+ if (po) {
1124
+ const pob = po.get(ok) || [];
1125
+ // Facts are all in the same graph. Different blank node labels represent
1126
+ // different existentials unless explicitly connected. Do NOT treat
1127
+ // triples as duplicates modulo blank renaming, or you'll incorrectly
1128
+ // drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
1129
+ return pob.some((t) => triplesEqual(t, tr));
1130
+ }
1131
+ }
1132
+ const pb = facts.__byPred.get(pk) || [];
1133
+ return pb.some((t) => triplesEqual(t, tr));
1254
1134
  }
1255
- for (const tr of rule.conclusion) {
1256
- collectVarsInTerm(tr.s, acc);
1257
- collectVarsInTerm(tr.p, acc);
1258
- collectVarsInTerm(tr.o, acc);
1135
+ // Non-IRI predicate: fall back to strict triple equality.
1136
+ return facts.some((t) => triplesEqual(t, tr));
1137
+ }
1138
+ function pushFactIndexed(facts, tr) {
1139
+ ensureFactIndexes(facts);
1140
+ facts.push(tr);
1141
+ indexFact(facts, tr);
1142
+ }
1143
+ function ensureBackRuleIndexes(backRules) {
1144
+ if (backRules.__byHeadPred && backRules.__wildHeadPred)
1145
+ return;
1146
+ Object.defineProperty(backRules, '__byHeadPred', {
1147
+ value: new Map(),
1148
+ enumerable: false,
1149
+ writable: true,
1150
+ });
1151
+ Object.defineProperty(backRules, '__wildHeadPred', {
1152
+ value: [],
1153
+ enumerable: false,
1154
+ writable: true,
1155
+ });
1156
+ for (const r of backRules)
1157
+ indexBackRule(backRules, r);
1158
+ }
1159
+ function indexBackRule(backRules, r) {
1160
+ if (!r || !r.conclusion || r.conclusion.length !== 1)
1161
+ return;
1162
+ const head = r.conclusion[0];
1163
+ if (head && head.p instanceof Iri) {
1164
+ const k = head.p.value;
1165
+ let bucket = backRules.__byHeadPred.get(k);
1166
+ if (!bucket) {
1167
+ bucket = [];
1168
+ backRules.__byHeadPred.set(k, bucket);
1169
+ }
1170
+ bucket.push(r);
1171
+ }
1172
+ else {
1173
+ backRules.__wildHeadPred.push(r);
1259
1174
  }
1260
- return acc;
1261
1175
  }
1262
- 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));
1295
+ if (i + 1 >= s.length) {
1296
+ out += '\\';
1297
+ continue;
1298
+ }
1299
+ const e = s[++i];
1300
+ switch (e) {
1301
+ case 't':
1302
+ out += '\t';
1303
+ break;
1304
+ case 'n':
1305
+ out += '\n';
1306
+ break;
1307
+ case 'r':
1308
+ out += '\r';
1309
+ break;
1310
+ case 'b':
1311
+ out += '\b';
1312
+ break;
1313
+ case 'f':
1314
+ out += '\f';
1315
+ break;
1316
+ case '"':
1317
+ out += '"';
1318
+ break;
1319
+ case "'":
1320
+ out += "'";
1321
+ break;
1322
+ case '\\':
1323
+ out += '\\';
1324
+ break;
1325
+ case 'u': {
1326
+ const hex = s.slice(i + 1, i + 5);
1327
+ if (/^[0-9A-Fa-f]{4}$/.test(hex)) {
1328
+ out += String.fromCharCode(parseInt(hex, 16));
1329
+ i += 4;
1364
1330
  }
1365
- else if (this.peek().typ === 'OpImpliedBy') {
1366
- this.next();
1367
- const second = this.parseTerm();
1368
- this.expectDot();
1369
- backwardRules.push(this.makeRule(first, second, false));
1331
+ else {
1332
+ out += '\\u';
1333
+ }
1334
+ break;
1335
+ }
1336
+ case 'U': {
1337
+ const hex = s.slice(i + 1, i + 9);
1338
+ if (/^[0-9A-Fa-f]{8}$/.test(hex)) {
1339
+ const cp = parseInt(hex, 16);
1340
+ if (cp >= 0 && cp <= 0x10ffff)
1341
+ out += String.fromCodePoint(cp);
1342
+ else
1343
+ out += '\\U' + hex;
1344
+ i += 8;
1370
1345
  }
1371
1346
  else {
1372
- 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
- }
1347
+ out += '\\U';
1401
1348
  }
1349
+ break;
1402
1350
  }
1351
+ default:
1352
+ // preserve unknown escapes
1353
+ out += '\\' + e;
1403
1354
  }
1404
- return [this.prefixes, triples, forwardRules, backwardRules];
1405
1355
  }
1406
- 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);
1356
+ return out;
1357
+ }
1358
+ function lex(inputText) {
1359
+ const chars = Array.from(inputText);
1360
+ const n = chars.length;
1361
+ let i = 0;
1362
+ const tokens = [];
1363
+ function peek(offset = 0) {
1364
+ const j = i + offset;
1365
+ return j >= 0 && j < n ? chars[j] : null;
1433
1366
  }
1434
- 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 || '';
1367
+ while (i < n) {
1368
+ let c = peek();
1369
+ if (c === null)
1370
+ break;
1371
+ // 1) Whitespace
1372
+ if (isWs(c)) {
1373
+ i++;
1374
+ continue;
1442
1375
  }
1443
- else {
1444
- this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
1376
+ // 2) Comments starting with '#'
1377
+ if (c === '#') {
1378
+ while (i < n && chars[i] !== '\n' && chars[i] !== '\r')
1379
+ i++;
1380
+ continue;
1445
1381
  }
1446
- this.expectDot();
1447
- this.prefixes.setBase(iri);
1448
- }
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);
1382
+ // 3) Two-character operators: => and <=
1383
+ if (c === '=') {
1384
+ if (peek(1) === '>') {
1385
+ tokens.push(new Token('OpImplies', null, i));
1386
+ i += 2;
1387
+ continue;
1388
+ }
1389
+ else {
1390
+ // N3 syntactic sugar: '=' means owl:sameAs
1391
+ tokens.push(new Token('Equals', null, i));
1392
+ i += 1;
1393
+ continue;
1394
+ }
1454
1395
  }
1455
- 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 || '');
1396
+ if (c === '<') {
1397
+ if (peek(1) === '=') {
1398
+ tokens.push(new Token('OpImpliedBy', null, i));
1399
+ i += 2;
1400
+ continue;
1401
+ }
1402
+ // N3 predicate inversion: "<-" (swap subject/object for this predicate)
1403
+ if (peek(1) === '-') {
1404
+ tokens.push(new Token('OpPredInvert', null, i));
1405
+ i += 2;
1406
+ continue;
1407
+ }
1408
+ // Otherwise IRIREF <...>
1409
+ const start = i;
1410
+ i++; // skip '<'
1411
+ const iriChars = [];
1412
+ while (i < n && chars[i] !== '>') {
1413
+ iriChars.push(chars[i]);
1414
+ i++;
1415
+ }
1416
+ if (i >= n || chars[i] !== '>') {
1417
+ throw new N3SyntaxError('Unterminated IRI <...>', start);
1418
+ }
1419
+ i++; // skip '>'
1420
+ const iri = iriChars.join('');
1421
+ tokens.push(new Token('IriRef', iri, start));
1422
+ continue;
1461
1423
  }
1462
- else if (tok2.typ === 'Ident') {
1463
- iri = this.prefixes.expandQName(tok2.value || '');
1424
+ // 4) Path + datatype operators: !, ^, ^^
1425
+ if (c === '!') {
1426
+ tokens.push(new Token('OpPathFwd', null, i));
1427
+ i += 1;
1428
+ continue;
1464
1429
  }
1465
- else {
1466
- this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
1430
+ if (c === '^') {
1431
+ if (peek(1) === '^') {
1432
+ tokens.push(new Token('HatHat', null, i));
1433
+ i += 2;
1434
+ continue;
1435
+ }
1436
+ tokens.push(new Token('OpPathRev', null, i));
1437
+ i += 1;
1438
+ continue;
1467
1439
  }
1468
- // 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 || '');
1440
+ // 5) Single-character punctuation
1441
+ if ('{}()[];,.'.includes(c)) {
1442
+ const mapping = {
1443
+ '{': 'LBrace',
1444
+ '}': 'RBrace',
1445
+ '(': 'LParen',
1446
+ ')': 'RParen',
1447
+ '[': 'LBracket',
1448
+ ']': 'RBracket',
1449
+ ';': 'Semicolon',
1450
+ ',': 'Comma',
1451
+ '.': 'Dot',
1452
+ };
1453
+ tokens.push(new Token(mapping[c], null, i));
1454
+ i++;
1455
+ continue;
1479
1456
  }
1480
- else if (tok.typ === 'Ident') {
1481
- iri = tok.value || '';
1482
- }
1483
- else {
1484
- this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
1485
- }
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;
1500
- }
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');
1509
- }
1510
- if (typ === 'IriRef') {
1511
- const base = this.prefixes.baseIri || '';
1512
- return internIri(resolveIriRef(val || '', base));
1513
- }
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);
1607
+ const raw = "'" + sChars.join('') + "'";
1608
+ const decoded = decodeN3StringEscapes(stripQuotes(raw));
1609
+ const s = JSON.stringify(decoded); // canonical short quoted form
1610
+ tokens.push(new Token('Literal', s, start));
1611
+ continue;
1565
1612
  }
1566
- 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());
1613
+ // Variable ?name
1614
+ if (c === '?') {
1615
+ const start = i;
1616
+ i++;
1617
+ const nameChars = [];
1618
+ let cc;
1619
+ while ((cc = peek()) !== null && isNameChar(cc)) {
1620
+ nameChars.push(cc);
1621
+ i++;
1622
+ }
1623
+ const name = nameChars.join('');
1624
+ tokens.push(new Token('Var', name, start));
1625
+ continue;
1580
1626
  }
1581
- 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}`);
1590
- }
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;
1607
- }
1608
- const subj = iriTerm;
1609
- while (true) {
1610
- let pred;
1611
- let invert = false;
1612
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
1613
- this.next();
1614
- pred = internIri(RDF_NS + 'type');
1615
- }
1616
- else if (this.peek().typ === 'OpPredInvert') {
1617
- this.next(); // "<-"
1618
- pred = this.parseTerm();
1619
- invert = true;
1627
+ // Directives: @prefix, @base (and language tags after string literals)
1628
+ if (c === '@') {
1629
+ const start = i;
1630
+ const prevTok = tokens.length ? tokens[tokens.length - 1] : null;
1631
+ const prevWasQuotedLiteral = prevTok && prevTok.typ === 'Literal' && typeof prevTok.value === 'string' && prevTok.value.startsWith('"');
1632
+ i++; // consume '@'
1633
+ if (prevWasQuotedLiteral) {
1634
+ // N3 grammar production LANGTAG:
1635
+ // "@" [a-zA-Z]+ ("-" [a-zA-Z0-9]+)*
1636
+ const tagChars = [];
1637
+ let cc = peek();
1638
+ if (cc === null || !/[A-Za-z]/.test(cc)) {
1639
+ throw new N3SyntaxError("Invalid language tag (expected [A-Za-z] after '@')", start);
1620
1640
  }
1621
- 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());
1671
- }
1672
- for (const o of objs) {
1673
- this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
1674
- }
1675
- if (this.peek().typ === 'Semicolon') {
1676
- this.next();
1677
- if (this.peek().typ === 'RBracket')
1678
- break;
1679
- continue;
1680
- }
1681
- break;
1722
+ // 7) Identifiers / keywords / QNames
1723
+ const start = i;
1724
+ const wordChars = [];
1725
+ let cc;
1726
+ while ((cc = peek()) !== null && isNameChar(cc)) {
1727
+ wordChars.push(cc);
1728
+ i++;
1682
1729
  }
1683
- if (this.peek().typ === 'RBracket') {
1684
- this.next();
1730
+ if (!wordChars.length) {
1731
+ throw new N3SyntaxError(`Unexpected char: ${JSON.stringify(c)}`, i);
1685
1732
  }
1686
- else {
1687
- this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
1733
+ const word = wordChars.join('');
1734
+ if (word === 'true' || word === 'false') {
1735
+ tokens.push(new Token('Literal', word, start));
1688
1736
  }
1689
- return new Blank(id);
1690
- }
1691
- parseGraph() {
1692
- const triples = [];
1693
- while (this.peek().typ !== 'RBrace') {
1694
- const left = this.parseTerm();
1695
- if (this.peek().typ === 'OpImplies') {
1696
- this.next();
1697
- const right = this.parseTerm();
1698
- const pred = internIri(LOG_NS + 'implies');
1699
- triples.push(new Triple(left, pred, right));
1700
- if (this.peek().typ === 'Dot')
1701
- this.next();
1702
- else if (this.peek().typ === 'RBrace') {
1703
- // ok
1704
- }
1705
- else {
1706
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1707
- }
1708
- }
1709
- else if (this.peek().typ === 'OpImpliedBy') {
1710
- this.next();
1711
- const right = this.parseTerm();
1712
- const pred = internIri(LOG_NS + 'impliedBy');
1713
- triples.push(new Triple(left, pred, right));
1714
- if (this.peek().typ === 'Dot')
1715
- this.next();
1716
- else if (this.peek().typ === 'RBrace') {
1717
- // ok
1718
- }
1719
- else {
1720
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1721
- }
1722
- }
1723
- else {
1724
- // N3 grammar allows: triples ::= subject predicateObjectList?
1725
- // So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
1726
- if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
1727
- if (this.pendingTriples.length > 0) {
1728
- triples.push(...this.pendingTriples);
1729
- this.pendingTriples = [];
1730
- }
1731
- if (this.peek().typ === 'Dot')
1732
- this.next();
1733
- continue;
1734
- }
1735
- triples.push(...this.parsePredicateObjectList(left));
1736
- if (this.peek().typ === 'Dot')
1737
- this.next();
1738
- else if (this.peek().typ === 'RBrace') {
1739
- // ok
1740
- }
1741
- else {
1742
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1743
- }
1744
- }
1737
+ else if ([...word].every((ch) => /[0-9.\-]/.test(ch))) {
1738
+ tokens.push(new Token('Literal', word, start));
1739
+ }
1740
+ else {
1741
+ tokens.push(new Token('Ident', word, start));
1745
1742
  }
1746
- this.next(); // consume '}'
1747
- return new GraphTerm(triples);
1748
1743
  }
1749
- 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;
1744
+ tokens.push(new Token('EOF', null, n));
1745
+ return tokens;
1746
+ }
1747
+ // ===========================================================================
1748
+ // PREFIX ENVIRONMENT
1749
+ // ===========================================================================
1750
+ class PrefixEnv {
1751
+ constructor(map, baseIri) {
1752
+ this.map = map || {}; // prefix -> IRI (including "" for @prefix :)
1753
+ this.baseIri = baseIri || ''; // base IRI for resolving <relative>
1805
1754
  }
1806
- parseObjectList() {
1807
- const objs = [this.parseTerm()];
1808
- while (this.peek().typ === 'Comma') {
1809
- this.next();
1810
- objs.push(this.parseTerm());
1811
- }
1812
- return objs;
1755
+ static newDefault() {
1756
+ const m = {};
1757
+ m['rdf'] = RDF_NS;
1758
+ m['rdfs'] = RDFS_NS;
1759
+ m['xsd'] = XSD_NS;
1760
+ m['log'] = LOG_NS;
1761
+ m['math'] = MATH_NS;
1762
+ m['string'] = STRING_NS;
1763
+ m['list'] = LIST_NS;
1764
+ m['time'] = TIME_NS;
1765
+ m['genid'] = SKOLEM_NS;
1766
+ m[''] = ''; // empty prefix default namespace
1767
+ return new PrefixEnv(m, ''); // base IRI starts empty
1813
1768
  }
1814
- 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;
1769
+ set(pref, base) {
1770
+ this.map[pref] = base;
1771
+ }
1772
+ setBase(baseIri) {
1773
+ this.baseIri = baseIri || '';
1774
+ }
1775
+ expandQName(q) {
1776
+ if (q.includes(':')) {
1777
+ const [p, local] = q.split(':', 2);
1778
+ const base = this.map[p] || '';
1779
+ if (base)
1780
+ return base + local;
1781
+ return q;
1823
1782
  }
1824
- let isFuse = false;
1825
- if (isForward) {
1826
- if (conclTerm instanceof Literal && conclTerm.value === 'false') {
1827
- isFuse = true;
1783
+ return q;
1784
+ }
1785
+ shrinkIri(iri) {
1786
+ let best = null; // [prefix, local]
1787
+ for (const [p, base] of Object.entries(this.map)) {
1788
+ if (!base)
1789
+ continue;
1790
+ if (iri.startsWith(base)) {
1791
+ const local = iri.slice(base.length);
1792
+ if (!local)
1793
+ continue;
1794
+ const cand = [p, local];
1795
+ if (best === null || cand[1].length < best[1].length)
1796
+ best = cand;
1828
1797
  }
1829
1798
  }
1830
- 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);
1799
+ if (best === null)
1800
+ return null;
1801
+ const [p, local] = best;
1802
+ if (p === '')
1803
+ return `:${local}`;
1804
+ return `${p}:${local}`;
1856
1805
  }
1857
- }
1858
- // ===========================================================================
1859
- // 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]}`;
1806
+ prefixesUsedForOutput(triples) {
1807
+ const used = new Set();
1808
+ for (const t of triples) {
1809
+ const iris = [];
1810
+ iris.push(...collectIrisInTerm(t.s));
1811
+ if (!isRdfTypePred(t.p)) {
1812
+ iris.push(...collectIrisInTerm(t.p));
1813
+ }
1814
+ iris.push(...collectIrisInTerm(t.o));
1815
+ for (const iri of iris) {
1816
+ for (const [p, base] of Object.entries(this.map)) {
1817
+ if (base && iri.startsWith(base))
1818
+ used.add(p);
1819
+ }
1868
1820
  }
1869
- return new Var(mapping[label]);
1870
- }
1871
- if (t instanceof ListTerm) {
1872
- return new ListTerm(t.elems.map((e) => convertTerm(e, mapping, counter)));
1873
- }
1874
- if (t instanceof OpenListTerm) {
1875
- return new OpenListTerm(t.prefix.map((e) => convertTerm(e, mapping, counter)), t.tailVar);
1876
1821
  }
1877
- 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);
1822
+ const v = [];
1823
+ for (const p of used) {
1824
+ if (this.map.hasOwnProperty(p))
1825
+ v.push([p, this.map[p]]);
1880
1826
  }
1881
- 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));
1827
+ v.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
1828
+ return v;
1885
1829
  }
1886
- const mapping = {};
1887
- const counter = [0];
1888
- const newPremise = premise.map((tr) => convertTriple(tr, mapping, counter));
1889
- return [newPremise, conclusion];
1890
1830
  }
1891
- // 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
1906
- }
1907
- if (!mapping.hasOwnProperty(label)) {
1908
- // If we have a global cache keyed by firingKey, use it to ensure
1909
- // deterministic blank IDs for the same rule+substitution instance.
1910
- if (globalMap && firingKey) {
1911
- const gk = `${firingKey}|${label}`;
1912
- let sk = globalMap.get(gk);
1913
- if (!sk) {
1914
- const idx = skCounter[0];
1915
- skCounter[0] += 1;
1916
- sk = `_:sk_${idx}`;
1917
- globalMap.set(gk, sk);
1918
- }
1919
- mapping[label] = sk;
1920
- }
1921
- else {
1922
- const idx = skCounter[0];
1923
- skCounter[0] += 1;
1924
- mapping[label] = `_:sk_${idx}`;
1925
- }
1926
- }
1927
- return new Blank(mapping[label]);
1831
+ function collectIrisInTerm(t) {
1832
+ const out = [];
1833
+ if (t instanceof Iri) {
1834
+ out.push(t.value);
1928
1835
  }
1929
- if (t instanceof ListTerm) {
1930
- return new ListTerm(t.elems.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter, firingKey, globalMap)));
1836
+ else if (t instanceof Literal) {
1837
+ const [_lex, dt] = literalParts(t.value);
1838
+ if (dt)
1839
+ out.push(dt); // so rdf/xsd prefixes are emitted when only used in ^^...
1931
1840
  }
1932
- if (t instanceof OpenListTerm) {
1933
- return new OpenListTerm(t.prefix.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter, firingKey, globalMap)), t.tailVar);
1841
+ else if (t instanceof ListTerm) {
1842
+ for (const x of t.elems)
1843
+ out.push(...collectIrisInTerm(x));
1934
1844
  }
1935
- if (t instanceof GraphTerm) {
1936
- return new GraphTerm(t.triples.map((tr) => skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap)));
1845
+ else if (t instanceof OpenListTerm) {
1846
+ for (const x of t.prefix)
1847
+ out.push(...collectIrisInTerm(x));
1937
1848
  }
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
- }
1849
+ else if (t instanceof GraphTerm) {
1850
+ for (const tr of t.triples) {
1851
+ out.push(...collectIrisInTerm(tr.s));
1852
+ out.push(...collectIrisInTerm(tr.p));
1853
+ out.push(...collectIrisInTerm(tr.o));
1973
1854
  }
1974
- return false;
1975
1855
  }
1976
- 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;
1986
- }
1987
- return true;
1856
+ return out;
1857
+ }
1858
+ function collectVarsInTerm(t, acc) {
1859
+ if (t instanceof Var) {
1860
+ acc.add(t.name);
1988
1861
  }
1989
- if (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;
1862
+ else if (t instanceof ListTerm) {
1863
+ for (const x of t.elems)
1864
+ collectVarsInTerm(x, acc);
1865
+ }
1866
+ else if (t instanceof OpenListTerm) {
1867
+ for (const x of t.prefix)
1868
+ collectVarsInTerm(x, acc);
1869
+ acc.add(t.tailVar);
1870
+ }
1871
+ else if (t instanceof GraphTerm) {
1872
+ for (const tr of t.triples) {
1873
+ collectVarsInTerm(tr.s, acc);
1874
+ collectVarsInTerm(tr.p, acc);
1875
+ collectVarsInTerm(tr.o, acc);
1997
1876
  }
1998
- return true;
1999
1877
  }
2000
- if (a instanceof GraphTerm) {
2001
- return alphaEqGraphTriples(a.triples, b.triples);
1878
+ }
1879
+ function varsInRule(rule) {
1880
+ const acc = new Set();
1881
+ for (const tr of rule.premise) {
1882
+ collectVarsInTerm(tr.s, acc);
1883
+ collectVarsInTerm(tr.p, acc);
1884
+ collectVarsInTerm(tr.o, acc);
2002
1885
  }
2003
- return false;
1886
+ for (const tr of rule.conclusion) {
1887
+ collectVarsInTerm(tr.s, acc);
1888
+ collectVarsInTerm(tr.p, acc);
1889
+ collectVarsInTerm(tr.o, acc);
1890
+ }
1891
+ return acc;
2004
1892
  }
2005
- function 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;
2042
- }
2043
- return false;
1893
+ function collectBlankLabelsInTerm(t, acc) {
1894
+ if (t instanceof Blank) {
1895
+ acc.add(t.label);
2044
1896
  }
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;
2055
- }
2056
- return true;
1897
+ else if (t instanceof ListTerm) {
1898
+ for (const x of t.elems)
1899
+ collectBlankLabelsInTerm(x, acc);
2057
1900
  }
2058
- if (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;
2066
- }
2067
- return true;
1901
+ else if (t instanceof OpenListTerm) {
1902
+ for (const x of t.prefix)
1903
+ collectBlankLabelsInTerm(x, acc);
2068
1904
  }
2069
- if (a instanceof GraphTerm) {
2070
- return alphaEqGraphTriples(a.triples, b.triples);
1905
+ else if (t instanceof GraphTerm) {
1906
+ for (const tr of t.triples) {
1907
+ collectBlankLabelsInTerm(tr.s, acc);
1908
+ collectBlankLabelsInTerm(tr.p, acc);
1909
+ collectBlankLabelsInTerm(tr.o, acc);
1910
+ }
2071
1911
  }
2072
- return false;
2073
1912
  }
2074
- function 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;
1913
+ function collectBlankLabelsInTriples(triples) {
1914
+ const acc = new Set();
1915
+ for (const tr of triples) {
1916
+ collectBlankLabelsInTerm(tr.s, acc);
1917
+ collectBlankLabelsInTerm(tr.p, acc);
1918
+ collectBlankLabelsInTerm(tr.o, acc);
2083
1919
  }
2084
- return 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;
1920
+ return acc;
2093
1921
  }
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;
1922
+ // ===========================================================================
1923
+ // PARSER
1924
+ // ===========================================================================
1925
+ class Parser {
1926
+ constructor(tokens) {
1927
+ this.toks = tokens;
1928
+ this.pos = 0;
1929
+ this.prefixes = PrefixEnv.newDefault();
1930
+ this.blankCounter = 0;
1931
+ this.pendingTriples = [];
2103
1932
  }
2104
- // Variables: renamable (ONLY inside quoted formulas)
2105
- if (a instanceof Var && b instanceof Var) {
2106
- return alphaEqVarName(a.name, b.name, vmap);
1933
+ peek() {
1934
+ return this.toks[this.pos];
2107
1935
  }
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;
1936
+ next() {
1937
+ const tok = this.toks[this.pos];
1938
+ this.pos += 1;
1939
+ return tok;
1940
+ }
1941
+ fail(message, tok = this.peek()) {
1942
+ const off = tok && typeof tok.offset === 'number' ? tok.offset : null;
1943
+ throw new N3SyntaxError(message, off);
1944
+ }
1945
+ expectDot() {
1946
+ const tok = this.next();
1947
+ if (tok.typ !== 'Dot') {
1948
+ this.fail(`Expected '.', got ${tok.toString()}`, tok);
2118
1949
  }
2119
- return true;
2120
1950
  }
2121
- 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;
1951
+ parseDocument() {
1952
+ const triples = [];
1953
+ const forwardRules = [];
1954
+ const backwardRules = [];
1955
+ while (this.peek().typ !== 'EOF') {
1956
+ if (this.peek().typ === 'AtPrefix') {
1957
+ this.next();
1958
+ this.parsePrefixDirective();
1959
+ }
1960
+ else if (this.peek().typ === 'AtBase') {
1961
+ this.next();
1962
+ this.parseBaseDirective();
1963
+ }
1964
+ else if (
1965
+ // SPARQL-style/Turtle-style directives (case-insensitive, no trailing '.')
1966
+ this.peek().typ === 'Ident' &&
1967
+ typeof this.peek().value === 'string' &&
1968
+ this.peek().value.toLowerCase() === 'prefix' &&
1969
+ this.toks[this.pos + 1] &&
1970
+ this.toks[this.pos + 1].typ === 'Ident' &&
1971
+ typeof this.toks[this.pos + 1].value === 'string' &&
1972
+ // Require PNAME_NS form (e.g., "ex:" or ":") to avoid clashing with a normal triple starting with IRI "prefix".
1973
+ this.toks[this.pos + 1].value.endsWith(':') &&
1974
+ this.toks[this.pos + 2] &&
1975
+ (this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')) {
1976
+ this.next(); // consume PREFIX keyword
1977
+ this.parseSparqlPrefixDirective();
1978
+ }
1979
+ else if (this.peek().typ === 'Ident' &&
1980
+ typeof this.peek().value === 'string' &&
1981
+ this.peek().value.toLowerCase() === 'base' &&
1982
+ this.toks[this.pos + 1] &&
1983
+ // SPARQL BASE requires an IRIREF.
1984
+ this.toks[this.pos + 1].typ === 'IriRef') {
1985
+ this.next(); // consume BASE keyword
1986
+ this.parseSparqlBaseDirective();
1987
+ }
1988
+ else {
1989
+ const first = this.parseTerm();
1990
+ if (this.peek().typ === 'OpImplies') {
1991
+ this.next();
1992
+ const second = this.parseTerm();
1993
+ this.expectDot();
1994
+ forwardRules.push(this.makeRule(first, second, true));
1995
+ }
1996
+ else if (this.peek().typ === 'OpImpliedBy') {
1997
+ this.next();
1998
+ const second = this.parseTerm();
1999
+ this.expectDot();
2000
+ backwardRules.push(this.makeRule(first, second, false));
2001
+ }
2002
+ else {
2003
+ let more;
2004
+ if (this.peek().typ === 'Dot') {
2005
+ // N3 grammar allows: triples ::= subject predicateObjectList?
2006
+ // So a bare subject followed by '.' is syntactically valid.
2007
+ // If the subject was a path / property-list that generated helper triples,
2008
+ // we emit those; otherwise this statement contributes no triples.
2009
+ more = [];
2010
+ if (this.pendingTriples.length > 0) {
2011
+ more = this.pendingTriples;
2012
+ this.pendingTriples = [];
2013
+ }
2014
+ this.next(); // consume '.'
2015
+ }
2016
+ else {
2017
+ more = this.parsePredicateObjectList(first);
2018
+ this.expectDot();
2019
+ }
2020
+ // normalize explicit log:implies / log:impliedBy at top-level
2021
+ for (const tr of more) {
2022
+ if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
2023
+ forwardRules.push(this.makeRule(tr.s, tr.o, true));
2024
+ }
2025
+ else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
2026
+ backwardRules.push(this.makeRule(tr.s, tr.o, false));
2027
+ }
2028
+ else {
2029
+ triples.push(tr);
2030
+ }
2031
+ }
2032
+ }
2033
+ }
2127
2034
  }
2128
- // tailVar is a var-name string, so treat it as renamable too
2129
- return alphaEqVarName(a.tailVar, b.tailVar, vmap);
2130
- }
2131
- // Nested formulas: compare with fresh maps (separate scope)
2132
- if (a instanceof GraphTerm && b instanceof GraphTerm) {
2133
- return alphaEqGraphTriples(a.triples, b.triples);
2035
+ return [this.prefixes, triples, forwardRules, backwardRules];
2134
2036
  }
2135
- 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))
2164
- continue;
2165
- used[j] = true;
2166
- if (step(i + 1, v2, b2))
2167
- return true;
2168
- used[j] = false;
2037
+ parsePrefixDirective() {
2038
+ const tok = this.next();
2039
+ if (tok.typ !== 'Ident') {
2040
+ this.fail(`Expected prefix name, got ${tok.toString()}`, tok);
2169
2041
  }
2170
- return false;
2042
+ const pref = tok.value || '';
2043
+ const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
2044
+ if (this.peek().typ === 'Dot') {
2045
+ this.next();
2046
+ if (!this.prefixes.map.hasOwnProperty(prefName)) {
2047
+ this.prefixes.set(prefName, '');
2048
+ }
2049
+ return;
2050
+ }
2051
+ const tok2 = this.next();
2052
+ let iri;
2053
+ if (tok2.typ === 'IriRef') {
2054
+ iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
2055
+ }
2056
+ else if (tok2.typ === 'Ident') {
2057
+ iri = this.prefixes.expandQName(tok2.value || '');
2058
+ }
2059
+ else {
2060
+ this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
2061
+ }
2062
+ this.expectDot();
2063
+ this.prefixes.set(prefName, iri);
2171
2064
  }
2172
- 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;
2065
+ parseBaseDirective() {
2066
+ const tok = this.next();
2067
+ let iri;
2068
+ if (tok.typ === 'IriRef') {
2069
+ iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
2070
+ }
2071
+ else if (tok.typ === 'Ident') {
2072
+ iri = tok.value || '';
2180
2073
  }
2181
2074
  else {
2182
- bmap[x] = y;
2183
- return true;
2075
+ this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
2184
2076
  }
2077
+ this.expectDot();
2078
+ this.prefixes.setBase(iri);
2185
2079
  }
2186
- 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;
2080
+ parseSparqlPrefixDirective() {
2081
+ // SPARQL/Turtle-style PREFIX directive: PREFIX pfx: <iri> (no trailing '.')
2082
+ const tok = this.next();
2083
+ if (tok.typ !== 'Ident') {
2084
+ this.fail(`Expected prefix name after PREFIX, got ${tok.toString()}`, tok);
2198
2085
  }
2199
- return true;
2086
+ const pref = tok.value || '';
2087
+ const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
2088
+ const tok2 = this.next();
2089
+ let iri;
2090
+ if (tok2.typ === 'IriRef') {
2091
+ iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
2092
+ }
2093
+ else if (tok2.typ === 'Ident') {
2094
+ iri = this.prefixes.expandQName(tok2.value || '');
2095
+ }
2096
+ else {
2097
+ this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
2098
+ }
2099
+ // N3/Turtle: PREFIX directives do not have a trailing '.', but accept it permissively.
2100
+ if (this.peek().typ === 'Dot')
2101
+ this.next();
2102
+ this.prefixes.set(prefName, iri);
2200
2103
  }
2201
- 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;
2104
+ parseSparqlBaseDirective() {
2105
+ // SPARQL/Turtle-style BASE directive: BASE <iri> (no trailing '.')
2106
+ const tok = this.next();
2107
+ let iri;
2108
+ if (tok.typ === 'IriRef') {
2109
+ iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
2207
2110
  }
2208
- return true;
2111
+ else if (tok.typ === 'Ident') {
2112
+ iri = tok.value || '';
2113
+ }
2114
+ else {
2115
+ this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
2116
+ }
2117
+ // N3/Turtle: BASE directives do not have a trailing '.', but accept it permissively.
2118
+ if (this.peek().typ === 'Dot')
2119
+ this.next();
2120
+ this.prefixes.setBase(iri);
2209
2121
  }
2210
- if (a instanceof GraphTerm && b instanceof GraphTerm) {
2211
- // formulas are alpha-equivalent up to var/blank renaming
2212
- return alphaEqGraphTriples(a.triples, b.triples);
2122
+ parseTerm() {
2123
+ let t = this.parsePathItem();
2124
+ while (this.peek().typ === 'OpPathFwd' || this.peek().typ === 'OpPathRev') {
2125
+ const dir = this.next().typ; // OpPathFwd | OpPathRev
2126
+ const pred = this.parsePathItem();
2127
+ this.blankCounter += 1;
2128
+ const bn = new Blank(`_:b${this.blankCounter}`);
2129
+ this.pendingTriples.push(dir === 'OpPathFwd' ? new Triple(t, pred, bn) : new Triple(bn, pred, t));
2130
+ t = bn;
2131
+ }
2132
+ return t;
2213
2133
  }
2214
- 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);
2134
+ parsePathItem() {
2135
+ const tok = this.next();
2136
+ const typ = tok.typ;
2137
+ const val = tok.value;
2138
+ if (typ === 'Equals') {
2139
+ return internIri(OWL_NS + 'sameAs');
2280
2140
  }
2281
- 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);
2141
+ if (typ === 'IriRef') {
2142
+ const base = this.prefixes.baseIri || '';
2143
+ return internIri(resolveIriRef(val || '', base));
2144
+ }
2145
+ if (typ === 'Ident') {
2146
+ const name = val || '';
2147
+ if (name === 'a') {
2148
+ return internIri(RDF_NS + 'type');
2288
2149
  }
2289
- let psb = ps.get(sk);
2290
- if (!psb) {
2291
- psb = [];
2292
- ps.set(sk, psb);
2150
+ else if (name.startsWith('_:')) {
2151
+ return new Blank(name);
2293
2152
  }
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);
2153
+ else if (name.includes(':')) {
2154
+ return internIri(this.prefixes.expandQName(name));
2302
2155
  }
2303
- let pob = po.get(ok);
2304
- if (!pob) {
2305
- pob = [];
2306
- po.set(ok, pob);
2156
+ else {
2157
+ return internIri(name);
2307
2158
  }
2308
- pob.push(tr);
2309
- }
2310
- }
2311
- const key = tripleFastKey(tr);
2312
- if (key !== null)
2313
- facts.__keySet.add(key);
2314
- }
2315
- function candidateFacts(facts, goal) {
2316
- ensureFactIndexes(facts);
2317
- if (goal.p instanceof Iri) {
2318
- const pk = goal.p.value;
2319
- const sk = termFastKey(goal.s);
2320
- const ok = termFastKey(goal.o);
2321
- /** @type {Triple[] | null} */
2322
- let byPS = null;
2323
- if (sk !== null) {
2324
- const ps = facts.__byPS.get(pk);
2325
- if (ps)
2326
- byPS = ps.get(sk) || null;
2327
2159
  }
2328
- /** @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;
2334
- }
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) || [];
2342
- }
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));
2160
+ if (typ === 'Literal') {
2161
+ let s = val || '';
2162
+ // Optional language tag: "..."@en, per N3 LANGTAG production.
2163
+ if (this.peek().typ === 'LangTag') {
2164
+ // Only quoted string literals can carry a language tag.
2165
+ if (!(s.startsWith('"') && s.endsWith('"'))) {
2166
+ this.fail('Language tag is only allowed on quoted string literals', this.peek());
2167
+ }
2168
+ const langTok = this.next();
2169
+ const lang = langTok.value || '';
2170
+ s = `${s}@${lang}`;
2171
+ // N3/Turtle: language tags and datatypes are mutually exclusive.
2172
+ if (this.peek().typ === 'HatHat') {
2173
+ this.fail('A literal cannot have both a language tag (@...) and a datatype (^^...)', this.peek());
2174
+ }
2175
+ }
2176
+ if (this.peek().typ === 'HatHat') {
2177
+ this.next();
2178
+ const dtTok = this.next();
2179
+ let dtIri;
2180
+ if (dtTok.typ === 'IriRef') {
2181
+ dtIri = dtTok.value || '';
2182
+ }
2183
+ else if (dtTok.typ === 'Ident') {
2184
+ const qn = dtTok.value || '';
2185
+ if (qn.includes(':'))
2186
+ dtIri = this.prefixes.expandQName(qn);
2187
+ else
2188
+ dtIri = qn;
2189
+ }
2190
+ else {
2191
+ this.fail(`Expected datatype after ^^, got ${dtTok.toString()}`, dtTok);
2192
+ }
2193
+ s = `${s}^^<${dtIri}>`;
2362
2194
  }
2195
+ return internLiteral(s);
2363
2196
  }
2364
- const pb = facts.__byPred.get(pk) || [];
2365
- return pb.some((t) => triplesEqual(t, tr));
2197
+ if (typ === 'Var')
2198
+ return new Var(val || '');
2199
+ if (typ === 'LParen')
2200
+ return this.parseList();
2201
+ if (typ === 'LBracket')
2202
+ return this.parseBlank();
2203
+ if (typ === 'LBrace')
2204
+ return this.parseGraph();
2205
+ this.fail(`Unexpected term token: ${tok.toString()}`, tok);
2366
2206
  }
2367
- // 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);
2207
+ parseList() {
2208
+ const elems = [];
2209
+ while (this.peek().typ !== 'RParen') {
2210
+ elems.push(this.parseTerm());
2401
2211
  }
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);
2481
- }
2482
- return normal.concat(delayed);
2483
- }
2484
- // @ts-nocheck
2485
- /* eslint-disable */
2486
- // ===========================================================================
2487
- // Unification + substitution
2488
- // ===========================================================================
2489
- function containsVarTerm(t, v) {
2490
- if (t instanceof Var)
2491
- return t.name === v;
2492
- if (t instanceof ListTerm)
2493
- return t.elems.some((e) => containsVarTerm(e, v));
2494
- if (t instanceof OpenListTerm)
2495
- return t.prefix.some((e) => containsVarTerm(e, v)) || t.tailVar === v;
2496
- if (t instanceof GraphTerm)
2497
- return t.triples.some((tr) => containsVarTerm(tr.s, v) || containsVarTerm(tr.p, v) || containsVarTerm(tr.o, v));
2498
- return false;
2499
- }
2500
- function isGroundTermInGraph(t) {
2501
- // variables inside graph terms are treated as local placeholders,
2502
- // so they don't make the *surrounding triple* non-ground.
2503
- if (t instanceof OpenListTerm)
2504
- return false;
2505
- if (t instanceof ListTerm)
2506
- return t.elems.every((e) => isGroundTermInGraph(e));
2507
- if (t instanceof GraphTerm)
2508
- return t.triples.every((tr) => isGroundTripleInGraph(tr));
2509
- // Iri/Literal/Blank/Var are all OK inside formulas
2510
- return true;
2511
- }
2512
- function isGroundTripleInGraph(tr) {
2513
- return isGroundTermInGraph(tr.s) && isGroundTermInGraph(tr.p) && isGroundTermInGraph(tr.o);
2514
- }
2515
- function isGroundTerm(t) {
2516
- if (t instanceof Var)
2517
- return false;
2518
- if (t instanceof ListTerm)
2519
- return t.elems.every((e) => isGroundTerm(e));
2520
- if (t instanceof OpenListTerm)
2521
- return false;
2522
- if (t instanceof GraphTerm)
2523
- return t.triples.every((tr) => isGroundTripleInGraph(tr));
2524
- return true;
2525
- }
2526
- function isGroundTriple(tr) {
2527
- return isGroundTerm(tr.s) && isGroundTerm(tr.p) && isGroundTerm(tr.o);
2528
- }
2529
- // Canonical JSON-ish encoding for use as a Skolem cache key.
2530
- // We only *call* this on ground terms in log:skolem, but it is
2531
- // robust to seeing vars/open lists anyway.
2532
- function skolemKeyFromTerm(t) {
2533
- function enc(u) {
2534
- if (u instanceof Iri)
2535
- return ['I', u.value];
2536
- if (u instanceof Literal)
2537
- return ['L', u.value];
2538
- if (u instanceof Blank)
2539
- return ['B', u.label];
2540
- if (u instanceof Var)
2541
- return ['V', u.name];
2542
- if (u instanceof ListTerm)
2543
- return ['List', u.elems.map(enc)];
2544
- if (u instanceof OpenListTerm)
2545
- return ['OpenList', u.prefix.map(enc), u.tailVar];
2546
- if (u instanceof GraphTerm)
2547
- return ['Graph', u.triples.map((tr) => [enc(tr.s), enc(tr.p), enc(tr.o)])];
2548
- return ['Other', String(u)];
2212
+ this.next(); // consume ')'
2213
+ return new ListTerm(elems);
2549
2214
  }
2550
- return JSON.stringify(enc(t));
2551
- }
2552
- function applySubstTerm(t, s) {
2553
- // Common case: variable
2554
- if (t instanceof Var) {
2555
- // Fast path: unbound variable → no change
2556
- const first = s[t.name];
2557
- if (first === undefined) {
2558
- return t;
2215
+ parseBlank() {
2216
+ // [] or [ ... ] property list
2217
+ if (this.peek().typ === 'RBracket') {
2218
+ this.next();
2219
+ this.blankCounter += 1;
2220
+ return new Blank(`_:b${this.blankCounter}`);
2559
2221
  }
2560
- // Follow chains X -> Y -> ... until we hit a non-var or a cycle.
2561
- let cur = first;
2562
- const seen = new Set([t.name]);
2563
- while (cur instanceof Var) {
2564
- const name = cur.name;
2565
- if (seen.has(name))
2566
- break; // cycle
2567
- seen.add(name);
2568
- const nxt = s[name];
2569
- if (!nxt)
2222
+ // IRI property list: [ id <IRI> predicateObjectList? ]
2223
+ // Lets you embed descriptions of an IRI directly in object position.
2224
+ if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'id') {
2225
+ const iriTok = this.next(); // consume 'id'
2226
+ const iriTerm = this.parseTerm();
2227
+ // N3 note: 'id' form is not meant to be used with blank node identifiers.
2228
+ if (iriTerm instanceof Blank && iriTerm.label.startsWith('_:')) {
2229
+ this.fail("Cannot use 'id' keyword with a blank node identifier inside [...]", iriTok);
2230
+ }
2231
+ // Optional ';' right after the id IRI (tolerated).
2232
+ if (this.peek().typ === 'Semicolon')
2233
+ this.next();
2234
+ // Empty IRI property list: [ id :iri ]
2235
+ if (this.peek().typ === 'RBracket') {
2236
+ this.next();
2237
+ return iriTerm;
2238
+ }
2239
+ const subj = iriTerm;
2240
+ while (true) {
2241
+ let pred;
2242
+ let invert = false;
2243
+ if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
2244
+ this.next();
2245
+ pred = internIri(RDF_NS + 'type');
2246
+ }
2247
+ else if (this.peek().typ === 'OpPredInvert') {
2248
+ this.next(); // "<-"
2249
+ pred = this.parseTerm();
2250
+ invert = true;
2251
+ }
2252
+ else {
2253
+ pred = this.parseTerm();
2254
+ }
2255
+ const objs = [this.parseTerm()];
2256
+ while (this.peek().typ === 'Comma') {
2257
+ this.next();
2258
+ objs.push(this.parseTerm());
2259
+ }
2260
+ for (const o of objs) {
2261
+ this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
2262
+ }
2263
+ if (this.peek().typ === 'Semicolon') {
2264
+ this.next();
2265
+ if (this.peek().typ === 'RBracket')
2266
+ break;
2267
+ continue;
2268
+ }
2570
2269
  break;
2571
- cur = nxt;
2572
- }
2573
- if (cur instanceof Var) {
2574
- // Still a var: keep it as is (no need to clone)
2575
- return cur;
2270
+ }
2271
+ if (this.peek().typ !== 'RBracket') {
2272
+ this.fail(`Expected ']' at end of IRI property list, got ${this.peek().toString()}`);
2273
+ }
2274
+ this.next();
2275
+ return iriTerm;
2576
2276
  }
2577
- // Bound to a non-var term: apply substitution recursively in case it
2578
- // contains variables inside.
2579
- return applySubstTerm(cur, s);
2580
- }
2581
- // Non-variable terms
2582
- if (t instanceof ListTerm) {
2583
- return new ListTerm(t.elems.map((e) => applySubstTerm(e, s)));
2584
- }
2585
- if (t instanceof OpenListTerm) {
2586
- const newPrefix = t.prefix.map((e) => applySubstTerm(e, s));
2587
- const tailTerm = s[t.tailVar];
2588
- if (tailTerm !== undefined) {
2589
- const tailApplied = applySubstTerm(tailTerm, s);
2590
- if (tailApplied instanceof ListTerm) {
2591
- return new ListTerm(newPrefix.concat(tailApplied.elems));
2277
+ // [ predicateObjectList ]
2278
+ this.blankCounter += 1;
2279
+ const id = `_:b${this.blankCounter}`;
2280
+ const subj = new Blank(id);
2281
+ while (true) {
2282
+ // Verb (can also be 'a')
2283
+ let pred;
2284
+ let invert = false;
2285
+ if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
2286
+ this.next();
2287
+ pred = internIri(RDF_NS + 'type');
2592
2288
  }
2593
- else if (tailApplied instanceof OpenListTerm) {
2594
- return new OpenListTerm(newPrefix.concat(tailApplied.prefix), tailApplied.tailVar);
2289
+ else if (this.peek().typ === 'OpPredInvert') {
2290
+ this.next(); // consume "<-"
2291
+ pred = this.parseTerm();
2292
+ invert = true;
2595
2293
  }
2596
2294
  else {
2597
- return new OpenListTerm(newPrefix, t.tailVar);
2295
+ pred = this.parseTerm();
2296
+ }
2297
+ // Object list: o1, o2, ...
2298
+ const objs = [this.parseTerm()];
2299
+ while (this.peek().typ === 'Comma') {
2300
+ this.next();
2301
+ objs.push(this.parseTerm());
2302
+ }
2303
+ for (const o of objs) {
2304
+ this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
2598
2305
  }
2306
+ if (this.peek().typ === 'Semicolon') {
2307
+ this.next();
2308
+ if (this.peek().typ === 'RBracket')
2309
+ break;
2310
+ continue;
2311
+ }
2312
+ break;
2599
2313
  }
2600
- else {
2601
- return new OpenListTerm(newPrefix, t.tailVar);
2314
+ if (this.peek().typ === 'RBracket') {
2315
+ this.next();
2602
2316
  }
2603
- }
2604
- if (t instanceof GraphTerm) {
2605
- return new GraphTerm(t.triples.map((tr) => applySubstTriple(tr, s)));
2606
- }
2607
- return t;
2608
- }
2609
- function applySubstTriple(tr, s) {
2610
- return new Triple(applySubstTerm(tr.s, s), applySubstTerm(tr.p, s), applySubstTerm(tr.o, s));
2611
- }
2612
- function iriValue(t) {
2613
- return t instanceof Iri ? t.value : null;
2614
- }
2615
- function unifyOpenWithList(prefix, tailv, ys, subst) {
2616
- if (ys.length < prefix.length)
2617
- return null;
2618
- let s2 = { ...subst };
2619
- for (let i = 0; i < prefix.length; i++) {
2620
- s2 = unifyTerm(prefix[i], ys[i], s2);
2621
- if (s2 === null)
2622
- return null;
2623
- }
2624
- const rest = new ListTerm(ys.slice(prefix.length));
2625
- s2 = unifyTerm(new Var(tailv), rest, s2);
2626
- if (s2 === null)
2627
- return null;
2628
- return s2;
2629
- }
2630
- function unifyGraphTriples(xs, ys, subst) {
2631
- if (xs.length !== ys.length)
2632
- return null;
2633
- // Fast path: exact same sequence.
2634
- if (triplesListEqual(xs, ys))
2635
- return { ...subst };
2636
- // Backtracking match (order-insensitive), *threading* the substitution through.
2637
- const used = new Array(ys.length).fill(false);
2638
- function step(i, s) {
2639
- if (i >= xs.length)
2640
- return s;
2641
- const x = xs[i];
2642
- for (let j = 0; j < ys.length; j++) {
2643
- if (used[j])
2644
- continue;
2645
- const y = ys[j];
2646
- // Cheap pruning when both predicates are IRIs.
2647
- if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
2648
- continue;
2649
- const s2 = unifyTriple(x, y, s); // IMPORTANT: use `s`, not {}
2650
- if (s2 === null)
2651
- continue;
2652
- used[j] = true;
2653
- const s3 = step(i + 1, s2);
2654
- if (s3 !== null)
2655
- return s3;
2656
- used[j] = false;
2317
+ else {
2318
+ this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
2657
2319
  }
2658
- return null;
2659
- }
2660
- return step(0, { ...subst }); // IMPORTANT: start from the incoming subst
2661
- }
2662
- function unifyTerm(a, b, subst) {
2663
- return unifyTermWithOptions(a, b, subst, {
2664
- boolValueEq: true,
2665
- intDecimalEq: false,
2666
- });
2667
- }
2668
- function unifyTermListAppend(a, b, subst) {
2669
- // Keep list:append behavior: allow integer<->decimal exact equality,
2670
- // but do NOT add boolean-value equivalence (preserves current semantics).
2671
- return unifyTermWithOptions(a, b, subst, {
2672
- boolValueEq: false,
2673
- intDecimalEq: true,
2674
- });
2675
- }
2676
- function unifyTermWithOptions(a, b, subst, opts) {
2677
- a = applySubstTerm(a, subst);
2678
- b = applySubstTerm(b, subst);
2679
- // Variable binding
2680
- if (a instanceof Var) {
2681
- const v = a.name;
2682
- const t = b;
2683
- if (t instanceof Var && t.name === v)
2684
- return { ...subst };
2685
- if (containsVarTerm(t, v))
2686
- return null;
2687
- const s2 = { ...subst };
2688
- s2[v] = t;
2689
- return s2;
2690
- }
2691
- if (b instanceof Var) {
2692
- return unifyTermWithOptions(b, a, subst, opts);
2693
- }
2694
- // Exact matches
2695
- if (a instanceof Iri && b instanceof Iri && a.value === b.value)
2696
- return { ...subst };
2697
- if (a instanceof Literal && b instanceof Literal && a.value === b.value)
2698
- return { ...subst };
2699
- if (a instanceof Blank && b instanceof Blank && a.label === b.label)
2700
- return { ...subst };
2701
- // Plain string vs xsd:string equivalence
2702
- if (a instanceof Literal && b instanceof Literal) {
2703
- if (literalsEquivalentAsXsdString(a.value, b.value))
2704
- return { ...subst };
2705
- }
2706
- // Boolean-value equivalence (ONLY for normal unifyTerm)
2707
- if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
2708
- const ai = parseBooleanLiteralInfo(a);
2709
- const bi = parseBooleanLiteralInfo(b);
2710
- if (ai && bi && ai.value === bi.value)
2711
- return { ...subst };
2320
+ return new Blank(id);
2712
2321
  }
2713
- // Numeric-value match:
2714
- // - always allow equality when datatype matches (existing behavior)
2715
- // - optionally allow integer<->decimal exact equality (list:append only)
2716
- if (a instanceof Literal && b instanceof Literal) {
2717
- const ai = parseNumericLiteralInfo(a);
2718
- const bi = parseNumericLiteralInfo(b);
2719
- if (ai && bi) {
2720
- if (ai.dt === bi.dt) {
2721
- if (ai.kind === 'bigint' && bi.kind === 'bigint') {
2722
- if (ai.value === bi.value)
2723
- return { ...subst };
2322
+ parseGraph() {
2323
+ const triples = [];
2324
+ while (this.peek().typ !== 'RBrace') {
2325
+ const left = this.parseTerm();
2326
+ if (this.peek().typ === 'OpImplies') {
2327
+ this.next();
2328
+ const right = this.parseTerm();
2329
+ const pred = internIri(LOG_NS + 'implies');
2330
+ triples.push(new Triple(left, pred, right));
2331
+ if (this.peek().typ === 'Dot')
2332
+ this.next();
2333
+ else if (this.peek().typ === 'RBrace') {
2334
+ // ok
2724
2335
  }
2725
2336
  else {
2726
- const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
2727
- const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
2728
- if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn)
2729
- return { ...subst };
2337
+ this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
2338
+ }
2339
+ }
2340
+ else if (this.peek().typ === 'OpImpliedBy') {
2341
+ this.next();
2342
+ const right = this.parseTerm();
2343
+ const pred = internIri(LOG_NS + 'impliedBy');
2344
+ triples.push(new Triple(left, pred, right));
2345
+ if (this.peek().typ === 'Dot')
2346
+ this.next();
2347
+ else if (this.peek().typ === 'RBrace') {
2348
+ // ok
2349
+ }
2350
+ else {
2351
+ this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
2352
+ }
2353
+ }
2354
+ else {
2355
+ // N3 grammar allows: triples ::= subject predicateObjectList?
2356
+ // So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
2357
+ if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
2358
+ if (this.pendingTriples.length > 0) {
2359
+ triples.push(...this.pendingTriples);
2360
+ this.pendingTriples = [];
2361
+ }
2362
+ if (this.peek().typ === 'Dot')
2363
+ this.next();
2364
+ continue;
2365
+ }
2366
+ triples.push(...this.parsePredicateObjectList(left));
2367
+ if (this.peek().typ === 'Dot')
2368
+ this.next();
2369
+ else if (this.peek().typ === 'RBrace') {
2370
+ // ok
2730
2371
  }
2372
+ else {
2373
+ this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
2374
+ }
2375
+ }
2376
+ }
2377
+ this.next(); // consume '}'
2378
+ return new GraphTerm(triples);
2379
+ }
2380
+ parsePredicateObjectList(subject) {
2381
+ const out = [];
2382
+ // If the SUBJECT was a path, emit its helper triples first
2383
+ if (this.pendingTriples.length > 0) {
2384
+ out.push(...this.pendingTriples);
2385
+ this.pendingTriples = [];
2386
+ }
2387
+ while (true) {
2388
+ let verb;
2389
+ let invert = false;
2390
+ if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
2391
+ this.next();
2392
+ verb = internIri(RDF_NS + 'type');
2393
+ }
2394
+ else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
2395
+ // N3 syntactic sugar: "S has P O." means "S P O."
2396
+ this.next(); // consume "has"
2397
+ verb = this.parseTerm();
2731
2398
  }
2732
- if (opts.intDecimalEq) {
2733
- const intDt = XSD_NS + 'integer';
2734
- const decDt = XSD_NS + 'decimal';
2735
- if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
2736
- const intInfo = ai.dt === intDt ? ai : bi; // bigint
2737
- const decInfo = ai.dt === decDt ? ai : bi; // number + lexStr
2738
- const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
2739
- if (dec) {
2740
- const scaledInt = intInfo.value * pow10n(dec.scale);
2741
- if (scaledInt === dec.num)
2742
- return { ...subst };
2743
- }
2399
+ else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
2400
+ // N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
2401
+ this.next(); // consume "is"
2402
+ verb = this.parseTerm();
2403
+ if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
2404
+ this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
2744
2405
  }
2406
+ this.next(); // consume "of"
2407
+ invert = true;
2408
+ }
2409
+ else if (this.peek().typ === 'OpPredInvert') {
2410
+ this.next(); // "<-"
2411
+ verb = this.parseTerm();
2412
+ invert = true;
2413
+ }
2414
+ else {
2415
+ verb = this.parseTerm();
2416
+ }
2417
+ const objects = this.parseObjectList();
2418
+ // If VERB or OBJECTS contained paths, their helper triples must come
2419
+ // before the triples that consume the path results (Easter depends on this).
2420
+ if (this.pendingTriples.length > 0) {
2421
+ out.push(...this.pendingTriples);
2422
+ this.pendingTriples = [];
2423
+ }
2424
+ for (const o of objects) {
2425
+ out.push(new Triple(invert ? o : subject, verb, invert ? subject : o));
2745
2426
  }
2427
+ if (this.peek().typ === 'Semicolon') {
2428
+ this.next();
2429
+ if (this.peek().typ === 'Dot')
2430
+ break;
2431
+ continue;
2432
+ }
2433
+ break;
2746
2434
  }
2435
+ return out;
2747
2436
  }
2748
- // Open list vs concrete list
2749
- if (a instanceof OpenListTerm && b instanceof ListTerm) {
2750
- return unifyOpenWithList(a.prefix, a.tailVar, b.elems, subst);
2751
- }
2752
- if (a instanceof ListTerm && b instanceof OpenListTerm) {
2753
- return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
2754
- }
2755
- // Open list vs open list
2756
- if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
2757
- if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
2758
- return null;
2759
- let s2 = { ...subst };
2760
- for (let i = 0; i < a.prefix.length; i++) {
2761
- s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
2762
- if (s2 === null)
2763
- return null;
2437
+ parseObjectList() {
2438
+ const objs = [this.parseTerm()];
2439
+ while (this.peek().typ === 'Comma') {
2440
+ this.next();
2441
+ objs.push(this.parseTerm());
2764
2442
  }
2765
- return s2;
2443
+ return objs;
2766
2444
  }
2767
- // List terms
2768
- if (a instanceof ListTerm && b instanceof ListTerm) {
2769
- if (a.elems.length !== b.elems.length)
2770
- return null;
2771
- let s2 = { ...subst };
2772
- for (let i = 0; i < a.elems.length; i++) {
2773
- s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
2774
- if (s2 === null)
2775
- return null;
2445
+ makeRule(left, right, isForward) {
2446
+ let premiseTerm, conclTerm;
2447
+ if (isForward) {
2448
+ premiseTerm = left;
2449
+ conclTerm = right;
2776
2450
  }
2777
- return s2;
2778
- }
2779
- // Graphs
2780
- if (a instanceof GraphTerm && b instanceof GraphTerm) {
2781
- if (alphaEqGraphTriples(a.triples, b.triples))
2782
- return { ...subst };
2783
- return unifyGraphTriples(a.triples, b.triples, subst);
2784
- }
2785
- return null;
2786
- }
2787
- function unifyTriple(pat, fact, subst) {
2788
- // Predicates are usually the cheapest and most selective
2789
- const s1 = unifyTerm(pat.p, fact.p, subst);
2790
- if (s1 === null)
2791
- return null;
2792
- const s2 = unifyTerm(pat.s, fact.s, s1);
2793
- if (s2 === null)
2794
- return null;
2795
- const s3 = unifyTerm(pat.o, fact.o, s2);
2796
- return s3;
2797
- }
2798
- function composeSubst(outer, delta) {
2799
- if (!delta || Object.keys(delta).length === 0) {
2800
- return { ...outer };
2801
- }
2802
- const out = { ...outer };
2803
- for (const [k, v] of Object.entries(delta)) {
2804
- if (out.hasOwnProperty(k)) {
2805
- if (!termsEqual(out[k], v))
2806
- return null;
2451
+ else {
2452
+ premiseTerm = right;
2453
+ conclTerm = left;
2454
+ }
2455
+ let isFuse = false;
2456
+ if (isForward) {
2457
+ if (conclTerm instanceof Literal && conclTerm.value === 'false') {
2458
+ isFuse = true;
2459
+ }
2460
+ }
2461
+ let rawPremise;
2462
+ if (premiseTerm instanceof GraphTerm) {
2463
+ rawPremise = premiseTerm.triples;
2464
+ }
2465
+ else if (premiseTerm instanceof Literal && premiseTerm.value === 'true') {
2466
+ rawPremise = [];
2807
2467
  }
2808
2468
  else {
2809
- out[k] = v;
2469
+ rawPremise = [];
2470
+ }
2471
+ let rawConclusion;
2472
+ if (conclTerm instanceof GraphTerm) {
2473
+ rawConclusion = conclTerm.triples;
2474
+ }
2475
+ else if (conclTerm instanceof Literal && conclTerm.value === 'false') {
2476
+ rawConclusion = [];
2810
2477
  }
2478
+ else {
2479
+ rawConclusion = [];
2480
+ }
2481
+ // Blank nodes that occur explicitly in the head (conclusion)
2482
+ const headBlankLabels = collectBlankLabelsInTriples(rawConclusion);
2483
+ const [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
2484
+ // Reorder constraints for *forward* rules.
2485
+ const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
2486
+ return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
2811
2487
  }
2812
- return out;
2813
2488
  }
2489
+ // @ts-nocheck
2490
+ /* eslint-disable */
2814
2491
  // ===========================================================================
2815
2492
  // BUILTINS
2816
2493
  // ===========================================================================
@@ -5916,20 +5593,119 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
5916
5593
  if (pv === STRING_NS + 'format') {
5917
5594
  if (!(g.s instanceof ListTerm) || g.s.elems.length < 1)
5918
5595
  return [];
5919
- const fmtStr = termToJsString(g.s.elems[0]);
5920
- if (fmtStr === null)
5596
+ const fmtStr = termToJsString(g.s.elems[0]);
5597
+ if (fmtStr === null)
5598
+ return [];
5599
+ const args = [];
5600
+ for (let i = 1; i < g.s.elems.length; i++) {
5601
+ const aStr = termToJsString(g.s.elems[i]);
5602
+ if (aStr === null)
5603
+ return [];
5604
+ args.push(aStr);
5605
+ }
5606
+ const formatted = simpleStringFormat(fmtStr, args);
5607
+ if (formatted === null)
5608
+ return []; // unsupported format specifier(s)
5609
+ const lit = makeStringLiteral(formatted);
5610
+ if (g.o instanceof Var) {
5611
+ const s2 = { ...subst };
5612
+ s2[g.o.name] = lit;
5613
+ return [s2];
5614
+ }
5615
+ const s2 = unifyTerm(g.o, lit, subst);
5616
+ return s2 !== null ? [s2] : [];
5617
+ }
5618
+ // string:jsonPointer
5619
+ // Schema: ( $jsonText $pointer ) string:jsonPointer $value
5620
+ if (pv === STRING_NS + 'jsonPointer') {
5621
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2)
5622
+ return [];
5623
+ const jsonText = termToJsonText(g.s.elems[0]);
5624
+ const ptr = termToJsStringDecoded(g.s.elems[1]);
5625
+ if (jsonText === null || ptr === null)
5626
+ return [];
5627
+ const valTerm = jsonPointerLookup(jsonText, ptr);
5628
+ if (valTerm === null)
5629
+ return [];
5630
+ const s2 = unifyTerm(g.o, valTerm, subst);
5631
+ return s2 !== null ? [s2] : [];
5632
+ }
5633
+ // string:greaterThan
5634
+ if (pv === STRING_NS + 'greaterThan') {
5635
+ const sStr = termToJsString(g.s);
5636
+ const oStr = termToJsString(g.o);
5637
+ if (sStr === null || oStr === null)
5638
+ return [];
5639
+ return sStr > oStr ? [{ ...subst }] : [];
5640
+ }
5641
+ // string:lessThan
5642
+ if (pv === STRING_NS + 'lessThan') {
5643
+ const sStr = termToJsString(g.s);
5644
+ const oStr = termToJsString(g.o);
5645
+ if (sStr === null || oStr === null)
5646
+ return [];
5647
+ return sStr < oStr ? [{ ...subst }] : [];
5648
+ }
5649
+ // string:matches
5650
+ if (pv === STRING_NS + 'matches') {
5651
+ const sStr = termToJsString(g.s);
5652
+ const pattern = termToJsString(g.o);
5653
+ if (sStr === null || pattern === null)
5654
+ return [];
5655
+ const re = compileSwapRegex(pattern, '');
5656
+ if (!re)
5657
+ return [];
5658
+ return re.test(sStr) ? [{ ...subst }] : [];
5659
+ }
5660
+ // string:notEqualIgnoringCase
5661
+ if (pv === STRING_NS + 'notEqualIgnoringCase') {
5662
+ const sStr = termToJsString(g.s);
5663
+ const oStr = termToJsString(g.o);
5664
+ if (sStr === null || oStr === null)
5665
+ return [];
5666
+ return sStr.toLowerCase() !== oStr.toLowerCase() ? [{ ...subst }] : [];
5667
+ }
5668
+ // string:notGreaterThan (≤ in Unicode code order)
5669
+ if (pv === STRING_NS + 'notGreaterThan') {
5670
+ const sStr = termToJsString(g.s);
5671
+ const oStr = termToJsString(g.o);
5672
+ if (sStr === null || oStr === null)
5673
+ return [];
5674
+ return sStr <= oStr ? [{ ...subst }] : [];
5675
+ }
5676
+ // string:notLessThan (≥ in Unicode code order)
5677
+ if (pv === STRING_NS + 'notLessThan') {
5678
+ const sStr = termToJsString(g.s);
5679
+ const oStr = termToJsString(g.o);
5680
+ if (sStr === null || oStr === null)
5681
+ return [];
5682
+ return sStr >= oStr ? [{ ...subst }] : [];
5683
+ }
5684
+ // string:notMatches
5685
+ if (pv === STRING_NS + 'notMatches') {
5686
+ const sStr = termToJsString(g.s);
5687
+ const pattern = termToJsString(g.o);
5688
+ if (sStr === null || pattern === null)
5689
+ return [];
5690
+ const re = compileSwapRegex(pattern, '');
5691
+ if (!re)
5692
+ return [];
5693
+ return re.test(sStr) ? [] : [{ ...subst }];
5694
+ }
5695
+ // string:replace
5696
+ if (pv === STRING_NS + 'replace') {
5697
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3)
5698
+ return [];
5699
+ const dataStr = termToJsString(g.s.elems[0]);
5700
+ const searchStr = termToJsString(g.s.elems[1]);
5701
+ const replStr = termToJsString(g.s.elems[2]);
5702
+ if (dataStr === null || searchStr === null || replStr === null)
5703
+ return [];
5704
+ const re = compileSwapRegex(searchStr, 'g');
5705
+ if (!re)
5921
5706
  return [];
5922
- const args = [];
5923
- for (let i = 1; i < g.s.elems.length; i++) {
5924
- const aStr = termToJsString(g.s.elems[i]);
5925
- if (aStr === null)
5926
- return [];
5927
- args.push(aStr);
5928
- }
5929
- const formatted = simpleStringFormat(fmtStr, args);
5930
- if (formatted === null)
5931
- return []; // unsupported format specifier(s)
5932
- const lit = makeStringLiteral(formatted);
5707
+ const outStr = dataStr.replace(re, replStr);
5708
+ const lit = makeStringLiteral(outStr);
5933
5709
  if (g.o instanceof Var) {
5934
5710
  const s2 = { ...subst };
5935
5711
  s2[g.o.name] = lit;
@@ -5938,161 +5714,392 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
5938
5714
  const s2 = unifyTerm(g.o, lit, subst);
5939
5715
  return s2 !== null ? [s2] : [];
5940
5716
  }
5941
- // string:jsonPointer
5942
- // Schema: ( $jsonText $pointer ) string:jsonPointer $value
5943
- if (pv === STRING_NS + 'jsonPointer') {
5717
+ // string:scrape
5718
+ if (pv === STRING_NS + 'scrape') {
5944
5719
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2)
5945
5720
  return [];
5946
- const jsonText = termToJsonText(g.s.elems[0]);
5947
- const ptr = termToJsStringDecoded(g.s.elems[1]);
5948
- if (jsonText === null || ptr === null)
5721
+ const dataStr = termToJsString(g.s.elems[0]);
5722
+ const pattern = termToJsString(g.s.elems[1]);
5723
+ if (dataStr === null || pattern === null)
5949
5724
  return [];
5950
- const valTerm = jsonPointerLookup(jsonText, ptr);
5951
- if (valTerm === null)
5725
+ const re = compileSwapRegex(pattern, '');
5726
+ if (!re)
5952
5727
  return [];
5953
- const s2 = unifyTerm(g.o, valTerm, subst);
5728
+ const m = re.exec(dataStr);
5729
+ // Spec says “exactly 1 group”; we just use the first capturing group if present.
5730
+ if (!m || m.length < 2)
5731
+ return [];
5732
+ const group = m[1];
5733
+ const lit = makeStringLiteral(group);
5734
+ if (g.o instanceof Var) {
5735
+ const s2 = { ...subst };
5736
+ s2[g.o.name] = lit;
5737
+ return [s2];
5738
+ }
5739
+ const s2 = unifyTerm(g.o, lit, subst);
5954
5740
  return s2 !== null ? [s2] : [];
5955
5741
  }
5956
- // string:greaterThan
5957
- if (pv === STRING_NS + 'greaterThan') {
5742
+ // string:startsWith
5743
+ if (pv === STRING_NS + 'startsWith') {
5958
5744
  const sStr = termToJsString(g.s);
5959
5745
  const oStr = termToJsString(g.o);
5960
5746
  if (sStr === null || oStr === null)
5961
5747
  return [];
5962
- return sStr > oStr ? [{ ...subst }] : [];
5748
+ return sStr.startsWith(oStr) ? [{ ...subst }] : [];
5749
+ }
5750
+ // Unknown builtin
5751
+ return [];
5752
+ }
5753
+ function isBuiltinPred(p) {
5754
+ if (!(p instanceof Iri))
5755
+ return false;
5756
+ const v = p.value;
5757
+ // Super restricted mode: only treat => / <= as builtins.
5758
+ // Everything else should be handled as ordinary predicates (and thus must be
5759
+ // provided explicitly as facts/rules, without builtin evaluation).
5760
+ if (superRestrictedMode) {
5761
+ return v === LOG_NS + 'implies' || v === LOG_NS + 'impliedBy';
5762
+ }
5763
+ // Treat RDF Collections as list-term builtins too.
5764
+ if (v === RDF_NS + 'first' || v === RDF_NS + 'rest') {
5765
+ return true;
5766
+ }
5767
+ return (v.startsWith(CRYPTO_NS) ||
5768
+ v.startsWith(MATH_NS) ||
5769
+ v.startsWith(LOG_NS) ||
5770
+ v.startsWith(STRING_NS) ||
5771
+ v.startsWith(TIME_NS) ||
5772
+ v.startsWith(LIST_NS));
5773
+ }
5774
+ // @ts-nocheck
5775
+ /* eslint-disable */
5776
+ // ===========================================================================
5777
+ // Unification + substitution
5778
+ // ===========================================================================
5779
+ function containsVarTerm(t, v) {
5780
+ if (t instanceof Var)
5781
+ return t.name === v;
5782
+ if (t instanceof ListTerm)
5783
+ return t.elems.some((e) => containsVarTerm(e, v));
5784
+ if (t instanceof OpenListTerm)
5785
+ return t.prefix.some((e) => containsVarTerm(e, v)) || t.tailVar === v;
5786
+ if (t instanceof GraphTerm)
5787
+ return t.triples.some((tr) => containsVarTerm(tr.s, v) || containsVarTerm(tr.p, v) || containsVarTerm(tr.o, v));
5788
+ return false;
5789
+ }
5790
+ function isGroundTermInGraph(t) {
5791
+ // variables inside graph terms are treated as local placeholders,
5792
+ // so they don't make the *surrounding triple* non-ground.
5793
+ if (t instanceof OpenListTerm)
5794
+ return false;
5795
+ if (t instanceof ListTerm)
5796
+ return t.elems.every((e) => isGroundTermInGraph(e));
5797
+ if (t instanceof GraphTerm)
5798
+ return t.triples.every((tr) => isGroundTripleInGraph(tr));
5799
+ // Iri/Literal/Blank/Var are all OK inside formulas
5800
+ return true;
5801
+ }
5802
+ function isGroundTripleInGraph(tr) {
5803
+ return isGroundTermInGraph(tr.s) && isGroundTermInGraph(tr.p) && isGroundTermInGraph(tr.o);
5804
+ }
5805
+ function isGroundTerm(t) {
5806
+ if (t instanceof Var)
5807
+ return false;
5808
+ if (t instanceof ListTerm)
5809
+ return t.elems.every((e) => isGroundTerm(e));
5810
+ if (t instanceof OpenListTerm)
5811
+ return false;
5812
+ if (t instanceof GraphTerm)
5813
+ return t.triples.every((tr) => isGroundTripleInGraph(tr));
5814
+ return true;
5815
+ }
5816
+ function isGroundTriple(tr) {
5817
+ return isGroundTerm(tr.s) && isGroundTerm(tr.p) && isGroundTerm(tr.o);
5818
+ }
5819
+ // Canonical JSON-ish encoding for use as a Skolem cache key.
5820
+ // We only *call* this on ground terms in log:skolem, but it is
5821
+ // robust to seeing vars/open lists anyway.
5822
+ function skolemKeyFromTerm(t) {
5823
+ function enc(u) {
5824
+ if (u instanceof Iri)
5825
+ return ['I', u.value];
5826
+ if (u instanceof Literal)
5827
+ return ['L', u.value];
5828
+ if (u instanceof Blank)
5829
+ return ['B', u.label];
5830
+ if (u instanceof Var)
5831
+ return ['V', u.name];
5832
+ if (u instanceof ListTerm)
5833
+ return ['List', u.elems.map(enc)];
5834
+ if (u instanceof OpenListTerm)
5835
+ return ['OpenList', u.prefix.map(enc), u.tailVar];
5836
+ if (u instanceof GraphTerm)
5837
+ return ['Graph', u.triples.map((tr) => [enc(tr.s), enc(tr.p), enc(tr.o)])];
5838
+ return ['Other', String(u)];
5839
+ }
5840
+ return JSON.stringify(enc(t));
5841
+ }
5842
+ function applySubstTerm(t, s) {
5843
+ // Common case: variable
5844
+ if (t instanceof Var) {
5845
+ // Fast path: unbound variable → no change
5846
+ const first = s[t.name];
5847
+ if (first === undefined) {
5848
+ return t;
5849
+ }
5850
+ // Follow chains X -> Y -> ... until we hit a non-var or a cycle.
5851
+ let cur = first;
5852
+ const seen = new Set([t.name]);
5853
+ while (cur instanceof Var) {
5854
+ const name = cur.name;
5855
+ if (seen.has(name))
5856
+ break; // cycle
5857
+ seen.add(name);
5858
+ const nxt = s[name];
5859
+ if (!nxt)
5860
+ break;
5861
+ cur = nxt;
5862
+ }
5863
+ if (cur instanceof Var) {
5864
+ // Still a var: keep it as is (no need to clone)
5865
+ return cur;
5866
+ }
5867
+ // Bound to a non-var term: apply substitution recursively in case it
5868
+ // contains variables inside.
5869
+ return applySubstTerm(cur, s);
5870
+ }
5871
+ // Non-variable terms
5872
+ if (t instanceof ListTerm) {
5873
+ return new ListTerm(t.elems.map((e) => applySubstTerm(e, s)));
5874
+ }
5875
+ if (t instanceof OpenListTerm) {
5876
+ const newPrefix = t.prefix.map((e) => applySubstTerm(e, s));
5877
+ const tailTerm = s[t.tailVar];
5878
+ if (tailTerm !== undefined) {
5879
+ const tailApplied = applySubstTerm(tailTerm, s);
5880
+ if (tailApplied instanceof ListTerm) {
5881
+ return new ListTerm(newPrefix.concat(tailApplied.elems));
5882
+ }
5883
+ else if (tailApplied instanceof OpenListTerm) {
5884
+ return new OpenListTerm(newPrefix.concat(tailApplied.prefix), tailApplied.tailVar);
5885
+ }
5886
+ else {
5887
+ return new OpenListTerm(newPrefix, t.tailVar);
5888
+ }
5889
+ }
5890
+ else {
5891
+ return new OpenListTerm(newPrefix, t.tailVar);
5892
+ }
5893
+ }
5894
+ if (t instanceof GraphTerm) {
5895
+ return new GraphTerm(t.triples.map((tr) => applySubstTriple(tr, s)));
5896
+ }
5897
+ return t;
5898
+ }
5899
+ function applySubstTriple(tr, s) {
5900
+ return new Triple(applySubstTerm(tr.s, s), applySubstTerm(tr.p, s), applySubstTerm(tr.o, s));
5901
+ }
5902
+ function iriValue(t) {
5903
+ return t instanceof Iri ? t.value : null;
5904
+ }
5905
+ function unifyOpenWithList(prefix, tailv, ys, subst) {
5906
+ if (ys.length < prefix.length)
5907
+ return null;
5908
+ let s2 = { ...subst };
5909
+ for (let i = 0; i < prefix.length; i++) {
5910
+ s2 = unifyTerm(prefix[i], ys[i], s2);
5911
+ if (s2 === null)
5912
+ return null;
5913
+ }
5914
+ const rest = new ListTerm(ys.slice(prefix.length));
5915
+ s2 = unifyTerm(new Var(tailv), rest, s2);
5916
+ if (s2 === null)
5917
+ return null;
5918
+ return s2;
5919
+ }
5920
+ function unifyGraphTriples(xs, ys, subst) {
5921
+ if (xs.length !== ys.length)
5922
+ return null;
5923
+ // Fast path: exact same sequence.
5924
+ if (triplesListEqual(xs, ys))
5925
+ return { ...subst };
5926
+ // Backtracking match (order-insensitive), *threading* the substitution through.
5927
+ const used = new Array(ys.length).fill(false);
5928
+ function step(i, s) {
5929
+ if (i >= xs.length)
5930
+ return s;
5931
+ const x = xs[i];
5932
+ for (let j = 0; j < ys.length; j++) {
5933
+ if (used[j])
5934
+ continue;
5935
+ const y = ys[j];
5936
+ // Cheap pruning when both predicates are IRIs.
5937
+ if (x.p instanceof Iri && y.p instanceof Iri && x.p.value !== y.p.value)
5938
+ continue;
5939
+ const s2 = unifyTriple(x, y, s); // IMPORTANT: use `s`, not {}
5940
+ if (s2 === null)
5941
+ continue;
5942
+ used[j] = true;
5943
+ const s3 = step(i + 1, s2);
5944
+ if (s3 !== null)
5945
+ return s3;
5946
+ used[j] = false;
5947
+ }
5948
+ return null;
5949
+ }
5950
+ return step(0, { ...subst }); // IMPORTANT: start from the incoming subst
5951
+ }
5952
+ function unifyTerm(a, b, subst) {
5953
+ return unifyTermWithOptions(a, b, subst, {
5954
+ boolValueEq: true,
5955
+ intDecimalEq: false,
5956
+ });
5957
+ }
5958
+ function unifyTermListAppend(a, b, subst) {
5959
+ // Keep list:append behavior: allow integer<->decimal exact equality,
5960
+ // but do NOT add boolean-value equivalence (preserves current semantics).
5961
+ return unifyTermWithOptions(a, b, subst, {
5962
+ boolValueEq: false,
5963
+ intDecimalEq: true,
5964
+ });
5965
+ }
5966
+ function unifyTermWithOptions(a, b, subst, opts) {
5967
+ a = applySubstTerm(a, subst);
5968
+ b = applySubstTerm(b, subst);
5969
+ // Variable binding
5970
+ if (a instanceof Var) {
5971
+ const v = a.name;
5972
+ const t = b;
5973
+ if (t instanceof Var && t.name === v)
5974
+ return { ...subst };
5975
+ if (containsVarTerm(t, v))
5976
+ return null;
5977
+ const s2 = { ...subst };
5978
+ s2[v] = t;
5979
+ return s2;
5963
5980
  }
5964
- // string:lessThan
5965
- if (pv === STRING_NS + 'lessThan') {
5966
- const sStr = termToJsString(g.s);
5967
- const oStr = termToJsString(g.o);
5968
- if (sStr === null || oStr === null)
5969
- return [];
5970
- return sStr < oStr ? [{ ...subst }] : [];
5981
+ if (b instanceof Var) {
5982
+ return unifyTermWithOptions(b, a, subst, opts);
5971
5983
  }
5972
- // string:matches
5973
- if (pv === STRING_NS + 'matches') {
5974
- const sStr = termToJsString(g.s);
5975
- const pattern = termToJsString(g.o);
5976
- if (sStr === null || pattern === null)
5977
- return [];
5978
- const re = compileSwapRegex(pattern, '');
5979
- if (!re)
5980
- return [];
5981
- return re.test(sStr) ? [{ ...subst }] : [];
5984
+ // Exact matches
5985
+ if (a instanceof Iri && b instanceof Iri && a.value === b.value)
5986
+ return { ...subst };
5987
+ if (a instanceof Literal && b instanceof Literal && a.value === b.value)
5988
+ return { ...subst };
5989
+ if (a instanceof Blank && b instanceof Blank && a.label === b.label)
5990
+ return { ...subst };
5991
+ // Plain string vs xsd:string equivalence
5992
+ if (a instanceof Literal && b instanceof Literal) {
5993
+ if (literalsEquivalentAsXsdString(a.value, b.value))
5994
+ return { ...subst };
5982
5995
  }
5983
- // string:notEqualIgnoringCase
5984
- if (pv === STRING_NS + 'notEqualIgnoringCase') {
5985
- const sStr = termToJsString(g.s);
5986
- const oStr = termToJsString(g.o);
5987
- if (sStr === null || oStr === null)
5988
- return [];
5989
- return sStr.toLowerCase() !== oStr.toLowerCase() ? [{ ...subst }] : [];
5996
+ // Boolean-value equivalence (ONLY for normal unifyTerm)
5997
+ if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
5998
+ const ai = parseBooleanLiteralInfo(a);
5999
+ const bi = parseBooleanLiteralInfo(b);
6000
+ if (ai && bi && ai.value === bi.value)
6001
+ return { ...subst };
5990
6002
  }
5991
- // string:notGreaterThan (≤ in Unicode code order)
5992
- if (pv === STRING_NS + 'notGreaterThan') {
5993
- const sStr = termToJsString(g.s);
5994
- const oStr = termToJsString(g.o);
5995
- if (sStr === null || oStr === null)
5996
- return [];
5997
- return sStr <= oStr ? [{ ...subst }] : [];
6003
+ // Numeric-value match:
6004
+ // - always allow equality when datatype matches (existing behavior)
6005
+ // - optionally allow integer<->decimal exact equality (list:append only)
6006
+ if (a instanceof Literal && b instanceof Literal) {
6007
+ const ai = parseNumericLiteralInfo(a);
6008
+ const bi = parseNumericLiteralInfo(b);
6009
+ if (ai && bi) {
6010
+ if (ai.dt === bi.dt) {
6011
+ if (ai.kind === 'bigint' && bi.kind === 'bigint') {
6012
+ if (ai.value === bi.value)
6013
+ return { ...subst };
6014
+ }
6015
+ else {
6016
+ const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
6017
+ const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
6018
+ if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn)
6019
+ return { ...subst };
6020
+ }
6021
+ }
6022
+ if (opts.intDecimalEq) {
6023
+ const intDt = XSD_NS + 'integer';
6024
+ const decDt = XSD_NS + 'decimal';
6025
+ if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
6026
+ const intInfo = ai.dt === intDt ? ai : bi; // bigint
6027
+ const decInfo = ai.dt === decDt ? ai : bi; // number + lexStr
6028
+ const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
6029
+ if (dec) {
6030
+ const scaledInt = intInfo.value * pow10n(dec.scale);
6031
+ if (scaledInt === dec.num)
6032
+ return { ...subst };
6033
+ }
6034
+ }
6035
+ }
6036
+ }
5998
6037
  }
5999
- // string:notLessThan (≥ in Unicode code order)
6000
- if (pv === STRING_NS + 'notLessThan') {
6001
- const sStr = termToJsString(g.s);
6002
- const oStr = termToJsString(g.o);
6003
- if (sStr === null || oStr === null)
6004
- return [];
6005
- return sStr >= oStr ? [{ ...subst }] : [];
6038
+ // Open list vs concrete list
6039
+ if (a instanceof OpenListTerm && b instanceof ListTerm) {
6040
+ return unifyOpenWithList(a.prefix, a.tailVar, b.elems, subst);
6006
6041
  }
6007
- // string:notMatches
6008
- if (pv === STRING_NS + 'notMatches') {
6009
- const sStr = termToJsString(g.s);
6010
- const pattern = termToJsString(g.o);
6011
- if (sStr === null || pattern === null)
6012
- return [];
6013
- const re = compileSwapRegex(pattern, '');
6014
- if (!re)
6015
- return [];
6016
- return re.test(sStr) ? [] : [{ ...subst }];
6042
+ if (a instanceof ListTerm && b instanceof OpenListTerm) {
6043
+ return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
6017
6044
  }
6018
- // string:replace
6019
- if (pv === STRING_NS + 'replace') {
6020
- if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3)
6021
- return [];
6022
- const dataStr = termToJsString(g.s.elems[0]);
6023
- const searchStr = termToJsString(g.s.elems[1]);
6024
- const replStr = termToJsString(g.s.elems[2]);
6025
- if (dataStr === null || searchStr === null || replStr === null)
6026
- return [];
6027
- const re = compileSwapRegex(searchStr, 'g');
6028
- if (!re)
6029
- return [];
6030
- const outStr = dataStr.replace(re, replStr);
6031
- const lit = makeStringLiteral(outStr);
6032
- if (g.o instanceof Var) {
6033
- const s2 = { ...subst };
6034
- s2[g.o.name] = lit;
6035
- return [s2];
6045
+ // Open list vs open list
6046
+ if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
6047
+ if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
6048
+ return null;
6049
+ let s2 = { ...subst };
6050
+ for (let i = 0; i < a.prefix.length; i++) {
6051
+ s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
6052
+ if (s2 === null)
6053
+ return null;
6036
6054
  }
6037
- const s2 = unifyTerm(g.o, lit, subst);
6038
- return s2 !== null ? [s2] : [];
6055
+ return s2;
6039
6056
  }
6040
- // string:scrape
6041
- if (pv === STRING_NS + 'scrape') {
6042
- if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2)
6043
- return [];
6044
- const dataStr = termToJsString(g.s.elems[0]);
6045
- const pattern = termToJsString(g.s.elems[1]);
6046
- if (dataStr === null || pattern === null)
6047
- return [];
6048
- const re = compileSwapRegex(pattern, '');
6049
- if (!re)
6050
- return [];
6051
- const m = re.exec(dataStr);
6052
- // Spec says “exactly 1 group”; we just use the first capturing group if present.
6053
- if (!m || m.length < 2)
6054
- return [];
6055
- const group = m[1];
6056
- const lit = makeStringLiteral(group);
6057
- if (g.o instanceof Var) {
6058
- const s2 = { ...subst };
6059
- s2[g.o.name] = lit;
6060
- return [s2];
6057
+ // List terms
6058
+ if (a instanceof ListTerm && b instanceof ListTerm) {
6059
+ if (a.elems.length !== b.elems.length)
6060
+ return null;
6061
+ let s2 = { ...subst };
6062
+ for (let i = 0; i < a.elems.length; i++) {
6063
+ s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
6064
+ if (s2 === null)
6065
+ return null;
6061
6066
  }
6062
- const s2 = unifyTerm(g.o, lit, subst);
6063
- return s2 !== null ? [s2] : [];
6067
+ return s2;
6064
6068
  }
6065
- // string:startsWith
6066
- if (pv === STRING_NS + 'startsWith') {
6067
- const sStr = termToJsString(g.s);
6068
- const oStr = termToJsString(g.o);
6069
- if (sStr === null || oStr === null)
6070
- return [];
6071
- return sStr.startsWith(oStr) ? [{ ...subst }] : [];
6069
+ // Graphs
6070
+ if (a instanceof GraphTerm && b instanceof GraphTerm) {
6071
+ if (alphaEqGraphTriples(a.triples, b.triples))
6072
+ return { ...subst };
6073
+ return unifyGraphTriples(a.triples, b.triples, subst);
6072
6074
  }
6073
- // Unknown builtin
6074
- return [];
6075
+ return null;
6075
6076
  }
6076
- function isBuiltinPred(p) {
6077
- if (!(p instanceof Iri))
6078
- return false;
6079
- const v = p.value;
6080
- // Super restricted mode: only treat => / <= as builtins.
6081
- // Everything else should be handled as ordinary predicates (and thus must be
6082
- // provided explicitly as facts/rules, without builtin evaluation).
6083
- if (superRestrictedMode) {
6084
- return v === LOG_NS + 'implies' || v === LOG_NS + 'impliedBy';
6077
+ function unifyTriple(pat, fact, subst) {
6078
+ // Predicates are usually the cheapest and most selective
6079
+ const s1 = unifyTerm(pat.p, fact.p, subst);
6080
+ if (s1 === null)
6081
+ return null;
6082
+ const s2 = unifyTerm(pat.s, fact.s, s1);
6083
+ if (s2 === null)
6084
+ return null;
6085
+ const s3 = unifyTerm(pat.o, fact.o, s2);
6086
+ return s3;
6087
+ }
6088
+ function composeSubst(outer, delta) {
6089
+ if (!delta || Object.keys(delta).length === 0) {
6090
+ return { ...outer };
6085
6091
  }
6086
- // Treat RDF Collections as list-term builtins too.
6087
- if (v === RDF_NS + 'first' || v === RDF_NS + 'rest') {
6088
- return true;
6092
+ const out = { ...outer };
6093
+ for (const [k, v] of Object.entries(delta)) {
6094
+ if (out.hasOwnProperty(k)) {
6095
+ if (!termsEqual(out[k], v))
6096
+ return null;
6097
+ }
6098
+ else {
6099
+ out[k] = v;
6100
+ }
6089
6101
  }
6090
- return (v.startsWith(CRYPTO_NS) ||
6091
- v.startsWith(MATH_NS) ||
6092
- v.startsWith(LOG_NS) ||
6093
- v.startsWith(STRING_NS) ||
6094
- v.startsWith(TIME_NS) ||
6095
- v.startsWith(LIST_NS));
6102
+ return out;
6096
6103
  }
6097
6104
  // ===========================================================================
6098
6105
  // Backward proof (SLD-style)