eyeling 1.9.2 → 1.9.4

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.
@@ -679,1266 +679,6 @@ class DerivedFact {
679
679
  }
680
680
  }
681
681
 
682
- // ===========================================================================
683
- // LEXER
684
- // ===========================================================================
685
-
686
- class Token {
687
- constructor(typ, value = null, offset = null) {
688
- this.typ = typ;
689
- this.value = value;
690
- // Codepoint offset in the original source (Array.from(text) index).
691
- this.offset = offset;
692
- }
693
- toString() {
694
- const loc = typeof this.offset === 'number' ? `@${this.offset}` : '';
695
- if (this.value == null) return `Token(${this.typ}${loc})`;
696
- return `Token(${this.typ}${loc}, ${JSON.stringify(this.value)})`;
697
- }
698
- }
699
-
700
- class N3SyntaxError extends SyntaxError {
701
- constructor(message, offset = null) {
702
- super(message);
703
- this.name = 'N3SyntaxError';
704
- this.offset = offset;
705
- }
706
- }
707
-
708
- function isWs(c) {
709
- return /\s/.test(c);
710
- }
711
-
712
- function isNameChar(c) {
713
- return /[0-9A-Za-z_\-:]/.test(c);
714
- }
715
-
716
- function decodeN3StringEscapes(s) {
717
- let out = '';
718
- for (let i = 0; i < s.length; i++) {
719
- const c = s[i];
720
- if (c !== '\\') {
721
- out += c;
722
- continue;
723
- }
724
- if (i + 1 >= s.length) {
725
- out += '\\';
726
- continue;
727
- }
728
- const e = s[++i];
729
- switch (e) {
730
- case 't':
731
- out += '\t';
732
- break;
733
- case 'n':
734
- out += '\n';
735
- break;
736
- case 'r':
737
- out += '\r';
738
- break;
739
- case 'b':
740
- out += '\b';
741
- break;
742
- case 'f':
743
- out += '\f';
744
- break;
745
- case '"':
746
- out += '"';
747
- break;
748
- case "'":
749
- out += "'";
750
- break;
751
- case '\\':
752
- out += '\\';
753
- break;
754
-
755
- case 'u': {
756
- const hex = s.slice(i + 1, i + 5);
757
- if (/^[0-9A-Fa-f]{4}$/.test(hex)) {
758
- out += String.fromCharCode(parseInt(hex, 16));
759
- i += 4;
760
- } else {
761
- out += '\\u';
762
- }
763
- break;
764
- }
765
-
766
- case 'U': {
767
- const hex = s.slice(i + 1, i + 9);
768
- if (/^[0-9A-Fa-f]{8}$/.test(hex)) {
769
- const cp = parseInt(hex, 16);
770
- if (cp >= 0 && cp <= 0x10ffff) out += String.fromCodePoint(cp);
771
- else out += '\\U' + hex;
772
- i += 8;
773
- } else {
774
- out += '\\U';
775
- }
776
- break;
777
- }
778
-
779
- default:
780
- // preserve unknown escapes
781
- out += '\\' + e;
782
- }
783
- }
784
- return out;
785
- }
786
-
787
- function lex(inputText) {
788
- const chars = Array.from(inputText);
789
- const n = chars.length;
790
- let i = 0;
791
- const tokens = [];
792
-
793
- function peek(offset = 0) {
794
- const j = i + offset;
795
- return j >= 0 && j < n ? chars[j] : null;
796
- }
797
-
798
- while (i < n) {
799
- let c = peek();
800
- if (c === null) break;
801
-
802
- // 1) Whitespace
803
- if (isWs(c)) {
804
- i++;
805
- continue;
806
- }
807
-
808
- // 2) Comments starting with '#'
809
- if (c === '#') {
810
- while (i < n && chars[i] !== '\n' && chars[i] !== '\r') i++;
811
- continue;
812
- }
813
-
814
- // 3) Two-character operators: => and <=
815
- if (c === '=') {
816
- if (peek(1) === '>') {
817
- tokens.push(new Token('OpImplies', null, i));
818
- i += 2;
819
- continue;
820
- } else {
821
- // N3 syntactic sugar: '=' means owl:sameAs
822
- tokens.push(new Token('Equals', null, i));
823
- i += 1;
824
- continue;
825
- }
826
- }
827
-
828
- if (c === '<') {
829
- if (peek(1) === '=') {
830
- tokens.push(new Token('OpImpliedBy', null, i));
831
- i += 2;
832
- continue;
833
- }
834
- // N3 predicate inversion: "<-" (swap subject/object for this predicate)
835
- if (peek(1) === '-') {
836
- tokens.push(new Token('OpPredInvert', null, i));
837
- i += 2;
838
- continue;
839
- }
840
- // Otherwise IRIREF <...>
841
- const start = i;
842
- i++; // skip '<'
843
- const iriChars = [];
844
- while (i < n && chars[i] !== '>') {
845
- iriChars.push(chars[i]);
846
- i++;
847
- }
848
- if (i >= n || chars[i] !== '>') {
849
- throw new N3SyntaxError('Unterminated IRI <...>', start);
850
- }
851
- i++; // skip '>'
852
- const iri = iriChars.join('');
853
- tokens.push(new Token('IriRef', iri, start));
854
- continue;
855
- }
856
-
857
- // 4) Path + datatype operators: !, ^, ^^
858
- if (c === '!') {
859
- tokens.push(new Token('OpPathFwd', null, i));
860
- i += 1;
861
- continue;
862
- }
863
- if (c === '^') {
864
- if (peek(1) === '^') {
865
- tokens.push(new Token('HatHat', null, i));
866
- i += 2;
867
- continue;
868
- }
869
- tokens.push(new Token('OpPathRev', null, i));
870
- i += 1;
871
- continue;
872
- }
873
-
874
- // 5) Single-character punctuation
875
- if ('{}()[];,.'.includes(c)) {
876
- const mapping = {
877
- '{': 'LBrace',
878
- '}': 'RBrace',
879
- '(': 'LParen',
880
- ')': 'RParen',
881
- '[': 'LBracket',
882
- ']': 'RBracket',
883
- ';': 'Semicolon',
884
- ',': 'Comma',
885
- '.': 'Dot',
886
- };
887
- tokens.push(new Token(mapping[c], null, i));
888
- i++;
889
- continue;
890
- }
891
-
892
- // String literal: short "..." or long """..."""
893
- if (c === '"') {
894
- const start = i;
895
-
896
- // Long string literal """ ... """
897
- if (peek(1) === '"' && peek(2) === '"') {
898
- i += 3; // consume opening """
899
- const sChars = [];
900
- let closed = false;
901
- while (i < n) {
902
- const cc = chars[i];
903
-
904
- // Preserve escapes verbatim (same behavior as short strings)
905
- if (cc === '\\') {
906
- i++;
907
- if (i < n) {
908
- const esc = chars[i];
909
- i++;
910
- sChars.push('\\');
911
- sChars.push(esc);
912
- } else {
913
- sChars.push('\\');
914
- }
915
- continue;
916
- }
917
-
918
- // In long strings, a run of >= 3 delimiter quotes terminates the literal.
919
- // Any extra quotes beyond the final 3 are part of the content.
920
- if (cc === '"') {
921
- let run = 0;
922
- while (i + run < n && chars[i + run] === '"') run++;
923
-
924
- if (run >= 3) {
925
- for (let k = 0; k < run - 3; k++) sChars.push('"');
926
- i += run; // consume content quotes (if any) + closing delimiter
927
- closed = true;
928
- break;
929
- }
930
-
931
- for (let k = 0; k < run; k++) sChars.push('"');
932
- i += run;
933
- continue;
934
- }
935
-
936
- sChars.push(cc);
937
- i++;
938
- }
939
- if (!closed) throw new N3SyntaxError('Unterminated long string literal """..."""', start);
940
- const raw = '"""' + sChars.join('') + '"""';
941
- const decoded = decodeN3StringEscapes(stripQuotes(raw));
942
- const s = JSON.stringify(decoded); // canonical short quoted form
943
- tokens.push(new Token('Literal', s, start));
944
- continue;
945
- }
946
-
947
- // Short string literal " ... "
948
- i++; // consume opening "
949
- const sChars = [];
950
- while (i < n) {
951
- let cc = chars[i];
952
- i++;
953
- if (cc === '\\') {
954
- if (i < n) {
955
- const esc = chars[i];
956
- i++;
957
- sChars.push('\\');
958
- sChars.push(esc);
959
- }
960
- continue;
961
- }
962
- if (cc === '"') break;
963
- sChars.push(cc);
964
- }
965
- const raw = '"' + sChars.join('') + '"';
966
- const decoded = decodeN3StringEscapes(stripQuotes(raw));
967
- const s = JSON.stringify(decoded); // canonical short quoted form
968
- tokens.push(new Token('Literal', s, start));
969
- continue;
970
- }
971
-
972
- // String literal: short '...' or long '''...'''
973
- if (c === "'") {
974
- const start = i;
975
-
976
- // Long string literal ''' ... '''
977
- if (peek(1) === "'" && peek(2) === "'") {
978
- i += 3; // consume opening '''
979
- const sChars = [];
980
- let closed = false;
981
- while (i < n) {
982
- const cc = chars[i];
983
-
984
- // Preserve escapes verbatim (same behavior as short strings)
985
- if (cc === '\\') {
986
- i++;
987
- if (i < n) {
988
- const esc = chars[i];
989
- i++;
990
- sChars.push('\\');
991
- sChars.push(esc);
992
- } else {
993
- sChars.push('\\');
994
- }
995
- continue;
996
- }
997
-
998
- // In long strings, a run of >= 3 delimiter quotes terminates the literal.
999
- // Any extra quotes beyond the final 3 are part of the content.
1000
- if (cc === "'") {
1001
- let run = 0;
1002
- while (i + run < n && chars[i + run] === "'") run++;
1003
-
1004
- if (run >= 3) {
1005
- for (let k = 0; k < run - 3; k++) sChars.push("'");
1006
- i += run; // consume content quotes (if any) + closing delimiter
1007
- closed = true;
1008
- break;
1009
- }
1010
-
1011
- for (let k = 0; k < run; k++) sChars.push("'");
1012
- i += run;
1013
- continue;
1014
- }
1015
-
1016
- sChars.push(cc);
1017
- i++;
1018
- }
1019
- if (!closed) throw new N3SyntaxError("Unterminated long string literal '''...'''", start);
1020
- const raw = "'''" + sChars.join('') + "'''";
1021
- const decoded = decodeN3StringEscapes(stripQuotes(raw));
1022
- const s = JSON.stringify(decoded); // canonical short quoted form
1023
- tokens.push(new Token('Literal', s, start));
1024
- continue;
1025
- }
1026
-
1027
- // Short string literal ' ... '
1028
- i++; // consume opening '
1029
- const sChars = [];
1030
- while (i < n) {
1031
- let cc = chars[i];
1032
- i++;
1033
- if (cc === '\\') {
1034
- if (i < n) {
1035
- const esc = chars[i];
1036
- i++;
1037
- sChars.push('\\');
1038
- sChars.push(esc);
1039
- }
1040
- continue;
1041
- }
1042
- if (cc === "'") break;
1043
- sChars.push(cc);
1044
- }
1045
- const raw = "'" + sChars.join('') + "'";
1046
- const decoded = decodeN3StringEscapes(stripQuotes(raw));
1047
- const s = JSON.stringify(decoded); // canonical short quoted form
1048
- tokens.push(new Token('Literal', s, start));
1049
- continue;
1050
- }
1051
-
1052
- // Variable ?name
1053
- if (c === '?') {
1054
- const start = i;
1055
- i++;
1056
- const nameChars = [];
1057
- let cc;
1058
- while ((cc = peek()) !== null && isNameChar(cc)) {
1059
- nameChars.push(cc);
1060
- i++;
1061
- }
1062
- const name = nameChars.join('');
1063
- tokens.push(new Token('Var', name, start));
1064
- continue;
1065
- }
1066
-
1067
- // Directives: @prefix, @base (and language tags after string literals)
1068
- if (c === '@') {
1069
- const start = i;
1070
- const prevTok = tokens.length ? tokens[tokens.length - 1] : null;
1071
- const prevWasQuotedLiteral =
1072
- prevTok && prevTok.typ === 'Literal' && typeof prevTok.value === 'string' && prevTok.value.startsWith('"');
1073
-
1074
- i++; // consume '@'
1075
-
1076
- if (prevWasQuotedLiteral) {
1077
- // N3 grammar production LANGTAG:
1078
- // "@" [a-zA-Z]+ ("-" [a-zA-Z0-9]+)*
1079
- const tagChars = [];
1080
- let cc = peek();
1081
- if (cc === null || !/[A-Za-z]/.test(cc)) {
1082
- throw new N3SyntaxError("Invalid language tag (expected [A-Za-z] after '@')", start);
1083
- }
1084
- while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
1085
- tagChars.push(cc);
1086
- i++;
1087
- }
1088
- while (peek() === '-') {
1089
- tagChars.push('-');
1090
- i++; // consume '-'
1091
- const segChars = [];
1092
- while ((cc = peek()) !== null && /[A-Za-z0-9]/.test(cc)) {
1093
- segChars.push(cc);
1094
- i++;
1095
- }
1096
- if (!segChars.length) {
1097
- throw new N3SyntaxError("Invalid language tag (expected [A-Za-z0-9]+ after '-')", start);
1098
- }
1099
- tagChars.push(...segChars);
1100
- }
1101
- tokens.push(new Token('LangTag', tagChars.join(''), start));
1102
- continue;
1103
- }
1104
-
1105
- // Otherwise, treat as a directive (@prefix, @base)
1106
- const wordChars = [];
1107
- let cc;
1108
- while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
1109
- wordChars.push(cc);
1110
- i++;
1111
- }
1112
- const word = wordChars.join('');
1113
- if (word === 'prefix') tokens.push(new Token('AtPrefix', null, start));
1114
- else if (word === 'base') tokens.push(new Token('AtBase', null, start));
1115
- else throw new N3SyntaxError(`Unknown directive @${word}`, start);
1116
- continue;
1117
- }
1118
-
1119
- // 6) Numeric literal (integer or float)
1120
- if (/[0-9]/.test(c) || (c === '-' && peek(1) !== null && /[0-9]/.test(peek(1)))) {
1121
- const start = i;
1122
- const numChars = [c];
1123
- i++;
1124
- while (i < n) {
1125
- const cc = chars[i];
1126
- if (/[0-9]/.test(cc)) {
1127
- numChars.push(cc);
1128
- i++;
1129
- continue;
1130
- }
1131
- if (cc === '.') {
1132
- if (i + 1 < n && /[0-9]/.test(chars[i + 1])) {
1133
- numChars.push('.');
1134
- i++;
1135
- continue;
1136
- } else {
1137
- break;
1138
- }
1139
- }
1140
- break;
1141
- }
1142
-
1143
- // Optional exponent part: e.g., 1e0, 1.1e-3, 1.1E+0
1144
- if (i < n && (chars[i] === 'e' || chars[i] === 'E')) {
1145
- let j = i + 1;
1146
- if (j < n && (chars[j] === '+' || chars[j] === '-')) j++;
1147
- if (j < n && /[0-9]/.test(chars[j])) {
1148
- numChars.push(chars[i]); // e/E
1149
- i++;
1150
- if (i < n && (chars[i] === '+' || chars[i] === '-')) {
1151
- numChars.push(chars[i]);
1152
- i++;
1153
- }
1154
- while (i < n && /[0-9]/.test(chars[i])) {
1155
- numChars.push(chars[i]);
1156
- i++;
1157
- }
1158
- }
1159
- }
1160
-
1161
- tokens.push(new Token('Literal', numChars.join(''), start));
1162
- continue;
1163
- }
1164
-
1165
- // 7) Identifiers / keywords / QNames
1166
- const start = i;
1167
- const wordChars = [];
1168
- let cc;
1169
- while ((cc = peek()) !== null && isNameChar(cc)) {
1170
- wordChars.push(cc);
1171
- i++;
1172
- }
1173
- if (!wordChars.length) {
1174
- throw new N3SyntaxError(`Unexpected char: ${JSON.stringify(c)}`, i);
1175
- }
1176
- const word = wordChars.join('');
1177
- if (word === 'true' || word === 'false') {
1178
- tokens.push(new Token('Literal', word, start));
1179
- } else if ([...word].every((ch) => /[0-9.\-]/.test(ch))) {
1180
- tokens.push(new Token('Literal', word, start));
1181
- } else {
1182
- tokens.push(new Token('Ident', word, start));
1183
- }
1184
- }
1185
-
1186
- tokens.push(new Token('EOF', null, n));
1187
- return tokens;
1188
- }
1189
-
1190
- // ===========================================================================
1191
- // PREFIX ENVIRONMENT
1192
- // ===========================================================================
1193
-
1194
- class PrefixEnv {
1195
- constructor(map, baseIri) {
1196
- this.map = map || {}; // prefix -> IRI (including "" for @prefix :)
1197
- this.baseIri = baseIri || ''; // base IRI for resolving <relative>
1198
- }
1199
-
1200
- static newDefault() {
1201
- const m = {};
1202
- m['rdf'] = RDF_NS;
1203
- m['rdfs'] = RDFS_NS;
1204
- m['xsd'] = XSD_NS;
1205
- m['log'] = LOG_NS;
1206
- m['math'] = MATH_NS;
1207
- m['string'] = STRING_NS;
1208
- m['list'] = LIST_NS;
1209
- m['time'] = TIME_NS;
1210
- m['genid'] = SKOLEM_NS;
1211
- m[''] = ''; // empty prefix default namespace
1212
- return new PrefixEnv(m, ''); // base IRI starts empty
1213
- }
1214
-
1215
- set(pref, base) {
1216
- this.map[pref] = base;
1217
- }
1218
-
1219
- setBase(baseIri) {
1220
- this.baseIri = baseIri || '';
1221
- }
1222
-
1223
- expandQName(q) {
1224
- if (q.includes(':')) {
1225
- const [p, local] = q.split(':', 2);
1226
- const base = this.map[p] || '';
1227
- if (base) return base + local;
1228
- return q;
1229
- }
1230
- return q;
1231
- }
1232
-
1233
- shrinkIri(iri) {
1234
- let best = null; // [prefix, local]
1235
- for (const [p, base] of Object.entries(this.map)) {
1236
- if (!base) continue;
1237
- if (iri.startsWith(base)) {
1238
- const local = iri.slice(base.length);
1239
- if (!local) continue;
1240
- const cand = [p, local];
1241
- if (best === null || cand[1].length < best[1].length) best = cand;
1242
- }
1243
- }
1244
- if (best === null) return null;
1245
- const [p, local] = best;
1246
- if (p === '') return `:${local}`;
1247
- return `${p}:${local}`;
1248
- }
1249
-
1250
- prefixesUsedForOutput(triples) {
1251
- const used = new Set();
1252
- for (const t of triples) {
1253
- const iris = [];
1254
- iris.push(...collectIrisInTerm(t.s));
1255
- if (!isRdfTypePred(t.p)) {
1256
- iris.push(...collectIrisInTerm(t.p));
1257
- }
1258
- iris.push(...collectIrisInTerm(t.o));
1259
- for (const iri of iris) {
1260
- for (const [p, base] of Object.entries(this.map)) {
1261
- if (base && iri.startsWith(base)) used.add(p);
1262
- }
1263
- }
1264
- }
1265
- const v = [];
1266
- for (const p of used) {
1267
- if (this.map.hasOwnProperty(p)) v.push([p, this.map[p]]);
1268
- }
1269
- v.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
1270
- return v;
1271
- }
1272
- }
1273
-
1274
- function collectIrisInTerm(t) {
1275
- const out = [];
1276
- if (t instanceof Iri) {
1277
- out.push(t.value);
1278
- } else if (t instanceof Literal) {
1279
- const [_lex, dt] = literalParts(t.value);
1280
- if (dt) out.push(dt); // so rdf/xsd prefixes are emitted when only used in ^^...
1281
- } else if (t instanceof ListTerm) {
1282
- for (const x of t.elems) out.push(...collectIrisInTerm(x));
1283
- } else if (t instanceof OpenListTerm) {
1284
- for (const x of t.prefix) out.push(...collectIrisInTerm(x));
1285
- } else if (t instanceof GraphTerm) {
1286
- for (const tr of t.triples) {
1287
- out.push(...collectIrisInTerm(tr.s));
1288
- out.push(...collectIrisInTerm(tr.p));
1289
- out.push(...collectIrisInTerm(tr.o));
1290
- }
1291
- }
1292
- return out;
1293
- }
1294
-
1295
- function collectVarsInTerm(t, acc) {
1296
- if (t instanceof Var) {
1297
- acc.add(t.name);
1298
- } else if (t instanceof ListTerm) {
1299
- for (const x of t.elems) collectVarsInTerm(x, acc);
1300
- } else if (t instanceof OpenListTerm) {
1301
- for (const x of t.prefix) collectVarsInTerm(x, acc);
1302
- acc.add(t.tailVar);
1303
- } else if (t instanceof GraphTerm) {
1304
- for (const tr of t.triples) {
1305
- collectVarsInTerm(tr.s, acc);
1306
- collectVarsInTerm(tr.p, acc);
1307
- collectVarsInTerm(tr.o, acc);
1308
- }
1309
- }
1310
- }
1311
-
1312
- function varsInRule(rule) {
1313
- const acc = new Set();
1314
- for (const tr of rule.premise) {
1315
- collectVarsInTerm(tr.s, acc);
1316
- collectVarsInTerm(tr.p, acc);
1317
- collectVarsInTerm(tr.o, acc);
1318
- }
1319
- for (const tr of rule.conclusion) {
1320
- collectVarsInTerm(tr.s, acc);
1321
- collectVarsInTerm(tr.p, acc);
1322
- collectVarsInTerm(tr.o, acc);
1323
- }
1324
- return acc;
1325
- }
1326
-
1327
- function collectBlankLabelsInTerm(t, acc) {
1328
- if (t instanceof Blank) {
1329
- acc.add(t.label);
1330
- } else if (t instanceof ListTerm) {
1331
- for (const x of t.elems) collectBlankLabelsInTerm(x, acc);
1332
- } else if (t instanceof OpenListTerm) {
1333
- for (const x of t.prefix) collectBlankLabelsInTerm(x, acc);
1334
- } else if (t instanceof GraphTerm) {
1335
- for (const tr of t.triples) {
1336
- collectBlankLabelsInTerm(tr.s, acc);
1337
- collectBlankLabelsInTerm(tr.p, acc);
1338
- collectBlankLabelsInTerm(tr.o, acc);
1339
- }
1340
- }
1341
- }
1342
-
1343
- function collectBlankLabelsInTriples(triples) {
1344
- const acc = new Set();
1345
- for (const tr of triples) {
1346
- collectBlankLabelsInTerm(tr.s, acc);
1347
- collectBlankLabelsInTerm(tr.p, acc);
1348
- collectBlankLabelsInTerm(tr.o, acc);
1349
- }
1350
- return acc;
1351
- }
1352
-
1353
- // ===========================================================================
1354
- // PARSER
1355
- // ===========================================================================
1356
-
1357
- class Parser {
1358
- constructor(tokens) {
1359
- this.toks = tokens;
1360
- this.pos = 0;
1361
- this.prefixes = PrefixEnv.newDefault();
1362
- this.blankCounter = 0;
1363
- this.pendingTriples = [];
1364
- }
1365
-
1366
- peek() {
1367
- return this.toks[this.pos];
1368
- }
1369
-
1370
- next() {
1371
- const tok = this.toks[this.pos];
1372
- this.pos += 1;
1373
- return tok;
1374
- }
1375
-
1376
- fail(message, tok = this.peek()) {
1377
- const off = tok && typeof tok.offset === 'number' ? tok.offset : null;
1378
- throw new N3SyntaxError(message, off);
1379
- }
1380
-
1381
- expectDot() {
1382
- const tok = this.next();
1383
- if (tok.typ !== 'Dot') {
1384
- this.fail(`Expected '.', got ${tok.toString()}`, tok);
1385
- }
1386
- }
1387
-
1388
- parseDocument() {
1389
- const triples = [];
1390
- const forwardRules = [];
1391
- const backwardRules = [];
1392
-
1393
- while (this.peek().typ !== 'EOF') {
1394
- if (this.peek().typ === 'AtPrefix') {
1395
- this.next();
1396
- this.parsePrefixDirective();
1397
- } else if (this.peek().typ === 'AtBase') {
1398
- this.next();
1399
- this.parseBaseDirective();
1400
- } else if (
1401
- // SPARQL-style/Turtle-style directives (case-insensitive, no trailing '.')
1402
- this.peek().typ === 'Ident' &&
1403
- typeof this.peek().value === 'string' &&
1404
- this.peek().value.toLowerCase() === 'prefix' &&
1405
- this.toks[this.pos + 1] &&
1406
- this.toks[this.pos + 1].typ === 'Ident' &&
1407
- typeof this.toks[this.pos + 1].value === 'string' &&
1408
- // Require PNAME_NS form (e.g., "ex:" or ":") to avoid clashing with a normal triple starting with IRI "prefix".
1409
- this.toks[this.pos + 1].value.endsWith(':') &&
1410
- this.toks[this.pos + 2] &&
1411
- (this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')
1412
- ) {
1413
- this.next(); // consume PREFIX keyword
1414
- this.parseSparqlPrefixDirective();
1415
- } else if (
1416
- this.peek().typ === 'Ident' &&
1417
- typeof this.peek().value === 'string' &&
1418
- this.peek().value.toLowerCase() === 'base' &&
1419
- this.toks[this.pos + 1] &&
1420
- // SPARQL BASE requires an IRIREF.
1421
- this.toks[this.pos + 1].typ === 'IriRef'
1422
- ) {
1423
- this.next(); // consume BASE keyword
1424
- this.parseSparqlBaseDirective();
1425
- } else {
1426
- const first = this.parseTerm();
1427
- if (this.peek().typ === 'OpImplies') {
1428
- this.next();
1429
- const second = this.parseTerm();
1430
- this.expectDot();
1431
- forwardRules.push(this.makeRule(first, second, true));
1432
- } else if (this.peek().typ === 'OpImpliedBy') {
1433
- this.next();
1434
- const second = this.parseTerm();
1435
- this.expectDot();
1436
- backwardRules.push(this.makeRule(first, second, false));
1437
- } else {
1438
- let more;
1439
-
1440
- if (this.peek().typ === 'Dot') {
1441
- // N3 grammar allows: triples ::= subject predicateObjectList?
1442
- // So a bare subject followed by '.' is syntactically valid.
1443
- // If the subject was a path / property-list that generated helper triples,
1444
- // we emit those; otherwise this statement contributes no triples.
1445
- more = [];
1446
- if (this.pendingTriples.length > 0) {
1447
- more = this.pendingTriples;
1448
- this.pendingTriples = [];
1449
- }
1450
- this.next(); // consume '.'
1451
- } else {
1452
- more = this.parsePredicateObjectList(first);
1453
- this.expectDot();
1454
- }
1455
-
1456
- // normalize explicit log:implies / log:impliedBy at top-level
1457
- for (const tr of more) {
1458
- if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
1459
- forwardRules.push(this.makeRule(tr.s, tr.o, true));
1460
- } else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
1461
- backwardRules.push(this.makeRule(tr.s, tr.o, false));
1462
- } else {
1463
- triples.push(tr);
1464
- }
1465
- }
1466
- }
1467
- }
1468
- }
1469
-
1470
- return [this.prefixes, triples, forwardRules, backwardRules];
1471
- }
1472
-
1473
- parsePrefixDirective() {
1474
- const tok = this.next();
1475
- if (tok.typ !== 'Ident') {
1476
- this.fail(`Expected prefix name, got ${tok.toString()}`, tok);
1477
- }
1478
- const pref = tok.value || '';
1479
- const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
1480
-
1481
- if (this.peek().typ === 'Dot') {
1482
- this.next();
1483
- if (!this.prefixes.map.hasOwnProperty(prefName)) {
1484
- this.prefixes.set(prefName, '');
1485
- }
1486
- return;
1487
- }
1488
-
1489
- const tok2 = this.next();
1490
- let iri;
1491
- if (tok2.typ === 'IriRef') {
1492
- iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
1493
- } else if (tok2.typ === 'Ident') {
1494
- iri = this.prefixes.expandQName(tok2.value || '');
1495
- } else {
1496
- this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
1497
- }
1498
- this.expectDot();
1499
- this.prefixes.set(prefName, iri);
1500
- }
1501
-
1502
- parseBaseDirective() {
1503
- const tok = this.next();
1504
- let iri;
1505
- if (tok.typ === 'IriRef') {
1506
- iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
1507
- } else if (tok.typ === 'Ident') {
1508
- iri = tok.value || '';
1509
- } else {
1510
- this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
1511
- }
1512
- this.expectDot();
1513
- this.prefixes.setBase(iri);
1514
- }
1515
-
1516
- parseSparqlPrefixDirective() {
1517
- // SPARQL/Turtle-style PREFIX directive: PREFIX pfx: <iri> (no trailing '.')
1518
- const tok = this.next();
1519
- if (tok.typ !== 'Ident') {
1520
- this.fail(`Expected prefix name after PREFIX, got ${tok.toString()}`, tok);
1521
- }
1522
- const pref = tok.value || '';
1523
- const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
1524
-
1525
- const tok2 = this.next();
1526
- let iri;
1527
- if (tok2.typ === 'IriRef') {
1528
- iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
1529
- } else if (tok2.typ === 'Ident') {
1530
- iri = this.prefixes.expandQName(tok2.value || '');
1531
- } else {
1532
- this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
1533
- }
1534
-
1535
- // N3/Turtle: PREFIX directives do not have a trailing '.', but accept it permissively.
1536
- if (this.peek().typ === 'Dot') this.next();
1537
-
1538
- this.prefixes.set(prefName, iri);
1539
- }
1540
-
1541
- parseSparqlBaseDirective() {
1542
- // SPARQL/Turtle-style BASE directive: BASE <iri> (no trailing '.')
1543
- const tok = this.next();
1544
- let iri;
1545
- if (tok.typ === 'IriRef') {
1546
- iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
1547
- } else if (tok.typ === 'Ident') {
1548
- iri = tok.value || '';
1549
- } else {
1550
- this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
1551
- }
1552
-
1553
- // N3/Turtle: BASE directives do not have a trailing '.', but accept it permissively.
1554
- if (this.peek().typ === 'Dot') this.next();
1555
-
1556
- this.prefixes.setBase(iri);
1557
- }
1558
-
1559
- parseTerm() {
1560
- let t = this.parsePathItem();
1561
-
1562
- while (this.peek().typ === 'OpPathFwd' || this.peek().typ === 'OpPathRev') {
1563
- const dir = this.next().typ; // OpPathFwd | OpPathRev
1564
- const pred = this.parsePathItem();
1565
-
1566
- this.blankCounter += 1;
1567
- const bn = new Blank(`_:b${this.blankCounter}`);
1568
-
1569
- this.pendingTriples.push(dir === 'OpPathFwd' ? new Triple(t, pred, bn) : new Triple(bn, pred, t));
1570
-
1571
- t = bn;
1572
- }
1573
-
1574
- return t;
1575
- }
1576
-
1577
- parsePathItem() {
1578
- const tok = this.next();
1579
- const typ = tok.typ;
1580
- const val = tok.value;
1581
-
1582
- if (typ === 'Equals') {
1583
- return internIri(OWL_NS + 'sameAs');
1584
- }
1585
-
1586
- if (typ === 'IriRef') {
1587
- const base = this.prefixes.baseIri || '';
1588
- return internIri(resolveIriRef(val || '', base));
1589
- }
1590
- if (typ === 'Ident') {
1591
- const name = val || '';
1592
- if (name === 'a') {
1593
- return internIri(RDF_NS + 'type');
1594
- } else if (name.startsWith('_:')) {
1595
- return new Blank(name);
1596
- } else if (name.includes(':')) {
1597
- return internIri(this.prefixes.expandQName(name));
1598
- } else {
1599
- return internIri(name);
1600
- }
1601
- }
1602
-
1603
- if (typ === 'Literal') {
1604
- let s = val || '';
1605
-
1606
- // Optional language tag: "..."@en, per N3 LANGTAG production.
1607
- if (this.peek().typ === 'LangTag') {
1608
- // Only quoted string literals can carry a language tag.
1609
- if (!(s.startsWith('"') && s.endsWith('"'))) {
1610
- this.fail('Language tag is only allowed on quoted string literals', this.peek());
1611
- }
1612
- const langTok = this.next();
1613
- const lang = langTok.value || '';
1614
- s = `${s}@${lang}`;
1615
-
1616
- // N3/Turtle: language tags and datatypes are mutually exclusive.
1617
- if (this.peek().typ === 'HatHat') {
1618
- this.fail('A literal cannot have both a language tag (@...) and a datatype (^^...)', this.peek());
1619
- }
1620
- }
1621
-
1622
- if (this.peek().typ === 'HatHat') {
1623
- this.next();
1624
- const dtTok = this.next();
1625
- let dtIri;
1626
- if (dtTok.typ === 'IriRef') {
1627
- dtIri = dtTok.value || '';
1628
- } else if (dtTok.typ === 'Ident') {
1629
- const qn = dtTok.value || '';
1630
- if (qn.includes(':')) dtIri = this.prefixes.expandQName(qn);
1631
- else dtIri = qn;
1632
- } else {
1633
- this.fail(`Expected datatype after ^^, got ${dtTok.toString()}`, dtTok);
1634
- }
1635
- s = `${s}^^<${dtIri}>`;
1636
- }
1637
- return internLiteral(s);
1638
- }
1639
-
1640
- if (typ === 'Var') return new Var(val || '');
1641
- if (typ === 'LParen') return this.parseList();
1642
- if (typ === 'LBracket') return this.parseBlank();
1643
- if (typ === 'LBrace') return this.parseGraph();
1644
-
1645
- this.fail(`Unexpected term token: ${tok.toString()}`, tok);
1646
- }
1647
-
1648
- parseList() {
1649
- const elems = [];
1650
- while (this.peek().typ !== 'RParen') {
1651
- elems.push(this.parseTerm());
1652
- }
1653
- this.next(); // consume ')'
1654
- return new ListTerm(elems);
1655
- }
1656
-
1657
- parseBlank() {
1658
- // [] or [ ... ] property list
1659
- if (this.peek().typ === 'RBracket') {
1660
- this.next();
1661
- this.blankCounter += 1;
1662
- return new Blank(`_:b${this.blankCounter}`);
1663
- }
1664
-
1665
- // IRI property list: [ id <IRI> predicateObjectList? ]
1666
- // Lets you embed descriptions of an IRI directly in object position.
1667
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'id') {
1668
- const iriTok = this.next(); // consume 'id'
1669
- const iriTerm = this.parseTerm();
1670
-
1671
- // N3 note: 'id' form is not meant to be used with blank node identifiers.
1672
- if (iriTerm instanceof Blank && iriTerm.label.startsWith('_:')) {
1673
- this.fail("Cannot use 'id' keyword with a blank node identifier inside [...]", iriTok);
1674
- }
1675
-
1676
- // Optional ';' right after the id IRI (tolerated).
1677
- if (this.peek().typ === 'Semicolon') this.next();
1678
-
1679
- // Empty IRI property list: [ id :iri ]
1680
- if (this.peek().typ === 'RBracket') {
1681
- this.next();
1682
- return iriTerm;
1683
- }
1684
-
1685
- const subj = iriTerm;
1686
- while (true) {
1687
- let pred;
1688
- let invert = false;
1689
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
1690
- this.next();
1691
- pred = internIri(RDF_NS + 'type');
1692
- } else if (this.peek().typ === 'OpPredInvert') {
1693
- this.next(); // "<-"
1694
- pred = this.parseTerm();
1695
- invert = true;
1696
- } else {
1697
- pred = this.parseTerm();
1698
- }
1699
-
1700
- const objs = [this.parseTerm()];
1701
- while (this.peek().typ === 'Comma') {
1702
- this.next();
1703
- objs.push(this.parseTerm());
1704
- }
1705
-
1706
- for (const o of objs) {
1707
- this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
1708
- }
1709
-
1710
- if (this.peek().typ === 'Semicolon') {
1711
- this.next();
1712
- if (this.peek().typ === 'RBracket') break;
1713
- continue;
1714
- }
1715
- break;
1716
- }
1717
-
1718
- if (this.peek().typ !== 'RBracket') {
1719
- this.fail(`Expected ']' at end of IRI property list, got ${this.peek().toString()}`);
1720
- }
1721
- this.next();
1722
- return iriTerm;
1723
- }
1724
-
1725
- // [ predicateObjectList ]
1726
- this.blankCounter += 1;
1727
- const id = `_:b${this.blankCounter}`;
1728
- const subj = new Blank(id);
1729
-
1730
- while (true) {
1731
- // Verb (can also be 'a')
1732
- let pred;
1733
- let invert = false;
1734
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
1735
- this.next();
1736
- pred = internIri(RDF_NS + 'type');
1737
- } else if (this.peek().typ === 'OpPredInvert') {
1738
- this.next(); // consume "<-"
1739
- pred = this.parseTerm();
1740
- invert = true;
1741
- } else {
1742
- pred = this.parseTerm();
1743
- }
1744
-
1745
- // Object list: o1, o2, ...
1746
- const objs = [this.parseTerm()];
1747
- while (this.peek().typ === 'Comma') {
1748
- this.next();
1749
- objs.push(this.parseTerm());
1750
- }
1751
-
1752
- for (const o of objs) {
1753
- this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
1754
- }
1755
-
1756
- if (this.peek().typ === 'Semicolon') {
1757
- this.next();
1758
- if (this.peek().typ === 'RBracket') break;
1759
- continue;
1760
- }
1761
- break;
1762
- }
1763
-
1764
- if (this.peek().typ === 'RBracket') {
1765
- this.next();
1766
- } else {
1767
- this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
1768
- }
1769
-
1770
- return new Blank(id);
1771
- }
1772
-
1773
- parseGraph() {
1774
- const triples = [];
1775
- while (this.peek().typ !== 'RBrace') {
1776
- const left = this.parseTerm();
1777
- if (this.peek().typ === 'OpImplies') {
1778
- this.next();
1779
- const right = this.parseTerm();
1780
- const pred = internIri(LOG_NS + 'implies');
1781
- triples.push(new Triple(left, pred, right));
1782
- if (this.peek().typ === 'Dot') this.next();
1783
- else if (this.peek().typ === 'RBrace') {
1784
- // ok
1785
- } else {
1786
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1787
- }
1788
- } else if (this.peek().typ === 'OpImpliedBy') {
1789
- this.next();
1790
- const right = this.parseTerm();
1791
- const pred = internIri(LOG_NS + 'impliedBy');
1792
- triples.push(new Triple(left, pred, right));
1793
- if (this.peek().typ === 'Dot') this.next();
1794
- else if (this.peek().typ === 'RBrace') {
1795
- // ok
1796
- } else {
1797
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1798
- }
1799
- } else {
1800
- // N3 grammar allows: triples ::= subject predicateObjectList?
1801
- // So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
1802
- if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
1803
- if (this.pendingTriples.length > 0) {
1804
- triples.push(...this.pendingTriples);
1805
- this.pendingTriples = [];
1806
- }
1807
- if (this.peek().typ === 'Dot') this.next();
1808
- continue;
1809
- }
1810
-
1811
- triples.push(...this.parsePredicateObjectList(left));
1812
- if (this.peek().typ === 'Dot') this.next();
1813
- else if (this.peek().typ === 'RBrace') {
1814
- // ok
1815
- } else {
1816
- this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1817
- }
1818
- }
1819
- }
1820
- this.next(); // consume '}'
1821
- return new GraphTerm(triples);
1822
- }
1823
-
1824
- parsePredicateObjectList(subject) {
1825
- const out = [];
1826
-
1827
- // If the SUBJECT was a path, emit its helper triples first
1828
- if (this.pendingTriples.length > 0) {
1829
- out.push(...this.pendingTriples);
1830
- this.pendingTriples = [];
1831
- }
1832
-
1833
- while (true) {
1834
- let verb;
1835
- let invert = false;
1836
-
1837
- if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
1838
- this.next();
1839
- verb = internIri(RDF_NS + 'type');
1840
- } else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
1841
- // N3 syntactic sugar: "S has P O." means "S P O."
1842
- this.next(); // consume "has"
1843
- verb = this.parseTerm();
1844
- } else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
1845
- // N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
1846
- this.next(); // consume "is"
1847
- verb = this.parseTerm();
1848
- if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
1849
- this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
1850
- }
1851
- this.next(); // consume "of"
1852
- invert = true;
1853
- } else if (this.peek().typ === 'OpPredInvert') {
1854
- this.next(); // "<-"
1855
- verb = this.parseTerm();
1856
- invert = true;
1857
- } else {
1858
- verb = this.parseTerm();
1859
- }
1860
-
1861
- const objects = this.parseObjectList();
1862
-
1863
- // If VERB or OBJECTS contained paths, their helper triples must come
1864
- // before the triples that consume the path results (Easter depends on this).
1865
- if (this.pendingTriples.length > 0) {
1866
- out.push(...this.pendingTriples);
1867
- this.pendingTriples = [];
1868
- }
1869
-
1870
- for (const o of objects) {
1871
- out.push(new Triple(invert ? o : subject, verb, invert ? subject : o));
1872
- }
1873
-
1874
- if (this.peek().typ === 'Semicolon') {
1875
- this.next();
1876
- if (this.peek().typ === 'Dot') break;
1877
- continue;
1878
- }
1879
- break;
1880
- }
1881
-
1882
- return out;
1883
- }
1884
-
1885
- parseObjectList() {
1886
- const objs = [this.parseTerm()];
1887
- while (this.peek().typ === 'Comma') {
1888
- this.next();
1889
- objs.push(this.parseTerm());
1890
- }
1891
- return objs;
1892
- }
1893
-
1894
- makeRule(left, right, isForward) {
1895
- let premiseTerm, conclTerm;
1896
-
1897
- if (isForward) {
1898
- premiseTerm = left;
1899
- conclTerm = right;
1900
- } else {
1901
- premiseTerm = right;
1902
- conclTerm = left;
1903
- }
1904
-
1905
- let isFuse = false;
1906
- if (isForward) {
1907
- if (conclTerm instanceof Literal && conclTerm.value === 'false') {
1908
- isFuse = true;
1909
- }
1910
- }
1911
-
1912
- let rawPremise;
1913
- if (premiseTerm instanceof GraphTerm) {
1914
- rawPremise = premiseTerm.triples;
1915
- } else if (premiseTerm instanceof Literal && premiseTerm.value === 'true') {
1916
- rawPremise = [];
1917
- } else {
1918
- rawPremise = [];
1919
- }
1920
-
1921
- let rawConclusion;
1922
- if (conclTerm instanceof GraphTerm) {
1923
- rawConclusion = conclTerm.triples;
1924
- } else if (conclTerm instanceof Literal && conclTerm.value === 'false') {
1925
- rawConclusion = [];
1926
- } else {
1927
- rawConclusion = [];
1928
- }
1929
-
1930
- // Blank nodes that occur explicitly in the head (conclusion)
1931
- const headBlankLabels = collectBlankLabelsInTriples(rawConclusion);
1932
-
1933
- const [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
1934
-
1935
- // Reorder constraints for *forward* rules.
1936
- const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
1937
-
1938
- return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
1939
- }
1940
- }
1941
-
1942
682
  // ===========================================================================
1943
683
  // Blank-node lifting and Skolemization
1944
684
  // ===========================================================================