eyeling 1.6.21 → 1.6.23
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/README.md +22 -29
- package/eyeling.js +166 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,33 +157,6 @@ The CLI prints only newly derived forward facts.
|
|
|
157
157
|
- the backward prover is **iterative** (explicit stack), so deep chains won’t blow the JS call stack
|
|
158
158
|
- for very deep backward chains, substitutions may be compactified (semantics-preserving) to avoid quadratic “copy a growing substitution object” behavior
|
|
159
159
|
|
|
160
|
-
## Parsing: practical N3 subset
|
|
161
|
-
|
|
162
|
-
Supported:
|
|
163
|
-
|
|
164
|
-
- `@prefix` / `@base`
|
|
165
|
-
- triples with `;` and `,`
|
|
166
|
-
- variables `?x`
|
|
167
|
-
- blank nodes:
|
|
168
|
-
- anonymous `[]`
|
|
169
|
-
- property lists `[ :p :o; :q :r ]`
|
|
170
|
-
- collections `( ... )`
|
|
171
|
-
- quoted formulas `{ ... }`
|
|
172
|
-
- implications:
|
|
173
|
-
- forward rules `{ P } => { C } .`
|
|
174
|
-
- backward rules `{ H } <= { B } .`
|
|
175
|
-
- datatyped literals with `^^`
|
|
176
|
-
- language-tagged string literals: `"hello"@en`, `"colour"@en-GB`
|
|
177
|
-
- long string literals: `"""..."""` (can contain newlines; can also carry a language tag)
|
|
178
|
-
- inverted predicate sugar: `?x <- :p ?y` (swaps subject/object for that predicate)
|
|
179
|
-
- resource paths (forward `!` and reverse `^`): `:joe!:hasAddress!:hasCity "Metropolis".`
|
|
180
|
-
- `#` line comments
|
|
181
|
-
|
|
182
|
-
Non-goals / current limits:
|
|
183
|
-
|
|
184
|
-
- not a full W3C N3 grammar (some edge cases for identifiers, quantifiers, advanced syntax)
|
|
185
|
-
- proof output is local per derived triple (not a global exported proof tree)
|
|
186
|
-
|
|
187
160
|
## Blank nodes and quantification (pragmatic N3/EYE-style)
|
|
188
161
|
|
|
189
162
|
`eyeling` follows the usual N3 intuition:
|
|
@@ -207,7 +180,7 @@ During reasoning:
|
|
|
207
180
|
|
|
208
181
|
- any **derived** `log:implies` / `log:impliedBy` triple with formula subject/object is turned into a new live forward/backward rule.
|
|
209
182
|
|
|
210
|
-
## Inference fuse
|
|
183
|
+
## Inference fuse
|
|
211
184
|
|
|
212
185
|
Rules whose conclusion is `false` are treated as hard failures:
|
|
213
186
|
|
|
@@ -220,7 +193,27 @@ Rules whose conclusion is `false` are treated as hard failures:
|
|
|
220
193
|
|
|
221
194
|
As soon as the premise is provable, `eyeling` exits with status code `2`.
|
|
222
195
|
|
|
223
|
-
##
|
|
196
|
+
## Syntax + built-ins:
|
|
197
|
+
|
|
198
|
+
`eyeling`’s parser targets (nearly) the full *Notation3 Language* grammar from the W3C N3 Community Group:
|
|
199
|
+
https://w3c.github.io/N3/spec/
|
|
200
|
+
|
|
201
|
+
In practice this means: it’s a Turtle superset that also accepts quoted formulas, rules, paths, and the N3 “syntax shorthand”
|
|
202
|
+
operators (`=`, `=>`, `<=`) described in the spec.
|
|
203
|
+
|
|
204
|
+
Commonly used N3/Turtle features:
|
|
205
|
+
|
|
206
|
+
- Prefix/base directives (`@prefix` / `@base`, and SPARQL-style `PREFIX` / `BASE`)
|
|
207
|
+
- Triples with `;` and `,`
|
|
208
|
+
- Variables (`?x`)
|
|
209
|
+
- Blank nodes (`[]`, and `[ :p :o; :q :r ]`)
|
|
210
|
+
- Collections `( ... )`
|
|
211
|
+
- Quoted formulas `{ ... }`
|
|
212
|
+
- Implications (`=>`, `<=`)
|
|
213
|
+
- Datatyped literals (`^^`) and language tags (`"..."@en`)
|
|
214
|
+
- Inverse predicate sugar (`<-` and keyword forms like `is ... of`)
|
|
215
|
+
- Resource paths (`!` and `^`)
|
|
216
|
+
- `#` line comments
|
|
224
217
|
|
|
225
218
|
`eyeling` implements a pragmatic subset of common N3 builtin families and evaluates them during backward goal proving:
|
|
226
219
|
|
package/eyeling.js
CHANGED
|
@@ -759,8 +759,9 @@ function lex(inputText) {
|
|
|
759
759
|
// ===========================================================================
|
|
760
760
|
|
|
761
761
|
class PrefixEnv {
|
|
762
|
-
constructor(map) {
|
|
763
|
-
this.map = map || {}; // prefix -> IRI
|
|
762
|
+
constructor(map, baseIri) {
|
|
763
|
+
this.map = map || {}; // prefix -> IRI (including "" for @prefix :)
|
|
764
|
+
this.baseIri = baseIri || ''; // base IRI for resolving <relative>
|
|
764
765
|
}
|
|
765
766
|
|
|
766
767
|
static newDefault() {
|
|
@@ -774,14 +775,18 @@ class PrefixEnv {
|
|
|
774
775
|
m['list'] = LIST_NS;
|
|
775
776
|
m['time'] = TIME_NS;
|
|
776
777
|
m['genid'] = SKOLEM_NS;
|
|
777
|
-
m[''] = '';
|
|
778
|
-
return new PrefixEnv(m);
|
|
778
|
+
m[''] = ''; // empty prefix default namespace
|
|
779
|
+
return new PrefixEnv(m, ''); // base IRI starts empty
|
|
779
780
|
}
|
|
780
781
|
|
|
781
782
|
set(pref, base) {
|
|
782
783
|
this.map[pref] = base;
|
|
783
784
|
}
|
|
784
785
|
|
|
786
|
+
setBase(baseIri) {
|
|
787
|
+
this.baseIri = baseIri || '';
|
|
788
|
+
}
|
|
789
|
+
|
|
785
790
|
expandQName(q) {
|
|
786
791
|
if (q.includes(':')) {
|
|
787
792
|
const [p, local] = q.split(':', 2);
|
|
@@ -954,6 +959,31 @@ class Parser {
|
|
|
954
959
|
} else if (this.peek().typ === 'AtBase') {
|
|
955
960
|
this.next();
|
|
956
961
|
this.parseBaseDirective();
|
|
962
|
+
} else if (
|
|
963
|
+
// SPARQL-style/Turtle-style directives (case-insensitive, no trailing '.')
|
|
964
|
+
this.peek().typ === 'Ident' &&
|
|
965
|
+
typeof this.peek().value === 'string' &&
|
|
966
|
+
this.peek().value.toLowerCase() === 'prefix' &&
|
|
967
|
+
this.toks[this.pos + 1] &&
|
|
968
|
+
this.toks[this.pos + 1].typ === 'Ident' &&
|
|
969
|
+
typeof this.toks[this.pos + 1].value === 'string' &&
|
|
970
|
+
// Require PNAME_NS form (e.g., "ex:" or ":") to avoid clashing with a normal triple starting with IRI "prefix".
|
|
971
|
+
this.toks[this.pos + 1].value.endsWith(':') &&
|
|
972
|
+
this.toks[this.pos + 2] &&
|
|
973
|
+
(this.toks[this.pos + 2].typ === 'IriRef' || this.toks[this.pos + 2].typ === 'Ident')
|
|
974
|
+
) {
|
|
975
|
+
this.next(); // consume PREFIX keyword
|
|
976
|
+
this.parseSparqlPrefixDirective();
|
|
977
|
+
} else if (
|
|
978
|
+
this.peek().typ === 'Ident' &&
|
|
979
|
+
typeof this.peek().value === 'string' &&
|
|
980
|
+
this.peek().value.toLowerCase() === 'base' &&
|
|
981
|
+
this.toks[this.pos + 1] &&
|
|
982
|
+
// SPARQL BASE requires an IRIREF.
|
|
983
|
+
this.toks[this.pos + 1].typ === 'IriRef'
|
|
984
|
+
) {
|
|
985
|
+
this.next(); // consume BASE keyword
|
|
986
|
+
this.parseSparqlBaseDirective();
|
|
957
987
|
} else {
|
|
958
988
|
const first = this.parseTerm();
|
|
959
989
|
if (this.peek().typ === 'OpImplies') {
|
|
@@ -970,15 +1000,16 @@ class Parser {
|
|
|
970
1000
|
let more;
|
|
971
1001
|
|
|
972
1002
|
if (this.peek().typ === 'Dot') {
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
1003
|
+
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
1004
|
+
// So a bare subject followed by '.' is syntactically valid.
|
|
1005
|
+
// If the subject was a path / property-list that generated helper triples,
|
|
1006
|
+
// we emit those; otherwise this statement contributes no triples.
|
|
1007
|
+
more = [];
|
|
1008
|
+
if (this.pendingTriples.length > 0) {
|
|
976
1009
|
more = this.pendingTriples;
|
|
977
1010
|
this.pendingTriples = [];
|
|
978
|
-
this.next(); // consume '.'
|
|
979
|
-
} else {
|
|
980
|
-
throw new Error(`Unexpected '.' after term; missing predicate/object list`);
|
|
981
1011
|
}
|
|
1012
|
+
this.next(); // consume '.'
|
|
982
1013
|
} else {
|
|
983
1014
|
more = this.parsePredicateObjectList(first);
|
|
984
1015
|
this.expectDot();
|
|
@@ -1020,7 +1051,7 @@ class Parser {
|
|
|
1020
1051
|
const tok2 = this.next();
|
|
1021
1052
|
let iri;
|
|
1022
1053
|
if (tok2.typ === 'IriRef') {
|
|
1023
|
-
iri = tok2.value || '';
|
|
1054
|
+
iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
|
|
1024
1055
|
} else if (tok2.typ === 'Ident') {
|
|
1025
1056
|
iri = this.prefixes.expandQName(tok2.value || '');
|
|
1026
1057
|
} else {
|
|
@@ -1034,14 +1065,57 @@ class Parser {
|
|
|
1034
1065
|
const tok = this.next();
|
|
1035
1066
|
let iri;
|
|
1036
1067
|
if (tok.typ === 'IriRef') {
|
|
1037
|
-
iri = tok.value || '';
|
|
1068
|
+
iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
|
|
1038
1069
|
} else if (tok.typ === 'Ident') {
|
|
1039
1070
|
iri = tok.value || '';
|
|
1040
1071
|
} else {
|
|
1041
1072
|
throw new Error(`Expected IRI after @base, got ${tok.toString()}`);
|
|
1042
1073
|
}
|
|
1043
1074
|
this.expectDot();
|
|
1044
|
-
this.prefixes.
|
|
1075
|
+
this.prefixes.setBase(iri);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
parseSparqlPrefixDirective() {
|
|
1079
|
+
// SPARQL/Turtle-style PREFIX directive: PREFIX pfx: <iri> (no trailing '.')
|
|
1080
|
+
const tok = this.next();
|
|
1081
|
+
if (tok.typ !== 'Ident') {
|
|
1082
|
+
throw new Error(`Expected prefix name after PREFIX, got ${tok.toString()}`);
|
|
1083
|
+
}
|
|
1084
|
+
const pref = tok.value || '';
|
|
1085
|
+
const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
|
|
1086
|
+
|
|
1087
|
+
const tok2 = this.next();
|
|
1088
|
+
let iri;
|
|
1089
|
+
if (tok2.typ === 'IriRef') {
|
|
1090
|
+
iri = resolveIriRef(tok2.value || '', this.prefixes.baseIri || '');
|
|
1091
|
+
} else if (tok2.typ === 'Ident') {
|
|
1092
|
+
iri = this.prefixes.expandQName(tok2.value || '');
|
|
1093
|
+
} else {
|
|
1094
|
+
throw new Error(`Expected IRI after PREFIX, got ${tok2.toString()}`);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// N3/Turtle: PREFIX directives do not have a trailing '.', but accept it permissively.
|
|
1098
|
+
if (this.peek().typ === 'Dot') this.next();
|
|
1099
|
+
|
|
1100
|
+
this.prefixes.set(prefName, iri);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
parseSparqlBaseDirective() {
|
|
1104
|
+
// SPARQL/Turtle-style BASE directive: BASE <iri> (no trailing '.')
|
|
1105
|
+
const tok = this.next();
|
|
1106
|
+
let iri;
|
|
1107
|
+
if (tok.typ === 'IriRef') {
|
|
1108
|
+
iri = resolveIriRef(tok.value || '', this.prefixes.baseIri || '');
|
|
1109
|
+
} else if (tok.typ === 'Ident') {
|
|
1110
|
+
iri = tok.value || '';
|
|
1111
|
+
} else {
|
|
1112
|
+
throw new Error(`Expected IRI after BASE, got ${tok.toString()}`);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// N3/Turtle: BASE directives do not have a trailing '.', but accept it permissively.
|
|
1116
|
+
if (this.peek().typ === 'Dot') this.next();
|
|
1117
|
+
|
|
1118
|
+
this.prefixes.setBase(iri);
|
|
1045
1119
|
}
|
|
1046
1120
|
|
|
1047
1121
|
parseTerm() {
|
|
@@ -1072,7 +1146,7 @@ class Parser {
|
|
|
1072
1146
|
}
|
|
1073
1147
|
|
|
1074
1148
|
if (typ === 'IriRef') {
|
|
1075
|
-
const base = this.prefixes.
|
|
1149
|
+
const base = this.prefixes.baseIri || '';
|
|
1076
1150
|
return internIri(resolveIriRef(val || '', base));
|
|
1077
1151
|
}
|
|
1078
1152
|
if (typ === 'Ident') {
|
|
@@ -1150,6 +1224,66 @@ class Parser {
|
|
|
1150
1224
|
return new Blank(`_:b${this.blankCounter}`);
|
|
1151
1225
|
}
|
|
1152
1226
|
|
|
1227
|
+
// IRI property list: [ id <IRI> predicateObjectList? ]
|
|
1228
|
+
// Lets you embed descriptions of an IRI directly in object position.
|
|
1229
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'id') {
|
|
1230
|
+
this.next(); // consume 'id'
|
|
1231
|
+
const iriTerm = this.parseTerm();
|
|
1232
|
+
|
|
1233
|
+
// N3 note: 'id' form is not meant to be used with blank node identifiers.
|
|
1234
|
+
if (iriTerm instanceof Blank && iriTerm.label.startsWith('_:')) {
|
|
1235
|
+
throw new Error("Cannot use 'id' keyword with a blank node identifier inside [...]");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Optional ';' right after the id IRI (tolerated).
|
|
1239
|
+
if (this.peek().typ === 'Semicolon') this.next();
|
|
1240
|
+
|
|
1241
|
+
// Empty IRI property list: [ id :iri ]
|
|
1242
|
+
if (this.peek().typ === 'RBracket') {
|
|
1243
|
+
this.next();
|
|
1244
|
+
return iriTerm;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const subj = iriTerm;
|
|
1248
|
+
while (true) {
|
|
1249
|
+
let pred;
|
|
1250
|
+
let invert = false;
|
|
1251
|
+
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
1252
|
+
this.next();
|
|
1253
|
+
pred = internIri(RDF_NS + 'type');
|
|
1254
|
+
} else if (this.peek().typ === 'OpPredInvert') {
|
|
1255
|
+
this.next(); // "<-"
|
|
1256
|
+
pred = this.parseTerm();
|
|
1257
|
+
invert = true;
|
|
1258
|
+
} else {
|
|
1259
|
+
pred = this.parseTerm();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const objs = [this.parseTerm()];
|
|
1263
|
+
while (this.peek().typ === 'Comma') {
|
|
1264
|
+
this.next();
|
|
1265
|
+
objs.push(this.parseTerm());
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
for (const o of objs) {
|
|
1269
|
+
this.pendingTriples.push(invert ? new Triple(o, pred, subj) : new Triple(subj, pred, o));
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (this.peek().typ === 'Semicolon') {
|
|
1273
|
+
this.next();
|
|
1274
|
+
if (this.peek().typ === 'RBracket') break;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if (this.peek().typ !== 'RBracket') {
|
|
1281
|
+
throw new Error(`Expected ']' at end of IRI property list, got ${JSON.stringify(this.peek())}`);
|
|
1282
|
+
}
|
|
1283
|
+
this.next();
|
|
1284
|
+
return iriTerm;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1153
1287
|
// [ predicateObjectList ]
|
|
1154
1288
|
this.blankCounter += 1;
|
|
1155
1289
|
const id = `_:b${this.blankCounter}`;
|
|
@@ -1225,15 +1359,15 @@ class Parser {
|
|
|
1225
1359
|
throw new Error(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
1226
1360
|
}
|
|
1227
1361
|
} else {
|
|
1228
|
-
//
|
|
1362
|
+
// N3 grammar allows: triples ::= subject predicateObjectList?
|
|
1363
|
+
// So a bare subject (optionally producing helper triples) is allowed inside formulas as well.
|
|
1229
1364
|
if (this.peek().typ === 'Dot' || this.peek().typ === 'RBrace') {
|
|
1230
|
-
|
|
1231
|
-
if (this.pendingTriples.length > 0 && lastTok && lastTok.typ === 'RBracket') {
|
|
1365
|
+
if (this.pendingTriples.length > 0) {
|
|
1232
1366
|
triples.push(...this.pendingTriples);
|
|
1233
1367
|
this.pendingTriples = [];
|
|
1234
|
-
if (this.peek().typ === 'Dot') this.next();
|
|
1235
|
-
continue;
|
|
1236
1368
|
}
|
|
1369
|
+
if (this.peek().typ === 'Dot') this.next();
|
|
1370
|
+
continue;
|
|
1237
1371
|
}
|
|
1238
1372
|
|
|
1239
1373
|
triples.push(...this.parsePredicateObjectList(left));
|
|
@@ -1265,6 +1399,19 @@ class Parser {
|
|
|
1265
1399
|
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
1266
1400
|
this.next();
|
|
1267
1401
|
verb = internIri(RDF_NS + 'type');
|
|
1402
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
1403
|
+
// N3 syntactic sugar: "S has P O." means "S P O."
|
|
1404
|
+
this.next(); // consume "has"
|
|
1405
|
+
verb = this.parseTerm();
|
|
1406
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
1407
|
+
// N3 syntactic sugar: "S is P of O." means "O P S." (inverse; equivalent to "<-")
|
|
1408
|
+
this.next(); // consume "is"
|
|
1409
|
+
verb = this.parseTerm();
|
|
1410
|
+
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
1411
|
+
throw new Error(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
1412
|
+
}
|
|
1413
|
+
this.next(); // consume "of"
|
|
1414
|
+
invert = true;
|
|
1268
1415
|
} else if (this.peek().typ === 'OpPredInvert') {
|
|
1269
1416
|
this.next(); // "<-"
|
|
1270
1417
|
verb = this.parseTerm();
|