eyeling 1.6.33 → 1.7.1

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 CHANGED
@@ -99,13 +99,15 @@ npm run test:packlist
99
99
  ### Usage
100
100
 
101
101
  ```
102
- Usage: eyeling.js [options] <file.n3>
102
+ Usage: eyeling [options] <file.n3>
103
103
 
104
104
  Options:
105
105
  -h, --help Show this help and exit.
106
106
  -v, --version Print version and exit.
107
107
  -p, --proof-comments Enable proof explanations.
108
108
  -n, --no-proof-comments Disable proof explanations (default).
109
+ -s, --super-restricted Disable all builtins except => and <=.
110
+ -a, --ast Print parsed AST as JSON and exit.
109
111
  ```
110
112
 
111
113
  By default, `eyeling`:
@@ -1,7 +1,7 @@
1
1
  # alignment-demo.n3
2
2
  # Minimal alignment example (SKOS mappings + inferred roll-up to a reference concept)
3
3
 
4
- @prefix ex: <http://example.org/> .
4
+ @prefix ex: <http://example.org/#> .
5
5
  @prefix ref: <http://example.org/taxonomy/ref/> .
6
6
  @prefix tel: <http://example.org/taxonomy/telraam/> .
7
7
  @prefix anpr: <http://example.org/taxonomy/anpr/> .
@@ -0,0 +1,7 @@
1
+ @prefix : <http://example/#> .
2
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
3
+ @prefix string: <http://www.w3.org/2000/10/swap/string#> .
4
+
5
+ :phayes a :Person ; :givenName "Pat"; :familyName "Hayes" .
6
+
7
+ { ?x a :Person . ?SCOPE log:notIncludes { ?x :name ?someName . } . ?x :givenName ?name1 . ?x :familyName ?name2 . (?name1 " " ?name2) string:concatenation ?FN . } => { ?x :name ?FN } .
@@ -0,0 +1,12 @@
1
+ PREFIX : <http://example/#>
2
+
3
+ DATA { :phayes a :Person ; :givenName "Pat"; :familyName "Hayes" . }
4
+
5
+ # Default value - calculate a name
6
+ RULE { ?x :name ?FN } WHERE {
7
+ ?x a :Person
8
+ NOT { ?x :name ?someName }
9
+ ?x :givenName ?name1 ;
10
+ :familyName ?name2 .
11
+ BIND(concat(?name1, " ", ?name2) AS ?FN)
12
+ }
@@ -0,0 +1,10 @@
1
+ @prefix : <http://example.org/#> .
2
+
3
+ :A :fatherOf :X . :B :motherOf :X . :C :motherOf :A .
4
+
5
+ { ?y :fatherOf ?x . } => { ?x :childOf ?y } .
6
+ { ?y :motherOf ?x . } => { ?x :childOf ?y } .
7
+ { ?x :childOf ?y . } => { ?x :descendedFrom ?y } .
8
+ { ?x :childOf ?z . ?z :childOf ?y . } => { ?x :descendedFrom ?y } .
9
+ { ?y :descendedFrom ?x . } => { ?x :ancestorOf ?y } .
10
+ { ?a :ancestorOf ?c . ?c :ancestorOf ?b . } => { ?a :ancestorOf ?b } .
@@ -0,0 +1,12 @@
1
+ PREFIX : <http://example.org/#>
2
+
3
+ DATA { :A :fatherOf :X . :B :motherOf :X . :C :motherOf :A . }
4
+
5
+ RULE { ?x :childOf ?y } WHERE { ?y :fatherOf ?x }
6
+ RULE { ?x :childOf ?y } WHERE { ?y :motherOf ?x }
7
+
8
+ RULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?y }
9
+ RULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?z . ?z :childOf ?y }
10
+
11
+ RULE { ?x :ancestorOf ?y } WHERE { ?y :descendedFrom ?x }
12
+ RULE { ?a :ancestorOf ?b } WHERE { ?a :ancestorOf ?c . ?c :ancestorOf ?b }
@@ -0,0 +1,8 @@
1
+ @prefix : <http://example/#> .
2
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
3
+
4
+ :x :p 1 ; :q 2 .
5
+
6
+ { ?x :p ?v1 . ?x :q ?v2 . ?v1 math:greaterThan 0 . ?v2 math:greaterThan 0 . } => { ?x :bothPositive true . } .
7
+ { ?x :p ?v1 . ?x :q ?v2 . ?v1 math:equalTo 0 . } => { ?x :oneIsZero true . } .
8
+ { ?x :p ?v1 . ?x :q ?v2 . ?v2 math:equalTo 0 . } => { ?x :oneIsZero true . } .
@@ -0,0 +1,9 @@
1
+ PREFIX : <http://example/#>
2
+
3
+ DATA { :x :p 1 ; :q 2 . }
4
+
5
+ RULE { ?x :bothPositive true . }
6
+ WHERE { ?x :p ?v1 FILTER ( ?v1 > 0 ) ?x :q ?v2 FILTER ( ?v2 > 0 ) }
7
+
8
+ RULE { ?x :oneIsZero true . }
9
+ WHERE { ?x :p ?v1 ; :q ?v2 FILTER ( ( ?v1 = 0 ) || ( ?v2 = 0 ) ) }
@@ -7,7 +7,7 @@
7
7
  @prefix string: <http://www.w3.org/2000/10/swap/string#> .
8
8
  @prefix list: <http://www.w3.org/2000/10/swap/list#> .
