eyeling 1.22.5 → 1.22.7

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 (38) hide show
  1. package/HANDBOOK.md +258 -9
  2. package/dist/browser/eyeling.browser.js +162 -11
  3. package/examples/act-alarm-bit-interoperability.n3 +180 -0
  4. package/examples/act-barley-seed-lineage.n3 +565 -0
  5. package/examples/act-docking-abort.n3 +285 -0
  6. package/examples/act-gravity-mediator-witness.n3 +235 -0
  7. package/examples/act-isolation-breach.n3 +354 -0
  8. package/examples/act-photosynthetic-exciton-transfer.n3 +245 -0
  9. package/examples/act-sensor-memory-reset.n3 +190 -0
  10. package/examples/act-tunnel-junction-wake-switch.n3 +225 -0
  11. package/examples/act-yeast-self-reproduction.n3 +248 -0
  12. package/examples/complex-matrix-stability.n3 +288 -0
  13. package/examples/deck/act-barley-seed-lineage.md +593 -0
  14. package/examples/fundamental-theorem-arithmetic.n3 +244 -0
  15. package/examples/harborsmr.n3 +233 -0
  16. package/examples/meta-rule-audit.n3 +135 -0
  17. package/examples/output/act-alarm-bit-interoperability.txt +20 -0
  18. package/examples/output/act-barley-seed-lineage.txt +25 -0
  19. package/examples/output/act-docking-abort.txt +22 -0
  20. package/examples/output/act-gravity-mediator-witness.txt +24 -0
  21. package/examples/output/act-isolation-breach.txt +27 -0
  22. package/examples/output/act-photosynthetic-exciton-transfer.txt +20 -0
  23. package/examples/output/act-sensor-memory-reset.txt +20 -0
  24. package/examples/output/act-tunnel-junction-wake-switch.txt +21 -0
  25. package/examples/output/act-yeast-self-reproduction.txt +23 -0
  26. package/examples/output/complex-matrix-stability.txt +14 -0
  27. package/examples/output/fundamental-theorem-arithmetic.txt +15 -0
  28. package/examples/output/get-uuid.n3 +2 -2
  29. package/examples/output/harborsmr.txt +20 -0
  30. package/examples/output/meta-rule-audit.n3 +44 -0
  31. package/examples/output/theory-diff.n3 +22 -0
  32. package/examples/theory-diff.n3 +125 -0
  33. package/eyeling.js +162 -11
  34. package/lib/builtins.js +52 -1
  35. package/lib/cli.js +31 -5
  36. package/lib/engine.js +79 -5
  37. package/package.json +1 -1
  38. package/test/api.test.js +127 -0
package/lib/builtins.js CHANGED
@@ -1875,6 +1875,23 @@ function __logNaturalPriorityFromTerm(t) {
1875
1875
  // ===========================================================================
1876
1876
  // Backward proof & builtins mutual recursion — declarations first
1877
1877
 
1878
+ function __varCameFromBoundSubstitution(goalTerm, subst) {
1879
+ if (!(goalTerm instanceof Var)) return false;
1880
+ if (!subst || !Object.prototype.hasOwnProperty.call(subst, goalTerm.name)) return false;
1881
+
1882
+ let cur = subst[goalTerm.name];
1883
+ const seen = new Set([goalTerm.name]);
1884
+
1885
+ while (cur instanceof Var) {
1886
+ if (seen.has(cur.name)) return true;
1887
+ seen.add(cur.name);
1888
+ if (!Object.prototype.hasOwnProperty.call(subst, cur.name)) return true;
1889
+ cur = subst[cur.name];
1890
+ }
1891
+
1892
+ return false;
1893
+ }
1894
+
1878
1895
  function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
1879
1896
  const g = applySubstTriple(goal, subst);
1880
1897
  const pv = iriValue(g.p);
@@ -3341,7 +3358,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3341
3358
  // Schema: $s+ log:rawType $o-
3342
3359
  // Returns one of log:Formula, log:Literal, rdf:List, or log:Other.
3343
3360
  if (pv === LOG_NS + 'rawType') {
3344
- if (g.s instanceof Var) return [];
3361
+ if (g.s instanceof Var && !__varCameFromBoundSubstitution(goal.s, subst)) return [];
3345
3362
 
3346
3363
  let ty;
3347
3364
  if (g.s instanceof GraphTerm) ty = internIri(LOG_NS + 'Formula');
@@ -3495,6 +3512,23 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3495
3512
  results.push(s2);
3496
3513
  }
3497
3514
 
3515
+ // Also match quoted log:implies triples present as data in the current scope.
3516
+ if (facts && typeof facts.length === 'number') {
3517
+ ensureFactIndexes(facts);
3518
+ const pb = facts.__byPred.get(goal.p.__tid) || [];
3519
+ for (const idx of pb) {
3520
+ const tr = facts[idx];
3521
+
3522
+ let s2 = unifyTerm(goal.s, tr.s, subst);
3523
+ if (s2 === null) continue;
3524
+
3525
+ s2 = unifyTerm(goal.o, tr.o, s2);
3526
+ if (s2 === null) continue;
3527
+
3528
+ results.push(s2);
3529
+ }
3530
+ }
3531
+
3498
3532
  return results;
3499
3533
  }
