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.
Files changed (3) hide show
  1. package/README.md +22 -29
  2. package/eyeling.js +166 -19
  3. 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 — `{ ... } => false.`
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
- ## Built-ins (overview)
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
- // Allow a bare blank-node property list statement, e.g. `[ a :Statement ].`
974
- const lastTok = this.toks[this.pos - 1];
975
- if (this.pendingTriples.length > 0 && lastTok && lastTok.typ === 'RBracket') {
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.set('', iri);
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.map[''] || '';
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
- // Allow a bare blank-node property list statement inside a formula, e.g. `{ [ a :X ]. }`
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
- const lastTok = this.toks[this.pos - 1];
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.6.21",
3
+ "version": "1.6.23",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [