eyeling 2.34.7 → 2.35.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.
package/README.md CHANGED
@@ -224,6 +224,25 @@ The CLI renders the `log:outputString` values directly:
224
224
  hello
225
225
  ```
226
226
 
227
+ ### Predicate memoization
228
+
229
+ Eyeling supports a predicate-local performance directive for backward rules:
230
+
231
+ ```n3
232
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
233
+
234
+ :predicate log:memoize true .
235
+ ```
236
+
237
+ A top-level `log:memoize true` triple is treated as an engine directive rather than an RDF fact. It asks the backward prover to reuse completed answers for that predicate during the current reasoning run. This is especially useful for recursive predicates with overlapping subgoals, such as the direct Fibonacci recurrence or Collatz trajectories where many starting values share suffixes:
238
+
239
+ ```n3
240
+ :fibonacci log:memoize true .
241
+ :collatz log:memoize true .
242
+ ```
243
+
244
+ Memoization is intentionally selective. It can slow down pure generators, predicates with mostly unique states, or small linear recursions because maintaining the table costs more than recomputation. Prefer it for dynamic-programming-shaped predicates that are called repeatedly with the same bound subject or object. The examples `examples/fibonacci.n3`, `examples/collatz-1000.n3`, and `examples/fundamental-theorem-arithmetic.n3` show cases where the directive helps.
245
+
227
246
  ---
228
247
 
229
248
  ## Command-line interface
@@ -743,6 +762,8 @@ Eyeling supports both native N3 list terms and materialized RDF collections. Ano
743
762
 
744
763
  Formula-aware built-ins make Eyeling useful for meta-reasoning. `log:includes`, `log:notIncludes`, `log:collectAllIn`, and `log:forAllIn` prove goals inside formula scopes. `log:query` selects output triples, while `log:outputString` writes selected string values directly to stdout.
745
764
 
765
+ `log:memoize` is a top-level performance directive for backward predicates, not an RDF-producing fact and not a general-purpose built-in. Use it when a recursive predicate has overlapping subgoals whose completed answers are likely to be requested again.
766
+
746
767
  `log:semantics`, `log:content`, and related built-ins may dereference sources. Use `--enforce-https` or `{ enforceHttps: true }` in environments where HTTP-to-HTTPS rewriting is required.
747
768
 
748
769
 
@@ -901,6 +922,8 @@ The backward prover solves rule bodies. It can match current facts, use backward
901
922
  { :example :works true } .
902
923
  ```
903
924
 
925
+ For selected predicates, `:predicate log:memoize true .` enables tabling of completed backward answers. This preserves the normal declarative reading of the backward rules but can turn repeated recursive work into table lookups. The directive is removed from the data facts before reasoning, so it does not appear in normal output.
926
+
904
927
  ### Dynamic rules
905
928
 
906
929
  Eyeling treats top-level `log:implies` and `log:impliedBy` as rule forms and can activate derived implication facts as live rules during reasoning. This supports programs that derive rules as part of their logic.
@@ -1042,6 +1065,16 @@ The repository contains more than two hundred N3 examples under `examples/`, RDF
1042
1065
  | `examples/rdf-messages.n3` | RDF Message Log replay. |
1043
1066
  | `examples/context-schema-audit.n3` | Quoted-context schema validation with `log:includes` and list arity checks. |
1044
1067
 
1068
+ ### Memoization examples
1069
+
1070
+ These examples demonstrate `log:memoize` on predicates that benefit from reusing completed backward-rule answers:
1071
+
1072
+ | Example | Memoized predicate | Why it helps |
1073
+ |---|---|---|
1074
+ | `examples/fibonacci.n3` | `:fibonacci` | The direct recurrence repeatedly asks for the same smaller Fibonacci numbers. |
1075
+ | `examples/collatz-1000.n3` | `:collatz` | Many starting values share Collatz suffix trajectories. |
1076
+ | `examples/fundamental-theorem-arithmetic.n3` | `:smallestDivisorFrom`, `:factorSmallest` | Factorization and independent checks reuse trial-division and factor-list subgoals. |
1077
+
1045
1078
  ### Running all examples through tests
1046
1079
 