9
9
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
10
- @prefix ex: <http://example.org/> .
10
+ @prefix ex: <http://example.org/#> .
11
11
 
12
12
  ex:doc ex:json """{
13
13
  "users": [
@@ -9,7 +9,7 @@
9
9
  @prefix math: <http://www.w3.org/2000/10/swap/math#> .
10
10
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
11
11
  @prefix owl: <http://www.w3.org/2002/07/owl#> .
12
- @prefix ex: <http://example.org/> .
12
+ @prefix ex: <http://example.org/#> .
13
13
 
14
14
  # ------------------------------------------------------------------
15
15
  # Master data (canonical N3 graph)
@@ -2,7 +2,7 @@
2
2
  # log:collectAllIn example
3
3
  # ========================
4
4
 
5
- @prefix : <http://example.org/>.
5
+ @prefix : <http://example.org/#>.
6
6
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
7
7
  @prefix string: <http://www.w3.org/2000/10/swap/string#> .
8
8
 
@@ -2,7 +2,7 @@
2
2
  # log:forAllIn example
3
3
  # ====================
4
4
 
5
- @prefix : <http://example.org/>.
5
+ @prefix : <http://example.org/#>.
6
6
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
7
7
 
8
8
  :c a :CompositeTask ;
@@ -2,7 +2,7 @@
2
2
  # log:notIncludes example
3
3
  # =======================
4
4
 
5
- @prefix : <http://example.org/> .
5
+ @prefix : <http://example.org/#> .
6
6
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
7
7
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
8
8
  @prefix math: <http://www.w3.org/2000/10/swap/math#> .
@@ -2,7 +2,7 @@
2
2
  # log:skolem example
3
3
  # ==================
4
4
 
5
- @prefix : <http://example.org/> .
5
+ @prefix : <http://example.org/#> .
6
6
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
7
7
 
8
8
  {
@@ -2,7 +2,7 @@
2
2
  # log:uri example
3
3
  # ===============
4
4
 
5
- @prefix : <http://example.org/> .
5
+ @prefix : <http://example.org/#> .
6
6
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
7
7
 
8
8
  {
@@ -14,7 +14,7 @@
14
14
  # eyeling minimal-skos-alignment.n3
15
15
  # ------------------------------------------------------------
16
16
 
17
- @prefix ex: <http://example.org/> .
17
+ @prefix ex: <http://example.org/#> .
18
18
  @prefix ref: <http://example.org/ref/> .
19
19
  @prefix anpr: <http://example.org/anpr/> .
20
20
  @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@@ -1,5 +1,5 @@
1
1
  @prefix anpr: <http://example.org/taxonomy/anpr/> .
2
- @prefix ex: <http://example.org/> .
2
+ @prefix ex: <http://example.org/#> .
3
3
  @prefix ref: <http://example.org/taxonomy/ref/> .
4
4
  @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
5
5
  @prefix tel: <http://example.org/taxonomy/telraam/> .
@@ -0,0 +1,3 @@
1
+ @prefix : <http://example/#> .
2
+
3
+ :phayes :name "Pat Hayes" .
@@ -0,0 +1,13 @@
1
+ @prefix : <http://example.org/#> .
2
+
3
+ :X :childOf :A .
4
+ :X :childOf :B .
5
+ :A :childOf :C .
6
+ :X :descendedFrom :A .
7
+ :X :descendedFrom :B .
8
+ :A :descendedFrom :C .
9
+ :X :descendedFrom :C .
10
+ :A :ancestorOf :X .
11
+ :B :ancestorOf :X .
12
+ :C :ancestorOf :A .
13
+ :C :ancestorOf :X .
@@ -0,0 +1,3 @@
1
+ @prefix : <http://example/#> .
2
+
3
+ :x :bothPositive true .
@@ -1,4 +1,4 @@
1
- @prefix ex: <http://example.org/> .
1
+ @prefix ex: <http://example.org/#> .
2
2
 
3
3
  ex:checks ex:firstUserNameOk true .
4
4
  <urn:example:user:u1> a ex:AllowedUser .
@@ -1,4 +1,4 @@
1
- @prefix ex: <http://example.org/> .
1
+ @prefix ex: <http://example.org/#> .
2
2
  @prefix genid: <https://eyereasoner.github.io/.well-known/genid/> .
3
3
  @prefix owl: <http://www.w3.org/2002/07/owl#> .
4
4
 
@@ -1,4 +1,4 @@
1
- @prefix : <http://example.org/> .
1
+ @prefix : <http://example.org/#> .
2
2
 
3
3
  :result1 :is ("Huey" "Dewey" "Louie") .
4
4
  :result2 :is (("Huey") ("Dewey") ("Louie")) .
@@ -1,3 +1,3 @@
1
- @prefix : <http://example.org/> .
1
+ @prefix : <http://example.org/#> .
2
2
 
3
3
  :result :is true .
@@ -1,4 +1,4 @@
1
- @prefix : <http://example.org/> .
1
+ @prefix : <http://example.org/#> .
2
2
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
3
3
 
4
4
  :x :y 1 .
@@ -1,4 +1,4 @@
1
- @prefix : <http://example.org/> .
1
+ @prefix : <http://example.org/#> .
2
2
  @prefix genid: <https://eyereasoner.github.io/.well-known/genid/> .
3
3
 
4
- :Result :skolem genid:0c562a90-a44f-264a-86e5-9664948aa178 .
4
+ :Result :skolem genid:32007e38-bd28-5f18-4712-ae1df708a5d4 .
@@ -1,4 +1,4 @@
1
- @prefix : <http://example.org/> .
1
+ @prefix : <http://example.org/#> .
2
2
 
3
3
  :uriString :is "https://www.w3.org" .
4
4
  :uriResource :is <https://www.w3.org> .
@@ -1,5 +1,5 @@
1
1
  @prefix anpr: <http://example.org/anpr/> .
2
- @prefix ex: <http://example.org/> .
2
+ @prefix ex: <http://example.org/#> .
3
3
  @prefix ref: <http://example.org/ref/> .
4
4
 
5
5
  anpr:VehicleWithPlate ex:treatedAs ref:Car .
@@ -1,3 +1,3 @@
1
- @prefix : <http://example.org/> .
1
+ @prefix : <http://example.org/#> .
2
2
 
3
3
  :Alice :hates :Nobody .
@@ -1,5 +1,5 @@
1
1
  @prefix anpr: <http://example.org/taxonomy/anpr/> .
2
- @prefix ex: <http://example.org/> .
2
+ @prefix ex: <http://example.org/#> .
3
3
  @prefix genid: <https://eyereasoner.github.io/.well-known/genid/> .
4
4
  @prefix prov: <http://www.w3.org/ns/prov#> .
5
5
  @prefix ref: <http://example.org/taxonomy/ref/> .
@@ -130,18 +130,18 @@ anpr:PassengerCar ex:narrowerOrEqualOf ref:RoadUser .
130
130
  anpr:Van ex:narrowerOrEqualOf ref:RoadUser .
131
131
  anpr:Truck ex:narrowerOrEqualOf ref:RoadUser .
132
132
  anpr:Bus ex:narrowerOrEqualOf ref:RoadUser .
133
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc a sosa:Observation .
134
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc a ex:AggregatedObservation .
135
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc sosa:observedProperty ref:Car .
136
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc sosa:hasFeatureOfInterest ex:segment1 .
137
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc sosa:resultTime "2025-12-23T08:00:00Z"^^xsd:dateTime .
138
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc sosa:hasSimpleResult 22 .
139
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc ex:basedOnRequest ex:request_cars_at_t1 .
140
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc ex:contributingValues (10 2 8 1 1 0) .
141
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc ex:contributingObservations (ex:obs_tel_car ex:obs_tel_heavy ex:obs_anpr_passenger ex:obs_anpr_van ex:obs_anpr_truck ex:obs_anpr_bus) .
142
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc prov:wasDerivedFrom ex:obs_tel_car .
143
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc prov:wasDerivedFrom ex:obs_tel_heavy .
144
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc prov:wasDerivedFrom ex:obs_anpr_passenger .
145
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc prov:wasDerivedFrom ex:obs_anpr_van .
146
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc prov:wasDerivedFrom ex:obs_anpr_truck .
147
- genid:5d9446e0-5075-531c-f72b-7fb8f210a4dc prov:wasDerivedFrom ex:obs_anpr_bus .
133
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 a sosa:Observation .
134
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 a ex:AggregatedObservation .
135
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 sosa:observedProperty ref:Car .
136
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 sosa:hasFeatureOfInterest ex:segment1 .
137
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 sosa:resultTime "2025-12-23T08:00:00Z"^^xsd:dateTime .
138
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 sosa:hasSimpleResult 22 .
139
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 ex:basedOnRequest ex:request_cars_at_t1 .
140
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 ex:contributingValues (10 2 8 1 1 0) .
141
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 ex:contributingObservations (ex:obs_tel_car ex:obs_tel_heavy ex:obs_anpr_passenger ex:obs_anpr_van ex:obs_anpr_truck ex:obs_anpr_bus) .
142
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 prov:wasDerivedFrom ex:obs_tel_car .
143
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 prov:wasDerivedFrom ex:obs_tel_heavy .
144
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 prov:wasDerivedFrom ex:obs_anpr_passenger .
145
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 prov:wasDerivedFrom ex:obs_anpr_van .
146
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 prov:wasDerivedFrom ex:obs_anpr_truck .
147
+ genid:baddfb7c-3e49-3ca8-bc4f-6de82f22dc60 prov:wasDerivedFrom ex:obs_anpr_bus .
package/examples/snaf.n3 CHANGED
@@ -3,7 +3,7 @@
3
3
  # ==========================
4
4
 
5
5
  @prefix log: <http://www.w3.org/2000/10/swap/log#>.
6
- @prefix : <http://example.org/>.
6
+ @prefix : <http://example.org/#>.
7
7
 
8
8
  :Alice :loves :Bob.
9
9
  :Bob a :Person.
@@ -0,0 +1,6 @@
1
+ PREFIX log: <http://www.w3.org/2000/10/swap/log#>
2
+ PREFIX : <http://example.org/#>
3
+
4
+ DATA { :Alice :loves :Bob. :Bob a :Person. }
5
+
6
+ RULE { :Alice :hates :Nobody. } WHERE { NOT { :Alice :hates ?X } ?X a :Person. }
@@ -10,7 +10,7 @@
10
10
  # or:
11
11
  # node eyeling.js traffic-skos-aggregate.n3
12
12
 
13
- @prefix ex: <http://example.org/> .
13
+ @prefix ex: <http://example.org/#> .
14
14
  @prefix ref: <http://example.org/taxonomy/ref/> .
15
15
  @prefix tel: <http://example.org/taxonomy/telraam/> .
16
16
  @prefix anpr: <http://example.org/taxonomy/anpr/> .
package/eyeling.js CHANGED
@@ -84,6 +84,8 @@ const jsonPointerCache = new Map();
84
84
 
85
85
  // Controls whether human-readable proof comments are printed.
86
86
  let proofCommentsEnabled = false;
87
+ // Super restricted mode: disable *all* builtins except => / <= (log:implies / log:impliedBy)
88
+ let superRestrictedMode = false;
87
89
 
88
90
  // ----------------------------------------------------------------------------
89
91
  // Deterministic time support
@@ -313,13 +315,24 @@ class DerivedFact {
313
315
  // ===========================================================================
314
316
 
315
317
  class Token {
316
- constructor(typ, value = null) {
318
+ constructor(typ, value = null, offset = null) {
317
319
  this.typ = typ;
318
320
  this.value = value;
321
+ // Codepoint offset in the original source (Array.from(text) index).
322
+ this.offset = offset;
319
323
  }
320
324
  toString() {
321
- if (this.value == null) return `Token(${this.typ})`;
322
- return `Token(${this.typ}, ${JSON.stringify(this.value)})`;
325
+ const loc = typeof this.offset === 'number' ? `@${this.offset}` : '';
326
+ if (this.value == null) return `Token(${this.typ}${loc})`;
327
+ return `Token(${this.typ}${loc}, ${JSON.stringify(this.value)})`;
328
+ }
329
+ }
330
+
331
+ class N3SyntaxError extends SyntaxError {
332
+ constructor(message, offset = null) {
333
+ super(message);
334
+ this.name = 'N3SyntaxError';
335
+ this.offset = offset;
323
336
  }
324
337
  }
325
338
 
@@ -432,12 +445,12 @@ function lex(inputText) {
432
445
  // 3) Two-character operators: => and <=
433
446
  if (c === '=') {
434
447
  if (peek(1) === '>') {
435
- tokens.push(new Token('OpImplies'));
448
+ tokens.push(new Token('OpImplies', null, i));
436
449
  i += 2;
437
450
  continue;
438
451
  } else {
439
452
  // N3 syntactic sugar: '=' means owl:sameAs
440
- tokens.push(new Token('Equals'));
453
+ tokens.push(new Token('Equals', null, i));
441
454
  i += 1;
442
455
  continue;
443
456
  }
@@ -445,17 +458,18 @@ function lex(inputText) {
445
458
 
446
459
  if (c === '<') {
447
460
  if (peek(1) === '=') {
448
- tokens.push(new Token('OpImpliedBy'));
461
+ tokens.push(new Token('OpImpliedBy', null, i));
449
462
  i += 2;
450
463
  continue;
451
464
  }
452
465
  // N3 predicate inversion: "<-" (swap subject/object for this predicate)
453
466
  if (peek(1) === '-') {
454
- tokens.push(new Token('OpPredInvert'));
467
+ tokens.push(new Token('OpPredInvert', null, i));
455
468
  i += 2;
456
469
  continue;
457
470
  }
458
471
  // Otherwise IRIREF <...>
472
+ const start = i;
459
473
  i++; // skip '<'
460
474
  const iriChars = [];
461
475
  while (i < n && chars[i] !== '>') {
@@ -463,27 +477,27 @@ function lex(inputText) {
463
477
  i++;
464
478
  }
465
479
  if (i >= n || chars[i] !== '>') {
466
- throw new Error('Unterminated IRI <...>');
480
+ throw new N3SyntaxError('Unterminated IRI <...>', start);
467
481
  }
468
482
  i++; // skip '>'
469
483
  const iri = iriChars.join('');
470
- tokens.push(new Token('IriRef', iri));
484
+ tokens.push(new Token('IriRef', iri, start));
471
485
  continue;
472
486
  }
473
487
 
474
488
  // 4) Path + datatype operators: !, ^, ^^
475
489
  if (c === '!') {
476
- tokens.push(new Token('OpPathFwd'));
490
+ tokens.push(new Token('OpPathFwd', null, i));
477
491
  i += 1;
478
492
  continue;
479
493
  }
480
494
  if (c === '^') {
481
495
  if (peek(1) === '^') {
482
- tokens.push(new Token('HatHat'));
496
+ tokens.push(new Token('HatHat', null, i));
483
497
  i += 2;
484
498
  continue;
485
499
  }
486
- tokens.push(new Token('OpPathRev'));
500
+ tokens.push(new Token('OpPathRev', null, i));
487
501
  i += 1;
488
502
  continue;
489
503
  }
@@ -501,13 +515,15 @@ function lex(inputText) {
501
515
  ',': 'Comma',
502
516
  '.': 'Dot',
503
517
  };
504
- tokens.push(new Token(mapping[c]));
518
+ tokens.push(new Token(mapping[c], null, i));
505
519
  i++;
506
520
  continue;
507
521
  }
508
522
 
509
523
  // String literal: short "..." or long """..."""
510
524
  if (c === '"') {
525
+ const start = i;
526
+
511
527
  // Long string literal """ ... """
512
528
  if (peek(1) === '"' && peek(2) === '"') {
513
529
  i += 3; // consume opening """
