eyeling 1.17.2 → 1.18.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/lib/builtins.js CHANGED
@@ -61,6 +61,152 @@ function __useNumericCacheKey(key) {
61
61
  return typeof key === 'string' && key.length <= MAX_NUMERIC_CACHE_KEY_LEN;
62
62
  }
63
63
 
64
+ // ---------------------------------------------------------------------------
65
+ // Custom builtin registry
66
+ // ---------------------------------------------------------------------------
67
+ const __customBuiltinHandlers = new Map(); // predicate IRI -> evaluator(ctx) => deltas[]
68
+ const __loadedBuiltinModuleIds = new Set();
69
+
70
+ function __validateBuiltinIri(iri) {
71
+ if (typeof iri !== 'string' || !iri) {
72
+ throw new TypeError('Custom builtin IRI must be a non-empty string');
73
+ }
74
+ }
75
+
76
+ function registerBuiltin(iri, handler) {
77
+ __validateBuiltinIri(iri);
78
+ if (typeof handler !== 'function') {
79
+ throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
80
+ }
81
+ __customBuiltinHandlers.set(iri, handler);
82
+ return handler;
83
+ }
84
+
85
+ function unregisterBuiltin(iri) {
86
+ return __customBuiltinHandlers.delete(iri);
87
+ }
88
+
89
+ function listBuiltinIris() {
90
+ return Array.from(__customBuiltinHandlers.keys()).sort();
91
+ }
92
+
93
+ function __buildBuiltinRegistrationApi() {
94
+ return {
95
+ registerBuiltin,
96
+ unregisterBuiltin,
97
+ listBuiltinIris,
98
+ internIri,
99
+ internLiteral,
100
+ literalParts,
101
+ termToJsString,
102
+ termToJsStringDecoded,
103
+ termToN3,
104
+ iriValue,
105
+ unifyTerm,
106
+ applySubstTerm,
107
+ applySubstTriple,
108
+ proveGoals,
109
+ isGroundTerm,
110
+ computeConclusionFromFormula,
111
+ skolemIriFromGroundTerm,
112
+ parseBooleanLiteralInfo,
113
+ parseNumericLiteralInfo,
114
+ parseXsdDecimalToBigIntScale,
115
+ pow10n,
116
+ normalizeLiteralForFastKey,
117
+ literalsEquivalentAsXsdString,
118
+ materializeRdfLists,
119
+ terms: { Literal, Iri, Var, Blank, ListTerm, OpenListTerm, GraphTerm, Triple, Rule },
120
+ ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
121
+ };
122
+ }
123
+
124
+ function registerBuiltinModule(mod, origin = '<builtin-module>') {
125
+ if (!mod) throw new TypeError(`Builtin module ${origin} did not export anything`);
126
+
127
+ const api = __buildBuiltinRegistrationApi();
128
+
129
+ if (typeof mod === 'function') {
130
+ mod(api);
131
+ return true;
132
+ }
133
+
134
+ if (typeof mod.register === 'function') {
135
+ mod.register(api);
136
+ return true;
137
+ }
138
+
139
+ const candidates = [];
140
+ if (mod && typeof mod.builtins === 'object' && mod.builtins) candidates.push(mod.builtins);
141
+ if (mod && typeof mod.default === 'object' && mod.default) candidates.push(mod.default);
142
+ candidates.push(mod);
143
+
144
+ let registeredAny = false;
145
+ for (const obj of candidates) {
146
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) continue;
147
+ for (const [iri, handler] of Object.entries(obj)) {
148
+ if (typeof handler !== 'function') continue;
149
+ registerBuiltin(iri, handler);
150
+ registeredAny = true;
151
+ }
152
+ if (registeredAny) return true;
153
+ }
154
+
155
+ throw new TypeError(
156
+ `Builtin module ${origin} must export a function, a { register() } object, or an object mapping predicate IRIs to handlers`,
157
+ );
158
+ }
159
+
160
+ function loadBuiltinModule(specifier, options = {}) {
161
+ if (typeof require !== 'function') {
162
+ throw new Error('Custom builtin modules can only be loaded when require() is available');
163
+ }
164
+ if (typeof specifier !== 'string' || !specifier) {
165
+ throw new TypeError('Builtin module specifier must be a non-empty string');
166
+ }
167
+
168
+ const path = require('node:path');
169
+ const resolved = options && options.resolveFrom ? path.resolve(options.resolveFrom, specifier) : specifier;
170
+ const moduleId = String(resolved);
171
+ if (__loadedBuiltinModuleIds.has(moduleId)) return moduleId;
172
+
173
+ const loaded = require(resolved);
174
+ registerBuiltinModule(loaded, moduleId);
175
+ __loadedBuiltinModuleIds.add(moduleId);
176
+ return moduleId;
177
+ }
178
+
179
+ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGen, maxResults) {
180
+ const handler = __customBuiltinHandlers.get(pv);
181
+ if (typeof handler !== 'function') return null;
182
+
183
+ const ctx = {
184
+ iri: pv,
185
+ goal,
186
+ subst,
187
+ facts,
188
+ backRules,
189
+ depth,
190
+ varGen,
191
+ maxResults,
192
+ api: __buildBuiltinRegistrationApi(),
193
+ };
194
+
195
+ try {
196
+ const out = handler(ctx);
197
+ if (out == null) return [];
198
+ if (!Array.isArray(out)) {
199
+ throw new TypeError(`Custom builtin ${pv} must return an array of substitution deltas`);
200
+ }
201
+ return out;
202
+ } catch (err) {
203
+ if (err && typeof err === 'object' && typeof err.message === 'string') {
204
+ err.message = `Error in custom builtin ${pv}: ${err.message}`;
205
+ }
206
+ throw err;
207
+ }
208
+ }
209
+
64
210
  //