1047
1080
  ```bash
@@ -6242,7 +6242,7 @@ function runParsedDocumentOnce(mergedDocument, { rdfMode = false, outputPrefixes
6242
6242
  let outTriples = [];
6243
6243
  let queryDerived = [];
6244
6244
  if (hasQueries) {
6245
- const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules, { prefixes });
6245
+ const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules, null, { captureExplanations: false, prefixes });
6246
6246
  derived = res.derived;
6247
6247
  outTriples = res.queryTriples;
6248
6248
  queryDerived = res.queryDerived || [];
@@ -6929,7 +6929,7 @@ function factsContainOutputStrings(triplesForOutput) {
6929
6929
  let outDerived = [];
6930
6930
 
6931
6931
  if (hasQueries) {
6932
- const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules, { prefixes });
6932
+ const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules, null, { captureExplanations: engine.getProofCommentsEnabled(), prefixes });
6933
6933
  derived = res.derived;
6934
6934
  outTriples = res.queryTriples;
6935
6935
  outDerived = res.queryDerived;
@@ -7507,6 +7507,7 @@ const LOG_COLLECT_ALL_IN_IRI = LOG_NS + 'collectAllIn';
7507
7507
  const LOG_FOR_ALL_IN_IRI = LOG_NS + 'forAllIn';
7508
7508
  const LOG_INCLUDES_IRI = LOG_NS + 'includes';
7509
7509
  const LOG_NOT_INCLUDES_IRI = LOG_NS + 'notIncludes';
7510
+ const LOG_MEMOIZE_IRI = LOG_NS + 'memoize';
7510
7511
  const LOG_IMPLIES_IRI = LOG_NS + 'implies';
7511
7512
  const LOG_IMPLIED_BY_IRI = LOG_NS + 'impliedBy';
7512
7513
 
@@ -7608,6 +7609,7 @@ const FACT_INDEX_ARRAY_MAP_FIELDS = ['__byPred', '__byPNonFastS', '__byPNonFastO
7608
7609
  const FACT_INDEX_NESTED_ARRAY_MAP_FIELDS = ['__byPS', '__byPO'];
7609
7610
  const FACT_INDEX_MAP_FIELDS = FACT_INDEX_ARRAY_MAP_FIELDS.concat(FACT_INDEX_NESTED_ARRAY_MAP_FIELDS);
7610
7611
  const FACT_INDEX_REQUIRED_FIELDS = FACT_INDEX_ARRAY_FIELDS.concat(FACT_INDEX_MAP_FIELDS, ['__keySet']);
7612
+ const FACT_INDEX_ALL_FIELDS = FACT_INDEX_REQUIRED_FIELDS.concat(['__keySetComplete']);
7611
7613
 
7612
7614
  let version = 'dev';
7613
7615
  try {
@@ -8675,7 +8677,13 @@ function termLookupKey(t) {
8675
8677
  return t.__tid;
8676
8678
  }
8677
8679
  if (t instanceof Blank) return t.__tid;
8678
- if (t instanceof Literal) return literalLookupKey(t);
8680
+ if (t instanceof Literal) {
8681
+ const cached = t.__lookupKey;
8682
+ if (cached !== undefined) return cached;
8683
+ const key = literalLookupKey(t);
8684
+ Object.defineProperty(t, '__lookupKey', { value: key, enumerable: false });
8685
+ return key;
8686
+ }
8679
8687
 
8680
8688
  if (t instanceof ListTerm) {
8681
8689
  const cached = t.__lookupKey;
@@ -8713,6 +8721,13 @@ function tripleFastKey(tr) {
8713
8721
  return ks + '\t' + kp + '\t' + ko;
8714
8722
  }
8715
8723
 
8724
+ function resetFactIndexes(facts) {
8725
+ if (!facts) return;
8726
+ for (const name of FACT_INDEX_ALL_FIELDS) {
8727
+ try { delete facts[name]; } catch {}
8728
+ }
8729
+ }
8730
+
8716
8731
  function ensureFactIndexes(facts) {
8717
8732
  if (FACT_INDEX_REQUIRED_FIELDS.every((name) => facts[name])) return;
8718
8733
 
@@ -9143,6 +9158,157 @@ function isLogImpliedBy(p) {
9143
9158
  return p instanceof Iri && p.value === LOG_IMPLIED_BY_IRI;
9144
9159
  }
9145
9160
 
9161
+ function isLogMemoize(p) {
9162
+ return p instanceof Iri && p.value === LOG_MEMOIZE_IRI;
9163
+ }
9164
+
9165
+ function isTruthyMemoizeObject(o) {
9166
+ if (!(o instanceof Literal)) return false;
9167
+ if (o.value === 'true') return true;
9168
+ const info = parseBooleanLiteralInfo(o);
9169
+ return !!(info && info.value === true);
9170
+ }
9171
+
9172
+ // ===========================================================================
9173
+ // Predicate memoization declarations
9174
+ // ===========================================================================
9175
+ //
9176
+ // Eyeling accepts top-level declarations of the form:
9177
+ // :predicate log:memoize true.
9178
+ // They are engine directives, not data facts. Each declaration asks the
9179
+ // backward prover to table completed answers for that predicate during the
9180
+ // current reasoning scope. The hint is deliberately predicate-local and
9181
+ // conservative: unbound calls and finite-result probes still use the normal
9182
+ // path, and cached answers are invalidated when the fact/rule scope changes.
9183
+
9184
+ function __extractLogMemoizeDeclarations(facts) {
9185
+ const memoized = new Set();
9186
+ if (!Array.isArray(facts) || facts.length === 0) return memoized;
9187
+
9188
+ let write = 0;
9189
+ let removed = false;
9190
+ for (let read = 0; read < facts.length; read++) {
9191
+ const tr = facts[read];
9192
+ if (tr && isLogMemoize(tr.p) && tr.s instanceof Iri && isTruthyMemoizeObject(tr.o)) {
9193
+ memoized.add(tr.s.__tid);
9194
+ removed = true;
9195
+ continue;
9196
+ }
9197
+ if (write !== read) facts[write] = tr;
9198
+ write++;
9199
+ }
9200
+
9201
+ if (removed) {
9202
+ facts.length = write;
9203
+ resetFactIndexes(facts);
9204
+ }
9205
+ return memoized;
9206
+ }
9207
+
9208
+ function __attachMemoizedPredicates(scopeCarrier, memoized) {
9209
+ if (!scopeCarrier || !memoized || memoized.size === 0) return;
9210
+ __setHiddenWritable(scopeCarrier, 'memoizedPredicates', memoized);
9211
+ }
9212
+
9213
+ function __memoizedPredicateSet(facts, backRules) {
9214
+ return (facts && facts.memoizedPredicates) || (backRules && backRules.memoizedPredicates) || null;
9215
+ }
9216
+
9217
+ function __predicateIsMemoized(facts, backRules, p) {
9218
+ if (!(p instanceof Iri)) return false;
9219
+ const memoized = __memoizedPredicateSet(facts, backRules);
9220
+ return !!(memoized && memoized.has(p.__tid));
9221
+ }
9222
+
9223
+ function __makePredicateMemoTable() {
9224
+ return {
9225
+ scopeVersion: null,
9226
+ entries: new Map(),
9227
+ };
9228
+ }
9229
+
9230
+ function __ensurePredicateMemoTable(facts, backRules) {
9231
+ let table = (facts && facts.predicateMemoTable) || (backRules && backRules.predicateMemoTable) || null;
9232
+ if (!table) table = __makePredicateMemoTable();
9233
+ __setHiddenWritable(facts, 'predicateMemoTable', table);
9234
+ __setHiddenWritable(backRules, 'predicateMemoTable', table);
9235
+
9236
+ const version = goalTableScopeVersion(facts, backRules);
9237
+ if (table.scopeVersion !== version) {
9238
+ table.scopeVersion = version;
9239
+ table.entries.clear();
9240
+ }
9241
+ return table;
9242
+ }
9243
+
9244
+ function __memoKeyPart(t) {
9245
+ if (__termHasVarOrBlank(t)) return { bound: false, text: '_' };
9246
+ const fast = termLookupKey(t);
9247
+ if (fast !== null) return { bound: true, text: encodeLookupKeyPart(fast) };
9248
+ return { bound: true, text: skolemKeyFromTerm(t) };
9249
+ }
9250
+
9251
+ function __predicateMemoKey(goal) {
9252
+ if (!(goal && goal.p instanceof Iri)) return null;
9253
+ const sPart = __memoKeyPart(goal.s);
9254
+ const oPart = __memoKeyPart(goal.o);
9255
+ if (!sPart.bound && !oPart.bound) return null;
9256
+ return `${goal.p.__tid}|${sPart.text}|${oPart.text}`;
9257
+ }
9258
+
9259
+ function __makePredicateMemoEntry() {
9260
+ return {
9261
+ computing: false,
9262
+ complete: false,
9263
+ unsafe: false,
9264
+ answers: [],
9265
+ answerKeys: new Set(),
9266
+ };
9267
+ }
9268
+
9269
+ function __getPredicateMemoEntry(facts, backRules, key) {
9270
+ const table = __ensurePredicateMemoTable(facts, backRules);
9271
+ let entry = table.entries.get(key);
9272
+ if (!entry) {
9273
+ entry = __makePredicateMemoEntry();
9274
+ table.entries.set(key, entry);
9275
+ }
9276
+ return entry;
9277
+ }
9278
+
9279
+ function __canStorePredicateMemo(maxResults) {
9280
+ return !(typeof maxResults === 'number' && maxResults > 0);
9281
+ }
9282
+
9283
+ function __canMemoizeAnswerTerm(t) {
9284
+ if (t instanceof Var || t instanceof OpenListTerm) return false;
9285
+ if (t instanceof ListTerm) return t.elems.every(__canMemoizeAnswerTerm);
9286
+ if (t instanceof GraphTerm) return t.triples.every((tr) => __canMemoizeAnswerTerm(tr.s) && __canMemoizeAnswerTerm(tr.p) && __canMemoizeAnswerTerm(tr.o));
9287
+ return true;
9288
+ }
9289
+
9290
+ function __memoAnswerKeyPart(t) {
9291
+ const fast = termLookupKey(t);
9292
+ if (fast !== null) return encodeLookupKeyPart(fast);
9293
+ return skolemKeyFromTerm(t);
9294
+ }
9295
+
9296
+ function __memoAnswerKey(tr) {
9297
+ return __memoAnswerKeyPart(tr.s) + '\t' + __memoAnswerKeyPart(tr.p) + '\t' + __memoAnswerKeyPart(tr.o);
9298
+ }
9299
+
9300
+ function __storePredicateMemoAnswer(entry, rawGoal, subst) {
9301
+ const answer = applySubstTriple(rawGoal, subst || {});
9302
+ if (!__canMemoizeAnswerTerm(answer.s) || !__canMemoizeAnswerTerm(answer.p) || !__canMemoizeAnswerTerm(answer.o)) {
9303
+ entry.unsafe = true;
9304
+ return;
9305
+ }
9306
+ const key = __memoAnswerKey(answer);
9307
+ if (entry.answerKeys.has(key)) return;
9308
+ entry.answerKeys.add(key);
9309
+ entry.answers.push(answer);
9310
+ }
9311
+
9146
9312
  // ===========================================================================
9147
9313
  // Completed-goal answer tables (minimal tabling)
9148
9314
  // ===========================================================================
@@ -10056,6 +10222,47 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
10056
10222
  continue;
10057
10223
  }
10058
10224
 
10225
+ if (frame.kind === 'memoComplete') {
10226
+ frame.entry.computing = false;
10227
+ if (frame.entry.unsafe) {
10228
+ frame.table.entries.delete(frame.key);
10229
+ } else {
10230
+ frame.entry.complete = true;
10231
+ }
10232
+ continue;
10233
+ }
10234
+
10235
+ if (frame.kind === 'memoAnswerIter') {
10236
+ const answers = frame.entry.answers;
10237
+ while (frame.idx < answers.length && results.length < max) {
10238
+ const answer = answers[frame.idx++];
10239
+ const mark = trail.length;
10240
+ if (!unifyTripleTrail(frame.goal0, answer)) {
10241
+ undoTo(mark);
10242
+ continue;
10243
+ }
10244
+
10245
+ if (!frame.restGoals.length) {
10246
+ results.push(gcCompactForGoals(substMut, [], answerVars));
10247
+ undoTo(mark);
10248
+ if (results.length >= max) return results;
10249
+ continue;
10250
+ }
10251
+
10252
+ stack.push(frame);
10253
+ stack.push({ kind: 'undo', substMark: mark, visitedMark: visitedTrail.length });
10254
+ stack.push({
10255
+ kind: 'node',
10256
+ goalsNow: frame.restGoals,
10257
+ curDepth: frame.curDepth + 1,
10258
+ canDeferBuiltins: frame.canDeferBuiltins,
10259
+ deferCount: 0,
10260
+ });
10261
+ break;
10262
+ }
10263
+ continue;
10264
+ }
10265
+
10059
10266
  if (frame.kind === 'deltaIter') {
10060
10267
  const deltas = frame.deltas;
10061
10268
  while (frame.idx < deltas.length && results.length < max) {
@@ -10197,7 +10404,21 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
10197
10404
  }
10198
10405
 
10199
10406
  const rawGoal = goalsNow[0];
10200
- const restGoals = goalsNow.length > 1 ? goalsNow.slice(1) : [];
10407
+ let restGoals = goalsNow.length > 1 ? goalsNow.slice(1) : [];
10408
+
10409
+ if (rawGoal && rawGoal.__kind === 'memoStore') {
10410
+ __storePredicateMemoAnswer(rawGoal.entry, rawGoal.goal, substMut);
10411
+ if (rawGoal.stop && restGoals.length === 0) continue;
10412
+ stack.push({
10413
+ kind: 'node',
10414
+ goalsNow: restGoals,
10415
+ curDepth: frame.curDepth,
10416
+ canDeferBuiltins: frame.canDeferBuiltins,
10417
+ deferCount: 0,
10418
+ });
10419
+ continue;
10420
+ }
10421
+
10201
10422
  const goal0 = applySubstTriple(rawGoal, substMut);
10202
10423
 
10203
10424
  // 1) Builtins
@@ -10293,8 +10514,46 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
10293
10514
  const goalKey = tripleKeyForVisited(goal0);
10294
10515
  const goalWasVisited = visitedCounts.has(goalKey);
10295
10516
 
10517
+ let memoCompleteFrame = null;
10518
+ let memoReplayFrame = null;
10519
+ if (__predicateIsMemoized(facts, backRules, goal0.p)) {
10520
+ const memoKey = __predicateMemoKey(goal0);
10521
+ if (memoKey !== null) {
10522
+ const memoEntry = __getPredicateMemoEntry(facts, backRules, memoKey);
10523
+ if (memoEntry.complete) {
10524
+ stack.push({
10525
+ kind: 'memoAnswerIter',
10526
+ entry: memoEntry,
10527
+ idx: 0,
10528
+ goal0,
10529
+ restGoals,
10530
+ curDepth: frame.curDepth,
10531
+ canDeferBuiltins: frame.canDeferBuiltins,
10532
+ });
10533
+ continue;
10534
+ }
10535
+ if (!memoEntry.computing && __canStorePredicateMemo(maxResults)) {
10536
+ memoEntry.computing = true;
10537
+ const table = __ensurePredicateMemoTable(facts, backRules);
10538
+ memoReplayFrame = {
10539
+ kind: 'memoAnswerIter',
10540
+ entry: memoEntry,
10541
+ idx: 0,
10542
+ goal0,
10543
+ restGoals,
10544
+ curDepth: frame.curDepth,
10545
+ canDeferBuiltins: frame.canDeferBuiltins,
10546
+ };
10547
+ memoCompleteFrame = { kind: 'memoComplete', entry: memoEntry, table, key: memoKey };
10548
+ restGoals = [{ __kind: 'memoStore', entry: memoEntry, goal: rawGoal, stop: true }];
10549
+ }
10550
+ }
10551
+ }
10552
+
10296
10553
  // 3) Backward rules (indexed by head predicate) — explored first
10297
10554
  if (goal0.p instanceof Iri) {
10555
+ if (memoReplayFrame) stack.push(memoReplayFrame);
10556
+ if (memoCompleteFrame) stack.push(memoCompleteFrame);
10298
10557
  ensureBackRuleIndexes(backRules);
10299
10558
  const candRules = (backRules.__byHeadPred.get(goal0.p.__tid) || []).concat(backRules.__wildHeadPred);
10300
10559
 
@@ -10326,6 +10585,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
10326
10585
  });
10327
10586
  }
10328
10587
  } else {
10588
+ if (memoReplayFrame) stack.push(memoReplayFrame);
10589
+ if (memoCompleteFrame) stack.push(memoCompleteFrame);
10329
10590
  // No IRI predicate: rule indexing doesn't apply; only try all facts.
10330
10591
  stack.push({
10331
10592
  kind: 'factIter',
@@ -10531,6 +10792,10 @@ function __exitReasoning(code, message /* optional */) {
10531
10792
  function forwardChain(facts, forwardRules, backRules, onDerived /* optional */, opts = {}) {
10532
10793
  enterReasoningRun();
10533
10794
  try {
10795
+ const memoizedPredicates = __extractLogMemoizeDeclarations(facts);
10796
+ __attachMemoizedPredicates(facts, memoizedPredicates);
10797
+ __attachMemoizedPredicates(backRules, memoizedPredicates);
10798
+
10534
10799
  ensureFactIndexes(facts);
10535
10800
  ensureBackRuleIndexes(backRules);
10536
10801
 
@@ -11169,7 +11434,7 @@ function reasonStream(input, opts = {}) {
11169
11434
  if (Array.isArray(logQueryRules) && logQueryRules.length) {
11170
11435
  // Query-selection mode: derive full closure, then output only the unique
11171
11436
  // instantiated conclusion triples of the log:query directives.
11172
- const res = forwardChainAndCollectLogQueryConclusions(facts, frules, brules, logQueryRules, { prefixes });
11437
+ const res = forwardChainAndCollectLogQueryConclusions(facts, frules, brules, logQueryRules, null, { captureExplanations: proof, prefixes });
11173
11438
  derived = res.derived;
11174
11439
  queryTriples = res.queryTriples;
11175
11440
  queryDerived = res.queryDerived;
@@ -11203,7 +11468,7 @@ function reasonStream(input, opts = {}) {
11203
11468
  onDerived(payload);
11204
11469
  }
11205
11470
  },
11206
- { prefixes },
11471
+ { captureExplanations: proof, prefixes },
11207
11472
  );
11208
11473
  }
11209
11474
 
@@ -14850,7 +15115,7 @@ function parseN3Text(text, opts = {}) {
14850
15115
  }
14851
15116
 
14852
15117
  const tokens = lex(text, { rdf });
14853
- const parser = new Parser(tokens);
15118
+ const parser = new Parser(tokens, { sourceOffsets: sourceLocations });
14854
15119
  if (baseIri) parser.prefixes.setBase(baseIri);
14855
15120
  const [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
14856
15121
 
@@ -15125,7 +15390,7 @@ function annotateSourceOffset(obj, offset) {
15125
15390
  }
15126
15391
 
15127
15392
  class Parser {
15128
- constructor(tokens) {
15393
+ constructor(tokens, options = {}) {
15129
15394
  this.toks = tokens;
15130
15395
  this.pos = 0;
15131
15396
  this.prefixes = PrefixEnv.newDefault();
@@ -15139,6 +15404,7 @@ class Parser {
15139
15404
  // makes derived output read naturally, e.g. ':s :p _:b.' preceding
15140
15405
  // '_:b :q :r.'
15141
15406
  this.pendingTriplesAfter = [];
15407
+ this.trackSourceOffsets = !!(options && options.sourceOffsets);
15142
15408
  }
15143
15409
 
15144
15410
  peek() {
@@ -15151,6 +15417,10 @@ class Parser {
15151
15417
  return tok;
15152
15418
  }
15153
15419
 
15420
+ annotateSourceOffset(obj, offset) {
15421
+ return this.trackSourceOffsets ? annotateSourceOffset(obj, offset) : obj;
15422
+ }
15423
+
15154
15424
  fail(message, tok = this.peek()) {
15155
15425
  const off = tok && typeof tok.offset === 'number' ? tok.offset : null;
15156
15426
  throw new N3SyntaxError(message, off);
@@ -15255,12 +15525,12 @@ class Parser {
15255
15525
  this.next();
15256
15526
  const second = this.parseTerm();
15257
15527
  this.expectDot();
15258
- forwardRules.push(annotateSourceOffset(this.makeRule(first, second, true), statementOffset));
15528
+ forwardRules.push(this.annotateSourceOffset(this.makeRule(first, second, true), statementOffset));
15259
15529
  } else if (this.peek().typ === 'OpImpliedBy') {
15260
15530
  this.next();
15261
15531
  const second = this.parseTerm();
15262
15532
  this.expectDot();
15263
- backwardRules.push(annotateSourceOffset(this.makeRule(first, second, false), statementOffset));
15533
+ backwardRules.push(this.annotateSourceOffset(this.makeRule(first, second, false), statementOffset));
15264
15534
  } else {
15265
15535
  let more;
15266
15536
 
@@ -15279,16 +15549,16 @@ class Parser {
15279
15549
 
15280
15550
  // normalize explicit log:implies / log:impliedBy at top-level
15281
15551
  for (const tr0 of more) {
15282
- const tr = annotateSourceOffset(tr0, statementOffset);
15552
+ const tr = this.annotateSourceOffset(tr0, statementOffset);
15283
15553
  if (isLogImplies(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
15284
- forwardRules.push(annotateSourceOffset(this.makeRule(tr.s, tr.o, true), statementOffset));
15554
+ forwardRules.push(this.annotateSourceOffset(this.makeRule(tr.s, tr.o, true), statementOffset));
15285
15555
  } else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
15286
- backwardRules.push(annotateSourceOffset(this.makeRule(tr.s, tr.o, false), statementOffset));
15556
+ backwardRules.push(this.annotateSourceOffset(this.makeRule(tr.s, tr.o, false), statementOffset));
15287
15557
  } else if (isLogQuery(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
15288
15558
  // Output-selection directive: { premise } log:query { conclusion }.
15289
15559
  // When present at top-level, eyeling prints only the instantiated conclusion
15290
15560
  // triples (unique) instead of all newly derived facts.
15291
- logQueries.push(annotateSourceOffset(this.makeRule(tr.s, tr.o, true), statementOffset));
15561
+ logQueries.push(this.annotateSourceOffset(this.makeRule(tr.s, tr.o, true), statementOffset));
15292
15562
  } else {
15293
15563
  triples.push(tr);
15294
15564
  }
@@ -19,6 +19,10 @@
19
19
  @prefix : <http://example.org/collatz#> .
20
20
  @prefix math: <http://www.w3.org/2000/10/swap/math#> .
21
21
  @prefix list: <http://www.w3.org/2000/10/swap/list#> .
22
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
23
+
24
+ # Memoize the overlapping Collatz suffixes shared by many starting values.
25
+ :collatz log:memoize true .
22
26
 
23
27
  # -----------------------------------------
24
28
  # Query / materialization of the test suite
@@ -3,18 +3,24 @@
3
3
  # See https://en.wikipedia.org/wiki/Fibonacci_number
4
4
  # ==================================================
5
5
 
6
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
6
7
  @prefix math: <http://www.w3.org/2000/10/swap/math#>.
7
8
  @prefix : <https://eyereasoner.github.io/eye/reasoning#>.
8
9
 
9
- {?X :fibonacci ?Y} <= {(?X 0 1) :fib ?Y}.
10
+ # Memoize the overlapping recursive subproblems. The direct definition below
11
+ # intentionally exposes the classic recurrence so repeated calls such as
12
+ # F(10000), F(1000), F(100), ... can reuse completed :fibonacci answers.
13
+ :fibonacci log:memoize true.
10
14
 
11
- {(0 ?A ?B) :fib ?A} <= true.
12
- {(1 ?A ?B) :fib ?B} <= true.
13
- {(?X ?A ?B) :fib ?Y} <= {
15
+ {0 :fibonacci 0} <= true.
16
+ {1 :fibonacci 1} <= true.
17
+ {?X :fibonacci ?Y} <= {
14
18
  ?X math:greaterThan 1.
15
- (?X 1) math:difference ?D.
16
- (?A ?B) math:sum ?S.
17
- (?D ?B ?S) :fib ?Y.
19
+ (?X 1) math:difference ?X1.
20
+ (?X 2) math:difference ?X2.
21
+ ?X1 :fibonacci ?Y1.
22
+ ?X2 :fibonacci ?Y2.
23
+ (?Y1 ?Y2) math:sum ?Y.
18
24
  }.
19
25
 
20
26
  {
@@ -34,4 +40,3 @@
34
40
  10000 :fibonacci ?F10000.
35
41
  }.
36
42
  }.
37
-
@@ -34,6 +34,12 @@
34
34
  # Backward helper relations
35
35
  # -------------------------
36
36
 
37
+ # Memoize repeated factorization subgoals. The example asks for the same
38
+ # smallest-divisor and factor-list computations through several independent
39
+ # checks, so tabling completed answers avoids recomputing trial division.
40
+ :smallestDivisorFrom log:memoize true .
41
+ :factorSmallest log:memoize true .
42
+
37
43
  # divides0(A,B): A divides B in the positive integers.
38
44
  { (?A ?B) :divides0 true }
39
45
  <=