3500
3534
 
@@ -3524,6 +3558,23 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3524
3558
  results.push(s2);
3525
3559
  }
3526
3560
 
3561
+ // Also match quoted log:impliedBy triples present as data in the current scope.
3562
+ if (facts && typeof facts.length === 'number') {
3563
+ ensureFactIndexes(facts);
3564
+ const pb = facts.__byPred.get(goal.p.__tid) || [];
3565
+ for (const idx of pb) {
3566
+ const tr = facts[idx];
3567
+
3568
+ let s2 = unifyTerm(goal.s, tr.s, subst);
3569
+ if (s2 === null) continue;
3570
+
3571
+ s2 = unifyTerm(goal.o, tr.o, s2);
3572
+ if (s2 === null) continue;
3573
+
3574
+ results.push(s2);
3575
+ }
3576
+ }
3577
+
3527
3578
  return results;
3528
3579
  }
3529
3580
 
package/lib/cli.js CHANGED
@@ -7,7 +7,11 @@
7
7
 
8
8
  'use strict';
9
9
 
10
+ const path = require('node:path');
11
+ const { pathToFileURL } = require('node:url');
12
+
10
13
  const engine = require('./engine');
14
+ const deref = require('./deref');
11
15
  const { PrefixEnv } = require('./prelude');
12
16
 