@@ -551,11 +567,11 @@ function lex(inputText) {
551
567
  sChars.push(cc);
552
568
  i++;
553
569
  }
554
- if (!closed) throw new Error('Unterminated long string literal """..."""');
570
+ if (!closed) throw new N3SyntaxError('Unterminated long string literal """..."""', start);
555
571
  const raw = '"""' + sChars.join('') + '"""';
556
572
  const decoded = decodeN3StringEscapes(stripQuotes(raw));
557
573
  const s = JSON.stringify(decoded); // canonical short quoted form
558
- tokens.push(new Token('Literal', s));
574
+ tokens.push(new Token('Literal', s, start));
559
575
  continue;
560
576
  }
561
577
 
@@ -580,12 +596,14 @@ function lex(inputText) {
580
596
  const raw = '"' + sChars.join('') + '"';
581
597
  const decoded = decodeN3StringEscapes(stripQuotes(raw));
582
598
  const s = JSON.stringify(decoded); // canonical short quoted form
583
- tokens.push(new Token('Literal', s));
599
+ tokens.push(new Token('Literal', s, start));
584
600
  continue;
585
601
  }
586
602
 
587
603
  // String literal: short '...' or long '''...'''
588
604
  if (c === "'") {
605
+ const start = i;
606
+
589
607
  // Long string literal ''' ... '''
590
608
  if (peek(1) === "'" && peek(2) === "'") {
591
609
  i += 3; // consume opening '''
@@ -629,11 +647,11 @@ function lex(inputText) {
629
647
  sChars.push(cc);
630
648
  i++;
631
649
  }
632
- if (!closed) throw new Error("Unterminated long string literal '''...'''");
650
+ if (!closed) throw new N3SyntaxError("Unterminated long string literal '''...'''", start);
633
651
  const raw = "'''" + sChars.join('') + "'''";
634
652
  const decoded = decodeN3StringEscapes(stripQuotes(raw));
635
653
  const s = JSON.stringify(decoded); // canonical short quoted form
636
- tokens.push(new Token('Literal', s));
654
+ tokens.push(new Token('Literal', s, start));
637
655
  continue;
638
656
  }
639
657
 
@@ -658,12 +676,13 @@ function lex(inputText) {
658
676
  const raw = "'" + sChars.join('') + "'";
659
677
  const decoded = decodeN3StringEscapes(stripQuotes(raw));
660
678
  const s = JSON.stringify(decoded); // canonical short quoted form
661
- tokens.push(new Token('Literal', s));
679
+ tokens.push(new Token('Literal', s, start));
662
680
  continue;
663
681
  }
664
682
 
665
683
  // Variable ?name
666
684
  if (c === '?') {
685
+ const start = i;
667
686
  i++;
668
687
  const nameChars = [];
669
688
  let cc;
@@ -672,12 +691,13 @@ function lex(inputText) {
672
691
  i++;
673
692
  }
674
693
  const name = nameChars.join('');
675
- tokens.push(new Token('Var', name));
694
+ tokens.push(new Token('Var', name, start));
676
695
  continue;
677
696
  }
678
697
 
679
698
  // Directives: @prefix, @base (and language tags after string literals)
680
699
  if (c === '@') {
700
+ const start = i;
681
701
  const prevTok = tokens.length ? tokens[tokens.length - 1] : null;
682
702
  const prevWasQuotedLiteral =
683
703
  prevTok && prevTok.typ === 'Literal' && typeof prevTok.value === 'string' && prevTok.value.startsWith('"');
@@ -690,7 +710,7 @@ function lex(inputText) {
690
710
  const tagChars = [];
691
711
  let cc = peek();
692
712
  if (cc === null || !/[A-Za-z]/.test(cc)) {
693
- throw new Error("Invalid language tag (expected [A-Za-z] after '@')");
713
+ throw new N3SyntaxError("Invalid language tag (expected [A-Za-z] after '@')", start);
694
714
  }
695
715
  while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
696
716
  tagChars.push(cc);
@@ -705,11 +725,11 @@ function lex(inputText) {
705
725
  i++;
706
726
  }
707
727
  if (!segChars.length) {
708
- throw new Error("Invalid language tag (expected [A-Za-z0-9]+ after '-')");
728
+ throw new N3SyntaxError("Invalid language tag (expected [A-Za-z0-9]+ after '-')", start);
709
729
  }
710
730
  tagChars.push(...segChars);
711
731
  }
712
- tokens.push(new Token('LangTag', tagChars.join('')));
732
+ tokens.push(new Token('LangTag', tagChars.join(''), start));
713
733
  continue;
714
734
  }
715
735
 
@@ -721,14 +741,15 @@ function lex(inputText) {
721
741
  i++;
722
742
  }
723
743
  const word = wordChars.join('');
724
- if (word === 'prefix') tokens.push(new Token('AtPrefix'));
725
- else if (word === 'base') tokens.push(new Token('AtBase'));
726
- else throw new Error(`Unknown directive @${word}`);
744
+ if (word === 'prefix') tokens.push(new Token('AtPrefix', null, start));
745
+ else if (word === 'base') tokens.push(new Token('AtBase', null, start));
746
+ else throw new N3SyntaxError(`Unknown directive @${word}`, start);
727
747
  continue;
728
748
  }