65
211
  // Engine hooks (injected once by makeBuiltins)
66
212
  //
@@ -1466,6 +1612,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
1466
1612
  if (pv !== allow1 && pv !== allow2) return [];
1467
1613
  }
1468
1614
 
1615
+ const registeredBuiltinResult = __evalRegisteredBuiltin(pv, g, subst, facts, backRules, depth, varGen, maxResults);
1616
+ if (registeredBuiltinResult !== null) return registeredBuiltinResult;
1617
+
1469
1618
  // -----------------------------------------------------------------
1470
1619
  // 4.1 crypto: builtins
1471
1620
  // -----------------------------------------------------------------
@@ -3677,6 +3826,8 @@ function isBuiltinPred(p) {
3677
3826
  return true;
3678
3827
  }
3679
3828
 
3829
+ if (__customBuiltinHandlers.has(v)) return true;
3830
+
3680
3831
  return (
3681
3832
  v.startsWith(CRYPTO_NS) ||
3682
3833
  v.startsWith(MATH_NS) ||
@@ -3834,6 +3985,11 @@ function listHasTriple(list, tr) {
3834
3985
 
3835
3986
  module.exports = {
3836
3987
  makeBuiltins,
3988
+ registerBuiltin,
3989
+ unregisterBuiltin,
3990
+ registerBuiltinModule,
3991
+ loadBuiltinModule,
3992
+ listBuiltinIris,
3837
3993
  // shared helpers used by engine/explain
3838
3994
  parseBooleanLiteralInfo,
3839
3995
  parseNumericLiteralInfo,
package/lib/cli.js CHANGED
@@ -55,7 +55,7 @@ function main() {
55
55
  argv.push(a);
56
56
  continue;
57
57
  }
58
- // Combined short flags (no flag in eyeling takes a value)
58
+ // Combined short flags (the long --builtin option takes a value)
59
59
  for (const ch of a.slice(1)) argv.push('-' + ch);
60
60
  }
61
61
  const prog = String(process.argv[1] || 'eyeling')
@@ -67,6 +67,7 @@ function main() {
67
67
  `Usage: ${prog} [options] <file.n3>\n\n` +
68
68
  `Options:\n` +
69
69
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
70
+ ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
70
71
  ` -d, --deterministic-skolem Make log:skolem stable across reasoning runs.\n` +
71
72
  ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
72
73
  ` -h, --help Show this help and exit.\n` +
@@ -89,6 +90,27 @@ function main() {
89
90
  process.exit(0);
90
91
  }
91
92
 
93
+ const builtinModules = [];
94
+ const positional = [];
95
+ for (let i = 0; i < argv.length; i++) {
96
+ const a = argv[i];
97
+ if (a === '--builtin') {
98
+ const next = argv[i + 1];
99
+ if (!next || next.startsWith('-')) {
100
+ console.error('Error: --builtin expects a module path.');
101
+ process.exit(1);
102
+ }
103
+ builtinModules.push(next);
104
+ i += 1;
105
+ continue;
106
+ }
107
+ if (typeof a === 'string' && a.startsWith('--builtin=')) {
108
+ builtinModules.push(a.slice('--builtin='.length));
109
+ continue;
110
+ }
111
+ if (!a.startsWith('-')) positional.push(a);
112
+ }
113
+
92
114
  const showAst = argv.includes('--ast') || argv.includes('-a');
93
115
  const streamMode = argv.includes('--stream') || argv.includes('-t');
94
116
 
@@ -113,7 +135,6 @@ function main() {
113
135
  }
114
136
 
115
137
  // Positional args (the N3 file)
116
- const positional = argv.filter((a) => !a.startsWith('-'));
117
138
  if (positional.length === 0) {
118
139
  printHelp(false);
119
140
  process.exit(0);
@@ -124,6 +145,16 @@ function main() {
124
145
  process.exit(1);
125
146
  }
126
147
 
148
+ for (const spec of builtinModules) {
149
+ try {
150
+ if (typeof engine.loadBuiltinModule === 'function')
151
+ engine.loadBuiltinModule(spec, { resolveFrom: process.cwd() });
152
+ } catch (e) {
153
+ console.error(`Error loading builtin module ${JSON.stringify(spec)}: ${e && e.message ? e.message : String(e)}`);
154
+ process.exit(1);
155
+ }
156
+ }
157
+
127
158
  const filePath = positional[0];
128
159
  let text;
129
160
  try {
package/lib/engine.js CHANGED
@@ -40,6 +40,11 @@ const { liftBlankRuleVars } = require('./rules');
40
40
 
41
41
  const {
42
42
  makeBuiltins,
43
+ registerBuiltin,
44
+ unregisterBuiltin,
45
+ registerBuiltinModule,
46
+ loadBuiltinModule,
47
+ listBuiltinIris,
43
48
  // helpers used by engine core
44
49
  parseBooleanLiteralInfo,
45
50
  parseNumericLiteralInfo,
@@ -526,6 +531,10 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
526
531
  termsEqualNoIntDecimal,
527
532
  });
528
533
 
534
+ try {
535
+ registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
536
+ } catch (_) {}
537
+
529
538
  // Initialize proof/output helpers (implemented in lib/explain.js).
530
539
  const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
531
540
  applySubstTerm,
@@ -3194,6 +3203,7 @@ function reasonStream(input, opts = {}) {
3194
3203
  enforceHttps = false,
3195
3204
  rdfjs = false,
3196
3205
  dataFactory = null,
3206
+ builtinModules = null,
3197
3207
  } = opts;
3198
3208
 
3199
3209
  const parsedInput = normalizeParsedReasonerInputSync(input);
@@ -3203,6 +3213,12 @@ function reasonStream(input, opts = {}) {
3203
3213
  deref.setEnforceHttpsEnabled(!!enforceHttps);
3204
3214
  proofCommentsEnabled = !!proof;
3205
3215
 
3216
+ if (Array.isArray(builtinModules)) {
3217
+ for (const spec of builtinModules) loadBuiltinModule(spec);
3218
+ } else if (typeof builtinModules === 'string' && builtinModules) {
3219
+ loadBuiltinModule(builtinModules);
3220
+ }
3221
+
3206
3222
  let prefixes, triples, frules, brules, logQueryRules;
3207
3223
 
3208
3224
  if (parsedInput) {
@@ -3427,4 +3443,9 @@ module.exports = {
3427
3443
  setTracePrefixes,
3428
3444
  getDeterministicSkolemEnabled,
3429
3445
  setDeterministicSkolemEnabled,
3446
+ registerBuiltin,
3447
+ unregisterBuiltin,
3448
+ registerBuiltinModule,
3449
+ loadBuiltinModule,
3450
+ listBuiltinIris,
3430
3451
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.17.2",
3
+ "version": "1.18.1",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -1762,6 +1762,35 @@ _:x :hates { _:foo :making :mess }.
1762
1762
  },
1763
1763
  expect: [/http:\/\/example\.org\/ancestor/m],
1764
1764
  },
1765
+ {
1766
+ name: '240 custom builtin module can be loaded via --builtin',
1767
+ run() {
1768
+ const tmp = require('node:fs').mkdtempSync(
1769
+ require('node:path').join(require('node:os').tmpdir(), 'eyeling-builtin-'),
1770
+ );
1771
+ const modPath = require('node:path').join(tmp, 'hello-builtin.js');
1772
+ require('node:fs').writeFileSync(
1773
+ modPath,
1774
+ `module.exports = ({ registerBuiltin, internLiteral, unifyTerm, terms }) => {\n` +
1775
+ ` const { Var } = terms;\n` +
1776
+ ` registerBuiltin("http://example.org/custom#hello", ({ goal, subst }) => {\n` +
1777
+ ` const lit = internLiteral("\\"world\\"");\n` +
1778
+ ` if (goal.o instanceof Var) { const s2 = { ...subst }; s2[goal.o.name] = lit; return [s2]; }\n` +
1779
+ ` const s2 = unifyTerm(goal.o, lit, subst);\n` +
1780
+ ` return s2 !== null ? [s2] : [];\n` +
1781
+ ` });\n` +
1782
+ `};\n`,
1783
+ 'utf8',
1784
+ );
1785
+ const out = reasonQuiet(
1786
+ { builtinModules: [modPath] },
1787
+ `@prefix : <http://example.org/> .\n@prefix cb: <http://example.org/custom#> .\n{ :x cb:hello ?o . } => { :x :value ?o . } .\n:x cb:hello ?o .\n`,
1788
+ );
1789
+ require('node:fs').rmSync(tmp, { recursive: true, force: true });
1790
+ return out;
1791
+ },
1792
+ expect: [/:x :value "world" \./m],
1793
+ },
1765
1794
  ];
1766
1795
 
1767
1796
  let passed = 0;