13
17
  function offsetToLineCol(text, offset) {
@@ -50,6 +54,29 @@ function readTextFromStdinSync() {
50
54
  return fs.readFileSync(0, { encoding: 'utf8' });
51
55
  }
52
56
 
57
+ function __isNetworkOrFileIri(s) {
58
+ return typeof s === 'string' && /^(https?:|file:\/\/)/i.test(s);
59
+ }
60
+
61
+ function __sourceLabelToBaseIri(sourceLabel) {
62
+ if (!sourceLabel || sourceLabel === '<stdin>') return '';
63
+ if (__isNetworkOrFileIri(sourceLabel)) return deref.stripFragment(sourceLabel);
64
+ return pathToFileURL(path.resolve(sourceLabel)).toString();
65
+ }
66
+
67
+ function __readInputSourceSync(sourceLabel) {
68
+ if (sourceLabel === '<stdin>') return readTextFromStdinSync();
69
+
70
+ if (__isNetworkOrFileIri(sourceLabel)) {
71
+ const txt = deref.derefTextSync(sourceLabel);
72
+ if (typeof txt !== 'string') throw new Error(`Failed to dereference ${sourceLabel}`);
73
+ return txt;
74
+ }
75
+
76
+ const fs = require('node:fs');
77
+ return fs.readFileSync(sourceLabel, { encoding: 'utf8' });
78
+ }
79
+
53
80
  function main() {
54
81
  // Drop "node" and script name; keep only user-provided args
55
82
  // Expand combined short options: -pt == -p -t
@@ -163,13 +190,11 @@ function main() {
163
190
  }
164
191
 
165
192
  const sourceLabel = useImplicitStdin || positional[0] === '-' ? '<stdin>' : positional[0];
193
+ const baseIri = __sourceLabelToBaseIri(sourceLabel);
194
+
166
195
  let text;
167
196
  try {
168
- if (sourceLabel === '<stdin>') text = readTextFromStdinSync();
169
- else {
170
- const fs = require('node:fs');
171
- text = fs.readFileSync(sourceLabel, { encoding: 'utf8' });
172
- }
197
+ text = __readInputSourceSync(sourceLabel);
173
198
  } catch (e) {
174
199
  if (sourceLabel === '<stdin>') console.error(`Error reading stdin: ${e.message}`);
175
200
  else console.error(`Error reading file ${JSON.stringify(sourceLabel)}: ${e.message}`);
@@ -181,6 +206,7 @@ function main() {
181
206
  try {
182
207
  toks = engine.lex(text);
183
208
  const parser = new engine.Parser(toks);
209
+ if (baseIri) parser.prefixes.setBase(baseIri);
184
210
  [prefixes, triples, frules, brules, qrules] = parser.parseDocument();
185
211
  // Make the parsed prefixes available to log:trace output (CLI path)
186
212
  engine.setTracePrefixes(prefixes);
package/lib/engine.js CHANGED
@@ -199,21 +199,80 @@ function __isStrictGroundTriple(tr) {
199
199
  // -----------------------------------------------------------------------------
200
200
  // Used to maintain O(1) membership sets for dynamically promoted rules, and to
201
201
  // memoize per-firing head-blank skolemization.
202
+ //
203
+ // Important: variables and blank nodes *inside quoted formulas* are local to the
204
+ // formula. Canonicalize those labels by first occurrence so alpha-equivalent
205
+ // formulas (for example the repeated results of log:semantics after
206
+ // standardize-apart) get the same identity key. Keep top-level blank labels
207
+ // untouched so distinct existential witnesses in the global fact set do not
208
+ // collapse together.
209
+ function __keyFromTermForRuleIdentity(term) {
210
+ const ctx = { quotedVar: new Map(), quotedBlank: new Map() };
211
+
212
+ function canonQuotedVar(name) {
213
+ let out = ctx.quotedVar.get(name);
214
+ if (!out) {
215
+ out = `v${ctx.quotedVar.size}`;
216
+ ctx.quotedVar.set(name, out);
217
+ }
218
+ return out;
219
+ }
220
+
221
+ function canonQuotedBlank(label) {
222
+ let out = ctx.quotedBlank.get(label);
223
+ if (!out) {
224
+ out = `b${ctx.quotedBlank.size}`;
225
+ ctx.quotedBlank.set(label, out);
226
+ }
227
+ return out;
228
+ }
229
+
230
+ function enc(u, inQuotedFormula) {
231
+ if (u instanceof Iri) return ['I', u.value];
232
+ if (u instanceof Literal) return ['L', u.value];
233
+ if (u instanceof Blank) return inQuotedFormula ? ['BQ', canonQuotedBlank(u.label)] : ['B', u.label];
234
+ if (u instanceof Var) return inQuotedFormula ? ['VQ', canonQuotedVar(u.name)] : ['V', u.name];
235
+ if (u instanceof ListTerm) return ['List', u.elems.map((e) => enc(e, inQuotedFormula))];
236
+ if (u instanceof OpenListTerm) {
237
+ return [
238
+ 'OpenList',
239
+ u.prefix.map((e) => enc(e, inQuotedFormula)),
240
+ inQuotedFormula ? ['TailVQ', canonQuotedVar(u.tailVar)] : ['TailV', u.tailVar],
241
+ ];
242
+ }
243
+ if (u instanceof GraphTerm)
244
+ return ['Graph', u.triples.map((tr) => [enc(tr.s, true), enc(tr.p, true), enc(tr.o, true)])];
245
+ return ['Other', String(u)];
246
+ }
247
+
248
+ return JSON.stringify(enc(term, false));
249
+ }
250
+
202
251
  function __ruleKey(isForward, isFuse, premise, conclusion, dynamicConclusionTerm /* optional */) {
203
252
  let out = (isForward ? 'F' : 'B') + (isFuse ? '!' : '') + '|P|';
204
253
  for (let i = 0; i < premise.length; i++) {
205
254
  const tr = premise[i];
206
255
  if (i) out += '\n';
207
- out += skolemKeyFromTerm(tr.s) + '\t' + skolemKeyFromTerm(tr.p) + '\t' + skolemKeyFromTerm(tr.o);
256
+ out +=
257
+ __keyFromTermForRuleIdentity(tr.s) +
258
+ '\t' +
259
+ __keyFromTermForRuleIdentity(tr.p) +
260
+ '\t' +
261
+ __keyFromTermForRuleIdentity(tr.o);
208
262
  }
209
263
  out += '|C|';
210
264
  for (let i = 0; i < conclusion.length; i++) {
211
265
  const tr = conclusion[i];
212
266
  if (i) out += '\n';
213
- out += skolemKeyFromTerm(tr.s) + '\t' + skolemKeyFromTerm(tr.p) + '\t' + skolemKeyFromTerm(tr.o);
267
+ out +=
268
+ __keyFromTermForRuleIdentity(tr.s) +
269
+ '\t' +
270
+ __keyFromTermForRuleIdentity(tr.p) +
271
+ '\t' +
272
+ __keyFromTermForRuleIdentity(tr.o);
214
273
  }
215
274
  if (dynamicConclusionTerm) {
216
- out += '|T|' + skolemKeyFromTerm(dynamicConclusionTerm);
275
+ out += '|T|' + __keyFromTermForRuleIdentity(dynamicConclusionTerm);
217
276
  }
218
277
  return out;
219
278
  }
@@ -224,7 +283,12 @@ function __firingKey(ruleIndex, instantiatedPremises) {
224
283
  for (let i = 0; i < instantiatedPremises.length; i++) {
225
284
  const tr = instantiatedPremises[i];
226
285
  if (i) out += '\n';
227
- out += skolemKeyFromTerm(tr.s) + '\t' + skolemKeyFromTerm(tr.p) + '\t' + skolemKeyFromTerm(tr.o);
286
+ out +=
287
+ __keyFromTermForRuleIdentity(tr.s) +
288
+ '\t' +
289
+ __keyFromTermForRuleIdentity(tr.p) +
290
+ '\t' +
291
+ __keyFromTermForRuleIdentity(tr.o);
228
292
  }
229
293
  return out;
230
294
  }
@@ -2464,7 +2528,17 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
2464
2528
  if (remaining <= 0) continue;
2465
2529
  const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
2466
2530
 
2467
- let deltas = evalBuiltin(goal0, {}, facts, backRules, frame.curDepth, varGen, builtinMax);
2531
+ const builtinGoalForEval = goalPredicateIri === LOG_NS + 'rawType' ? rawGoal : goal0;
2532
+ const builtinSubstForEval = goalPredicateIri === LOG_NS + 'rawType' ? substMut : {};
2533
+ let deltas = evalBuiltin(
2534
+ builtinGoalForEval,
2535
+ builtinSubstForEval,
2536
+ facts,
2537
+ backRules,
2538
+ frame.curDepth,
2539
+ varGen,
2540
+ builtinMax,
2541
+ );
2468
2542
 
2469
2543
  const dc = typeof frame.deferCount === 'number' ? frame.deferCount : 0;
2470
2544
  const builtinDeltasAreVacuous = deltas.length > 0 && deltas.every((d) => Object.keys(d).length === 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.22.5",
3
+ "version": "1.22.7",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
4
5
  const path = require('node:path');
5
6
  const { spawnSync } = require('node:child_process');
6
7
  const ROOT = path.resolve(__dirname, '..');
@@ -2178,6 +2179,132 @@ _:x :hates { _:foo :making :mess }.
2178
2179
  /^"3"\^\^xsd:integer\s+:is\s+\("3"\s+xsd:integer\)\s*\./m,
2179
2180
  ],
2180
2181
  },
2182
+ {
2183
+ name: '245 regression: log:includes sees quoted log:implies triples as data',
2184
+ opt: { proofComments: false },
2185
+ input: `@prefix : <http://example.org/> .
2186
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
2187
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
2188
+
2189
+ <> :data {
2190
+ {
2191
+ ?A a ?B .
2192
+ ?B rdfs:subClassOf ?C.
2193
+ }
2194
+ =>
2195
+ {
2196
+ ?A a ?C .
2197
+ }.
2198
+ }.
2199
+
2200
+ {
2201
+ ?W :data ?F.
2202
+ ?F log:includes { ?X log:implies ?Y }.
2203
+ }
2204
+ =>
2205
+ {
2206
+ :result :is ?X .
2207
+ }.
2208
+ `,
2209
+ expect: [/^:result\s+:is\s+\{[\s\S]*\?A\s+a\s+\?B\s*\.[\s\S]*\?B\s+rdfs:subClassOf\s+\?C\s*\.[\s\S]*\}\s*\./m],
2210
+ },
2211
+ {
2212
+ name: '246 regression: log:includes sees quoted log:impliedBy triples as data',
2213
+ opt: { proofComments: false },
2214
+ input: `@prefix : <http://example.org/> .
2215
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
2216
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
2217
+
2218
+ <> :data {
2219
+ {
2220
+ ?A a ?C .
2221
+ }
2222
+ <=
2223
+ {
2224
+ ?A a ?B .
2225
+ ?B rdfs:subClassOf ?C.
2226
+ }.
2227
+ }.
2228
+
2229
+ {
2230
+ ?W :data ?F.
2231
+ ?F log:includes { ?Y <= ?X }.
2232
+ }
2233
+ =>
2234
+ {
2235
+ :result :is ?X .
2236
+ }.
2237
+ `,
2238
+ expect: [/^:result\s+:is\s+\{[\s\S]*\?A\s+a\s+\?B\s*\.[\s\S]*\?B\s+rdfs:subClassOf\s+\?C\s*\.[\s\S]*\}\s*\./m],
2239
+ },
2240
+ {
2241
+ name: 'regression: log:rawType accepts quoted variables found via log:includes',
2242
+ opt: { proofComments: false },
2243
+ input: `@prefix log: <http://www.w3.org/2000/10/swap/log#> .
2244
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2245
+ @prefix : <http://example.org/ns#> .
2246
+
2247
+ { ?X :likes ?Y } <= { ?X :loves ?Y }.
2248
+
2249
+ {
2250
+ ?X log:impliedBy ?Y .
2251
+ ?X log:includes { ?Z1 :likes ?Z2 }.
2252
+ ?Z1 log:rawType ?T.
2253
+ }
2254
+ =>
2255
+ {
2256
+ :test :is true .
2257
+ }.
2258
+ `,
2259
+ expect: [/^:test\s+:is\s+true\s*\./m],
2260
+ },
2261
+ {
2262
+ name: 'regression: log:semantics body alpha-renaming does not refire blank-head rule forever',
2263
+ async run() {
2264
+ const os = require('node:os');
2265
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-alpha-fire-'));
2266
+ const mainPath = path.join(tmp, 'main.n3');
2267
+ const examplePath = path.join(tmp, 'example.n3');
2268
+
2269
+ fs.writeFileSync(
2270
+ mainPath,
2271
+ `@prefix log: <http://www.w3.org/2000/10/swap/log#> .
2272
+ @prefix : <urn:test#>.
2273
+
2274
+ <> :facts <./example.n3> .
2275
+
2276
+ { ?X :load ?S } <= { <> :facts ?F . ?F log:semantics ?S . }.
2277
+
2278
+ { ?This :load ?S . } => { [] a :Test . }.
2279
+ `,
2280
+ 'utf8',
2281
+ );
2282
+
2283
+ fs.writeFileSync(
2284
+ examplePath,
2285
+ `@prefix : <urn:test#>.
2286
+ { ?A :p ?B } <= { ?A :q ?B }.
2287
+ `,
2288
+ 'utf8',
2289
+ );
2290
+
2291
+ try {
2292
+ const r = spawnSync(process.execPath, [path.join(ROOT, 'bin', 'eyeling.cjs'), mainPath], {
2293
+ cwd: ROOT,
2294
+ encoding: 'utf8',
2295
+ maxBuffer: DEFAULT_MAX_BUFFER,
2296
+ timeout: 5000,
2297
+ });
2298
+
2299
+ if (r.error) throw r.error;
2300
+ assert.equal(r.status, 0, r.stderr || `unexpected exit ${r.status}`);
2301
+ mustOccurExactly(r.stdout, /^_:sk_\d+\s+a\s+:Test\s*\.$/gm, 1, 'expected exactly one derived :Test witness');
2302
+ return r.stdout;
2303
+ } finally {
2304
+ fs.rmSync(tmp, { recursive: true, force: true });
2305
+ }
2306
+ },
2307
+ },
2181
2308
  ];
2182
2309
 
2183
2310
  let passed = 0;