729
749
 
730
750
  // 6) Numeric literal (integer or float)
731
751
  if (/[0-9]/.test(c) || (c === '-' && peek(1) !== null && /[0-9]/.test(peek(1)))) {
752
+ const start = i;
732
753
  const numChars = [c];
733
754
  i++;
734
755
  while (i < n) {
@@ -768,11 +789,12 @@ function lex(inputText) {
768
789
  }
769
790
  }
770
791
 
771
- tokens.push(new Token('Literal', numChars.join('')));
792
+ tokens.push(new Token('Literal', numChars.join(''), start));
772
793
  continue;
773
794
  }
774
795
 
775
796
  // 7) Identifiers / keywords / QNames
797
+ const start = i;
776
798
  const wordChars = [];
777
799
  let cc;
778
800
  while ((cc = peek()) !== null && isNameChar(cc)) {
@@ -780,19 +802,19 @@ function lex(inputText) {
780
802
  i++;
781
803
  }
782
804
  if (!wordChars.length) {
783
- throw new Error(`Unexpected char: ${JSON.stringify(c)}`);
805
+ throw new N3SyntaxError(`Unexpected char: ${JSON.stringify(c)}`, i);
784
806
  }
785
807
  const word = wordChars.join('');
786
808
  if (word === 'true' || word === 'false') {
787
- tokens.push(new Token('Literal', word));
809
+ tokens.push(new Token('Literal', word, start));
788
810
  } else if ([...word].every((ch) => /[0-9.\-]/.test(ch))) {
789
- tokens.push(new Token('Literal', word));
811
+ tokens.push(new Token('Literal', word, start));
790
812
  } else {
791
- tokens.push(new Token('Ident', word));
813
+ tokens.push(new Token('Ident', word, start));
792
814
  }
793
815
  }
