eyeling 1.25.5 → 1.26.0

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.
@@ -31,6 +31,67 @@ function emptyParsedDocument() {
31
31
  };
32
32
  }
33
33
 
34
+ function lineStartsForText(text) {
35
+ const s = String(text);
36
+ const starts = [0];
37
+ for (let i = 0; i < s.length; i++) {
38
+ const c = s.charCodeAt(i);
39
+ if (c === 10) starts.push(i + 1); // LF
40
+ else if (c === 13) { // CR or CRLF
41
+ if (i + 1 < s.length && s.charCodeAt(i + 1) === 10) i++;
42
+ starts.push(i + 1);
43
+ }
44
+ }
45
+ return starts;
46
+ }
47
+
48
+ function offsetToLineFromStarts(offset, lineStarts) {
49
+ if (typeof offset !== 'number') return null;
50
+ let lo = 0;
51
+ let hi = lineStarts.length;
52
+ while (lo + 1 < hi) {
53
+ const mid = (lo + hi) >> 1;
54
+ if (lineStarts[mid] <= offset) lo = mid;
55
+ else hi = mid;
56
+ }
57
+ return lo + 1;
58
+ }
59
+
60
+ function annotateSourceLocation(obj, label, lineStarts) {
61
+ if (!obj || typeof obj.__sourceOffset !== 'number') return obj;
62
+ const line = offsetToLineFromStarts(obj.__sourceOffset, lineStarts);
63
+ Object.defineProperty(obj, '__source', {
64
+ value: { label, line },
65
+ enumerable: false,
66
+ writable: false,
67
+ configurable: true,
68
+ });
69
+ return obj;
70
+ }
71
+
72
+ function annotateParsedSourceLocations(doc, text, label) {
73
+ const lineStarts = lineStartsForText(text);
74
+ for (const tr of doc.triples || []) annotateSourceLocation(tr, label, lineStarts);
75
+ for (const r of doc.frules || []) annotateSourceLocation(r, label, lineStarts);
76
+ for (const r of doc.brules || []) annotateSourceLocation(r, label, lineStarts);
77
+ for (const r of doc.logQueryRules || []) annotateSourceLocation(r, label, lineStarts);
78
+ }
79
+
80
+ function copySourceMetadata(from, to) {
81
+ if (!from || !to) return to;
82
+ for (const key of ['__sourceOffset', '__source']) {
83
+ if (Object.prototype.hasOwnProperty.call(from, key)) {
84
+ Object.defineProperty(to, key, {
85
+ value: from[key],
86
+ enumerable: false,
87
+ writable: false,
88
+ configurable: true,
89
+ });
90
+ }
91
+ }
92
+ return to;
93
+ }
94
+
34
95
  function prefixesUsedInTokens(tokens, prefEnv) {
35
96
  const used = new Set();
36
97
  const toks = Array.isArray(tokens) ? tokens : [];
@@ -108,6 +169,7 @@ function parseN3Text(text, opts = {}) {
108
169
  label = '<input>',
109
170
  keepSourceArtifacts = true,
110
171
  collectUsedPrefixes = false,
172
+ collectSourceLocations = true,
111
173
  rdf = false,
112
174
  } = opts || {};
113
175
  const tokens = lex(text, { rdf });
@@ -116,6 +178,7 @@ function parseN3Text(text, opts = {}) {
116
178
  const [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
117
179
 
118
180
  const doc = { prefixes, triples, frules, brules, logQueryRules, label };
181
+ if (collectSourceLocations) annotateParsedSourceLocations(doc, text, label);
119
182
 
120
183
  if (collectUsedPrefixes) {
121
184
  Object.defineProperty(doc, 'usedPrefixes', {
@@ -161,7 +224,7 @@ function scopeBlankNodesInDocument(doc, sourceIndex) {
161
224
  }
162
225
 
163
226
  function cloneTriple(triple) {
164
- return new Triple(cloneTerm(triple.s), cloneTerm(triple.p), cloneTerm(triple.o));
227
+ return copySourceMetadata(triple, new Triple(cloneTerm(triple.s), cloneTerm(triple.p), cloneTerm(triple.o)));
165
228
  }
166
229
 
167
230
  function cloneRule(rule) {
@@ -170,12 +233,15 @@ function scopeBlankNodesInDocument(doc, sourceIndex) {
170
233
  for (const label of rule.headBlankLabels) headBlankLabels.add(scopedBlankLabel(label, sourceIndex, mapping));
171
234
  }
172
235
 
173
- const out = new Rule(
174
- (rule.premise || []).map(cloneTriple),
175
- (rule.conclusion || []).map(cloneTriple),
176
- rule.isForward,
177
- rule.isFuse,
178
- headBlankLabels,
236
+ const out = copySourceMetadata(
237
+ rule,
238
+ new Rule(
239
+ (rule.premise || []).map(cloneTriple),
240
+ (rule.conclusion || []).map(cloneTriple),
241
+ rule.isForward,
242
+ rule.isFuse,
243
+ headBlankLabels,
244
+ ),
179
245
  );
180
246
 
181
247
  if (rule && Object.prototype.hasOwnProperty.call(rule, '__dynamicConclusionTerm')) {
@@ -303,6 +369,7 @@ function parseN3SourceList(input, opts = {}) {
303
369
  label: source.label,
304
370
  baseIri: source.baseIri || (sources.length === 1 ? defaultBaseIri : ''),
305
371
  collectUsedPrefixes: true,
372
+ collectSourceLocations: !!opts.collectSourceLocations,
306
373
  keepSourceArtifacts: !!opts.keepSourceArtifacts,
307
374
  rdf: !!opts.rdf,
308
375
  }),
package/lib/parser.js CHANGED
@@ -42,6 +42,17 @@ function failInvalidKeywordLikeIdent(fail, tok, name) {
42
42
  fail(`invalid_keyword(${name})`, tok);
43
43
  }
44
44
 
45
+ function annotateSourceOffset(obj, offset) {
46
+ if (!obj || typeof offset !== 'number') return obj;
47
+ Object.defineProperty(obj, '__sourceOffset', {
48
+ value: offset,
49
+ enumerable: false,
50
+ writable: false,
51
+ configurable: true,
52
+ });
53
+ return obj;
54
+ }
55
+
45
56
  class Parser {
46
57
  constructor(tokens) {
47
58
  this.toks = tokens;
@@ -166,17 +177,19 @@ class Parser {
166
177
  continue;
167
178
  }
168
179
 
180
+ const statementToken = this.peek();
181
+ const statementOffset = statementToken && typeof statementToken.offset === 'number' ? statementToken.offset : null;
169
182
  const first = this.parseTerm();
170
183
  if (this.peek().typ === 'OpImplies') {
171
184
  this.next();
172
185
  const second = this.parseTerm();
173
186
  this.expectDot();
174
- forwardRules.push(this.makeRule(first, second, true));
187
+ forwardRules.push(annotateSourceOffset(this.makeRule(first, second, true), statementOffset));
175
188
  } else if (this.peek().typ === 'OpImpliedBy') {
176
189
  this.next();
177
190
  const second = this.parseTerm();
178
191
  this.expectDot();
179
- backwardRules.push(this.makeRule(first, second, false));
192
+ backwardRules.push(annotateSourceOffset(this.makeRule(first, second, false), statementOffset));
180
193
  } else {
181
194
  let more;
182
195
 
@@ -194,16 +207,17 @@ class Parser {
194
207
  }
195
208
 
196
209
  // normalize explicit log:implies / log:impliedBy at top-level
197
- for (const tr of more) {
210
+ for (const tr0 of more) {
211
+ const tr = annotateSourceOffset(tr0, statementOffset);
198
212
  if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
199
- forwardRules.push(this.makeRule(tr.s, tr.o, true));
213
+ forwardRules.push(annotateSourceOffset(this.makeRule(tr.s, tr.o, true), statementOffset));
200
214
  } else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
201
- backwardRules.push(this.makeRule(tr.s, tr.o, false));
215
+ backwardRules.push(annotateSourceOffset(this.makeRule(tr.s, tr.o, false), statementOffset));
202
216
  } else if (isLogQuery(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
203
217
  // Output-selection directive: { premise } log:query { conclusion }.
204
218
  // When present at top-level, eyeling prints only the instantiated conclusion
205
219
  // triples (unique) instead of all newly derived facts.
206
- logQueries.push(this.makeRule(tr.s, tr.o, true));
220
+ logQueries.push(annotateSourceOffset(this.makeRule(tr.s, tr.o, true), statementOffset));
207
221
  } else {
208
222
  triples.push(tr);
209
223
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.25.5",
3
+ "version": "1.26.0",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -454,7 +454,7 @@ ${U('x')} ${U('age')} "42".
454
454
  ${U('s')} ${U('p')} ${U('o')}.
455
455
  `,
456
456
  expect: [new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
457
- notExpect: [/^#/m],
457
+ notExpect: [/pe:why/m],
458
458
  },
459
459
  {
460
460
  name: '11 negative entailment: rule derives false (expect exit 65 => throws)',
@@ -1038,23 +1038,23 @@ ${U('a')} ${U('p')} ${U('b')}.
1038
1038
  expect: [new RegExp(`${EX}a>\\s+<${EX}q2>\\s+<${EX}b>\\s*\\.`)],
1039
1039
  },
1040
1040
  {
1041
- name: '30 sanity: proofComments:true enables proof comments',
1042
- opt: { proofComments: true },
1041
+ name: '30 sanity: proof:true enables N3 proof explanations',
1042
+ opt: { proof: true },
1043
1043
  input: `
1044
1044
  { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
1045
1045
  ${U('s')} ${U('p')} ${U('o')}.
1046
1046
  `,
1047
- expect: [/^#/m, new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
1047
+ expect: [/pe:why/m, /pe:by/m, new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
1048
1048
  },
1049
1049
  {
1050
- name: '31 sanity: -n suppresses proof comments',
1050
+ name: '31 sanity: default output has no proof explanations',
1051
1051
  opt: ['-n'],
1052
1052
  input: `
1053
1053
  { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
1054
1054
  ${U('s')} ${U('p')} ${U('o')}.
1055
1055
  `,
1056
1056
  expect: [new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
1057
- notExpect: [/^#/m],
1057
+ notExpect: [/pe:why/m],
1058
1058
  },
1059
1059
 
1060
1060
  // -------------------------
package/tools/bundle.js CHANGED
@@ -158,6 +158,9 @@ function buildBundleSource({ autoRunMain }) {
158
158
  out.push(
159
159
  ' if (typeof __entry.printExplanation === "function") __outerSelf.printExplanation = __entry.printExplanation;',
160
160
  );
161
+ out.push(
162
+ ' if (typeof __entry.renderProofDocument === "function") __outerSelf.renderProofDocument = __entry.renderProofDocument;',
163
+ );
161
164
  out.push(' if (typeof __entry.tripleToN3 === "function") __outerSelf.tripleToN3 = __entry.tripleToN3;');
162
165
  out.push(
163
166
  ' if (typeof __entry.collectOutputStringsFromFacts === "function") __outerSelf.collectOutputStringsFromFacts = __entry.collectOutputStringsFromFacts;',