794
816
 
795
- tokens.push(new Token('EOF'));
817
+ tokens.push(new Token('EOF', null, n));
796
818
  return tokens;
797
819
  }
798
820
 
@@ -982,10 +1004,15 @@ class Parser {
982
1004
  return tok;
983
1005
  }
984
1006
 
1007
+ fail(message, tok = this.peek()) {
1008
+ const off = tok && typeof tok.offset === 'number' ? tok.offset : null;
1009
+ throw new N3SyntaxError(message, off);
1010
+ }
1011
+
985
1012
  expectDot() {
986
1013
  const tok = this.next();
987
1014
  if (tok.typ !== 'Dot') {
988
- throw new Error(`Expected '.', got ${tok.toString()}`);
1015
+ this.fail(`Expected '.', got ${tok.toString()}`, tok);
989
1016
  }
990
1017
  }
991
1018
 
@@ -1077,7 +1104,7 @@ class Parser {
1077
1104
  parsePrefixDirective() {
1078
1105
  const tok = this.next();
1079
1106
  if (tok.typ !== 'Ident') {
1080
- throw new Error(`Expected prefix name, got ${tok.toString()}`);
1107
+ this.fail(`Expected prefix name, got ${tok.toString()}`, tok);
1081
1108
  }
1082
1109
  const pref = tok.value || '';
1083
1110
  const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
@@ -1097,7 +1124,7 @@ class Parser {
1097
1124
  } else if (tok2.typ === 'Ident') {
1098
1125
  iri = this.prefixes.expandQName(tok2.value || '');
1099
1126
  } else {
1100
- throw new Error(`Expected IRI after @prefix, got ${tok2.toString()}`);
1127
+ this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
1101
1128
  }
1102
1129
  this.expectDot();
1103
1130
  this.prefixes.set(prefName, iri);
@@ -1111,7 +1138,7 @@ class Parser {
1111
1138
  } else if (tok.typ === 'Ident') {
1112
1139
  iri = tok.value || '';
1113
1140
  } else {
1114
- throw new Error(`Expected IRI after @base, got ${tok.toString()}`);
1141
+ this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
1115
1142
  }
1116
1143
  this.expectDot();
1117
1144
  this.prefixes.setBase(iri);
@@ -1121,7 +1148,7 @@ class Parser {
1121
1148
  // SPARQL/Turtle-style PREFIX directive: PREFIX pfx: <iri> (no trailing '.')
1122
1149
  const tok = this.next();
1123
1150
  if (tok.typ !== 'Ident') {
1124
- throw new Error(`Expected prefix name after PREFIX, got ${tok.toString()}`);
1151
+ this.fail(`Expected prefix name after PREFIX, got ${tok.toString()}`, tok);
1125
1152
  }
1126
1153
  const pref = tok.value || '';
1127
1154
  const prefName = pref.endsWith(':') ? pref.slice(0, -1) : pref;
@@ -1133,7 +1160,7 @@ class Parser {
1133
1160
  } else if (tok2.typ === 'Ident') {
1134
1161
  iri = this.prefixes.expandQName(tok2.value || '');
1135
1162
  } else {
1136
- throw new Error(`Expected IRI after PREFIX, got ${tok2.toString()}`);
1163
+ this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
1137
1164
  }
1138
1165
 
1139
1166
  // N3/Turtle: PREFIX directives do not have a trailing '.', but accept it permissively.
@@ -1151,7 +1178,7 @@ class Parser {
1151
1178
  } else if (tok.typ === 'Ident') {
1152
1179
  iri = tok.value || '';
1153
1180
  } else {
1154
- throw new Error(`Expected IRI after BASE, got ${tok.toString()}`);
1181
+ this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
1155
1182
  }
1156
1183
 
1157
1184
  // N3/Turtle: BASE directives do not have a trailing '.', but accept it permissively.
@@ -1211,7 +1238,7 @@ class Parser {
1211
1238
  if (this.peek().typ === 'LangTag') {
1212
1239
  // Only quoted string literals can carry a language tag.
1213
1240
  if (!(s.startsWith('"') && s.endsWith('"'))) {
1214
- throw new Error('Language tag is only allowed on quoted string literals');
1241
+ this.fail('Language tag is only allowed on quoted string literals', this.peek());
1215
1242
  }
1216
1243
  const langTok = this.next();
1217
1244
  const lang = langTok.value || '';
@@ -1219,7 +1246,7 @@ class Parser {
1219
1246
 
1220
1247
  // N3/Turtle: language tags and datatypes are mutually exclusive.
1221
1248
  if (this.peek().typ === 'HatHat') {
1222
- throw new Error('A literal cannot have both a language tag (@...) and a datatype (^^...)');
1249
+ this.fail('A literal cannot have both a language tag (@...) and a datatype (^^...)', this.peek());
1223
1250
  }
1224
1251
  }
1225
1252
 
@@ -1234,7 +1261,7 @@ class Parser {
1234
1261
  if (qn.includes(':')) dtIri = this.prefixes.expandQName(qn);
1235
1262
  else dtIri = qn;
1236
1263
  } else {
1237
- throw new Error(`Expected datatype after ^^, got ${dtTok.toString()}`);
1264
+ this.fail(`Expected datatype after ^^, got ${dtTok.toString()}`, dtTok);
1238
1265
  }
1239
1266
  s = `${s}^^<${dtIri}>`;
1240
1267
  }
@@ -1246,7 +1273,7 @@ class Parser {
1246
1273
  if (typ === 'LBracket') return this.parseBlank();
1247
1274
  if (typ === 'LBrace') return this.parseGraph();
1248
1275
 
1249
- throw new Error(`Unexpected term token: ${tok.toString()}`);
1276
+ this.fail(`Unexpected term token: ${tok.toString()}`, tok);
1250
1277
  }
1251
1278
 
1252
1279
  parseList() {
@@ -1269,12 +1296,12 @@ class Parser {
1269
1296
  // IRI property list: [ id <IRI> predicateObjectList? ]
1270
1297
  // Lets you embed descriptions of an IRI directly in object position.
1271
1298
  if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'id') {
1272
- this.next(); // consume 'id'
1299
+ const iriTok = this.next(); // consume 'id'
1273
1300
  const iriTerm = this.parseTerm();
1274
1301
 
1275
1302
  // N3 note: 'id' form is not meant to be used with blank node identifiers.
1276
1303
  if (iriTerm instanceof Blank && iriTerm.label.startsWith('_:')) {
1277
- throw new Error("Cannot use 'id' keyword with a blank node identifier inside [...]");
1304
+ this.fail("Cannot use 'id' keyword with a blank node identifier inside [...]", iriTok);
1278
1305
  }
1279
1306
 
1280
1307
  // Optional ';' right after the id IRI (tolerated).
@@ -1320,7 +1347,7 @@ class Parser {
1320
1347
  }
1321
1348
 
1322
1349
  if (this.peek().typ !== 'RBracket') {
1323
- throw new Error(`Expected ']' at end of IRI property list, got ${JSON.stringify(this.peek())}`);
1350
+ this.fail(`Expected ']' at end of IRI property list, got ${this.peek().toString()}`);
1324
1351
  }
1325
1352
  this.next();
1326
1353
  return iriTerm;
@@ -1368,7 +1395,7 @@ class Parser {
1368
1395
  if (this.peek().typ === 'RBracket') {
1369
1396
  this.next();
1370
1397
  } else {
1371
- throw new Error(`Expected ']' at end of blank node property list, got ${JSON.stringify(this.peek())}`);
1398
+ this.fail(`Expected ']' at end of blank node property list, got ${this.peek().toString()}`);
1372
1399
  }
1373
1400
 
1374
1401
  return new Blank(id);
@@ -1387,7 +1414,7 @@ class Parser {
1387
1414
  else if (this.peek().typ === 'RBrace') {
1388
1415
  // ok
1389
1416
  } else {
1390
- throw new Error(`Expected '.' or '}', got ${this.peek().toString()}`);
1417
+ this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1391
1418
  }
1392
1419
  } else if (this.peek().typ === 'OpImpliedBy') {
1393
1420
  this.next();
@@ -1398,7 +1425,7 @@ class Parser {
1398
1425
  else if (this.peek().typ === 'RBrace') {
1399
1426
  // ok
1400
1427
  } else {
1401
- throw new Error(`Expected '.' or '}', got ${this.peek().toString()}`);
1428
+ this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1402
1429
  }
1403
1430
  } else {
1404
1431
  // N3 grammar allows: triples ::= subject predicateObjectList?
@@ -1417,7 +1444,7 @@ class Parser {
1417
1444
  else if (this.peek().typ === 'RBrace') {
1418
1445
  // ok
1419
1446
  } else {
1420
- throw new Error(`Expected '.' or '}', got ${this.peek().toString()}`);
1447
+ this.fail(`Expected '.' or '}', got ${this.peek().toString()}`);
1421
1448
  }
1422
1449
  }
1423
1450
  }
@@ -1450,7 +1477,7 @@ class Parser {
1450
1477
  this.next(); // consume "is"
1451
1478
  verb = this.parseTerm();
1452
1479
  if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
1453
- throw new Error(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
1480
+ this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
1454
1481
  }
1455
1482
  this.next(); // consume "of"
1456
1483
  invert = true;
@@ -3699,6 +3726,13 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3699
3726
  const pv = iriValue(g.p);
3700
3727
  if (pv === null) return null;
3701
3728
 
3729
+ // Super restricted mode: disable *all* builtins except => / <= (log:implies / log:impliedBy)
3730
+ if (superRestrictedMode) {
3731
+ const allow1 = LOG_NS + 'implies';
3732
+ const allow2 = LOG_NS + 'impliedBy';
3733
+ if (pv !== allow1 && pv !== allow2) return [];
3734
+ }
3735
+
3702
3736
  // -----------------------------------------------------------------
3703
3737
  // 4.1 crypto: builtins
3704
3738
  // -----------------------------------------------------------------
@@ -5118,6 +5152,13 @@ function isBuiltinPred(p) {
5118
5152
  if (!(p instanceof Iri)) return false;
5119
5153
  const v = p.value;
5120
5154
 
5155
+ // Super restricted mode: only treat => / <= as builtins.
5156
+ // Everything else should be handled as ordinary predicates (and thus must be
5157
+ // provided explicitly as facts/rules, without builtin evaluation).
5158
+ if (superRestrictedMode) {
5159
+ return v === LOG_NS + 'implies' || v === LOG_NS + 'impliedBy';
5160
+ }
5161
+
5121
5162
  // Treat RDF Collections as list-term builtins too.
5122
5163
  if (v === RDF_NS + 'first' || v === RDF_NS + 'rest') {
5123
5164
  return true;
@@ -5778,6 +5819,41 @@ function printExplanation(df, prefixes) {
5778
5819
  console.log('# ----------------------------------------------------------------------\n');
5779
5820
  }
5780
5821
 
5822
+
5823
+ function offsetToLineCol(text, offset) {
5824
+ const chars = Array.from(text);
5825
+ const n = Math.max(0, Math.min(typeof offset === 'number' ? offset : 0, chars.length));
5826
+ let line = 1;
5827
+ let col = 1;
5828
+ for (let i = 0; i < n; i++) {
5829
+ const c = chars[i];
5830
+ if (c === '\n') {
5831
+ line++;
5832
+ col = 1;
5833
+ } else if (c === '\r') {
5834
+ line++;
5835
+ col = 1;
5836
+ if (i + 1 < n && chars[i + 1] === '\n') i++; // swallow \n in CRLF
5837
+ } else {
5838
+ col++;
5839
+ }
5840
+ }
5841
+ return { line, col };
5842
+ }
5843
+
5844
+ function formatN3SyntaxError(err, text, path) {
5845
+ const off = err && typeof err.offset === 'number' ? err.offset : null;
5846
+ const label = path ? String(path) : '<input>';
5847
+ if (off === null) {
5848
+ return `Syntax error in ${label}: ${err && err.message ? err.message : String(err)}`;
5849
+ }
5850
+ const { line, col } = offsetToLineCol(text, off);
5851
+ const lines = String(text).split(/\r\n|\n|\r/);
5852
+ const lineText = lines[line - 1] ?? '';
5853
+ const caret = ' '.repeat(Math.max(0, col - 1)) + '^';
5854
+ return `Syntax error in ${label}:${line}:${col}: ${err.message}\n${lineText}\n${caret}`;
5855
+ }
5856
+
5781
5857
  // ===========================================================================
5782
5858
  // CLI entry point
5783
5859
  // ===========================================================================
@@ -5796,6 +5872,7 @@ function main() {
5796
5872
  ` -v, --version Print version and exit.\n` +
5797
5873
  ` -p, --proof-comments Enable proof explanations.\n` +
5798
5874
  ` -n, --no-proof-comments Disable proof explanations (default).\n` +
5875
+ ` -s, --super-restricted Disable all builtins except => and <=.\n` +
5799
5876
  ` -a, --ast Print parsed AST as JSON and exit.\n`;
5800
5877
  (toStderr ? console.error : console.log)(msg);
5801
5878
  }
@@ -5828,6 +5905,11 @@ function main() {
5828
5905
  proofCommentsEnabled = false;
5829
5906
  }
5830
5907
 
5908
+ // --super-restricted / -s: disable all builtins except => / <=
5909
+ if (argv.includes('--super-restricted') || argv.includes('-s')) {
5910
+ superRestrictedMode = true;
5911
+ }
5912
+
5831
5913
  // --------------------------------------------------------------------------
5832
5914
  // Positional args (the N3 file)
5833
5915
  // --------------------------------------------------------------------------
@@ -5853,9 +5935,19 @@ function main() {
5853
5935
  process.exit(1);
5854
5936
  }
5855
5937
 
5856
- const toks = lex(text);
5857
- const parser = new Parser(toks);
5858
- const [prefixes, triples, frules, brules] = parser.parseDocument();
5938
+ let toks;
5939
+ let prefixes, triples, frules, brules;
5940
+ try {
5941
+ toks = lex(text);
5942
+ const parser = new Parser(toks);
5943
+ [prefixes, triples, frules, brules] = parser.parseDocument();
5944
+ } catch (e) {
5945
+ if (e && e.name === 'N3SyntaxError') {
5946
+ console.error(formatN3SyntaxError(e, text, path));
5947
+ process.exit(1);
5948
+ }
5949
+ throw e;
5950
+ }
5859
5951
  if (showAst) {
5860
5952
  function astReplacer(_key, value) {
5861
5953
  if (value instanceof Set) return Array.from(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.6.33",
3
+ "version": "1.7.1",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [