eyeling 1.19.0 → 1.19.2
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/HANDBOOK.md +76 -12
- package/eyeling.js +167 -9
- package/lib/builtin-contract.js +121 -0
- package/lib/builtins.js +43 -9
- package/package.json +3 -2
- package/test/api.test.js +36 -0
- package/test/builtin-contract.test.js +147 -0
- package/test/fixtures/builtins/bad-export.js +3 -0
- package/test/fixtures/builtins/bad-return.js +5 -0
- package/test/fixtures/builtins/ok-builtins.js +7 -0
- package/test/fixtures/builtins/ok-default-map.js +7 -0
- package/test/fixtures/builtins/ok-map.js +5 -0
- package/test/fixtures/builtins/ok-register.js +7 -0
package/HANDBOOK.md
CHANGED
|
@@ -1820,6 +1820,7 @@ The bundle contains the whole engine. The CLI path is the “canonical behavior
|
|
|
1820
1820
|
The current CLI supports a small set of flags (see `lib/cli.js`):
|
|
1821
1821
|
|
|
1822
1822
|
- `-a`, `--ast` — print the parsed AST as JSON and exit.
|
|
1823
|
+
- `--builtin <module.js>` — load a custom builtin module (repeatable).
|
|
1823
1824
|
- `-d`, `--deterministic-skolem` — make `log:skolem` stable across runs.
|
|
1824
1825
|
- `-e`, `--enforce-https` — rewrite `http://…` to `https://…` for dereferencing builtins.
|
|
1825
1826
|
- `-p`, `--proof-comments` — include per-fact proof comment blocks in output.
|
|
@@ -2084,9 +2085,26 @@ const { reason } = require('eyeling');
|
|
|
2084
2085
|
const out = reason({ builtinModules: ['./hello-builtin.js'] }, n3Text);
|
|
2085
2086
|
```
|
|
2086
2087
|
|
|
2088
|
+
### 16.2.1 Stability rule for `--builtin`
|
|
2089
|
+
|
|
2090
|
+
Eyeling now treats the custom-builtin boundary as a **versioned API contract** rather than an informal convenience layer.
|
|
2091
|
+
|
|
2092
|
+
That matters for LLM-generated builtin modules: the model is free to decide **when** a builtin should be used, but it is **not** free to rename helpers, invent new helper names, change handler context fields, or change the expected return shape.
|
|
2093
|
+
|
|
2094
|
+
The stable rule is:
|
|
2095
|
+
|
|
2096
|
+
- builtin module loading accepts only the declared export forms
|
|
2097
|
+
- the helper API exposed by `__buildBuiltinRegistrationApi()` has an **exact key set**
|
|
2098
|
+
- helper names and helper arities are treated as public contract
|
|
2099
|
+
- builtin handlers receive an **exact** context object shape
|
|
2100
|
+
- builtin handlers must return an **array of substitution-delta objects**
|
|
2101
|
+
- any add/remove/rename of these contract elements is a **breaking change** and should bump the builtin API version
|
|
2102
|
+
|
|
2103
|
+
In code, this contract lives in `lib/builtin-contract.js`, is enforced at runtime by `lib/builtins.js`, and is locked by `test/builtin-contract.test.js`.
|
|
2104
|
+
|
|
2087
2105
|
### 16.3 What a builtin module may export
|
|
2088
2106
|
|
|
2089
|
-
Eyeling accepts
|
|
2107
|
+
Eyeling accepts these stable module shapes.
|
|
2090
2108
|
|
|
2091
2109
|
#### A function export
|
|
2092
2110
|
|
|
@@ -2123,23 +2141,54 @@ module.exports = {
|
|
|
2123
2141
|
};
|
|
2124
2142
|
```
|
|
2125
2143
|
|
|
2144
|
+
#### An object with `.builtins`
|
|
2145
|
+
|
|
2146
|
+
```js
|
|
2147
|
+
module.exports = {
|
|
2148
|
+
builtins: {
|
|
2149
|
+
'http://example.org/custom#ok': ({ subst }) => [subst],
|
|
2150
|
+
},
|
|
2151
|
+
};
|
|
2152
|
+
```
|
|
2153
|
+
|
|
2154
|
+
#### An object with `.default` as a plain object map
|
|
2155
|
+
|
|
2156
|
+
This is mainly an ESM/transpiler compatibility form.
|
|
2157
|
+
|
|
2158
|
+
```js
|
|
2159
|
+
module.exports = {
|
|
2160
|
+
default: {
|
|
2161
|
+
'http://example.org/custom#ok': ({ subst }) => [subst],
|
|
2162
|
+
},
|
|
2163
|
+
};
|
|
2164
|
+
```
|
|
2165
|
+
|
|
2126
2166
|
If none of those shapes match, Eyeling rejects the module with a descriptive error.
|
|
2127
2167
|
|
|
2128
2168
|
### 16.4 The handler contract
|
|
2129
2169
|
|
|
2130
|
-
Builtin handlers are called with
|
|
2170
|
+
Builtin handlers are called with an **exactly versioned** context object:
|
|
2131
2171
|
|
|
2132
2172
|
- `iri` — the predicate IRI string
|
|
2133
2173
|
- `goal` — the current triple goal
|
|
2134
2174
|
- `subst` — the current substitution
|
|
2135
|
-
- `facts
|
|
2175
|
+
- `facts` — the active fact store
|
|
2176
|
+
- `backRules` — the backward-rule set
|
|
2177
|
+
- `depth` — current proof depth
|
|
2178
|
+
- `varGen` — the variable generator state
|
|
2179
|
+
- `maxResults` — current result cap
|
|
2136
2180
|
- `api` — the same registration/helper API used by modules
|
|
2137
2181
|
|
|
2138
|
-
|
|
2182
|
+
The exact key set is part of the contract; adding, removing, or renaming a context field is a breaking change.
|
|
2183
|
+
|
|
2184
|
+
A handler returns **an array of substitution-delta objects**:
|
|
2139
2185
|
|
|
2140
2186
|
- `[]` means failure / no solutions
|
|
2141
|
-
- `[
|
|
2142
|
-
-
|
|
2187
|
+
- `[{}]` means success with no new bindings
|
|
2188
|
+
- `[{ ...delta }]` means one successful continuation with bindings
|
|
2189
|
+
- multiple objects mean a generator builtin
|
|
2190
|
+
|
|
2191
|
+
Returning a non-array, `null` elements, or non-object delta elements is rejected by the runtime contract wrapper.
|
|
2143
2192
|
|
|
2144
2193
|
In practice:
|
|
2145
2194
|
|
|
@@ -2152,13 +2201,28 @@ Custom builtin failures are wrapped so the predicate IRI appears in the thrown e
|
|
|
2152
2201
|
|
|
2153
2202
|
### 16.5 The helper API exposed to builtin modules
|
|
2154
2203
|
|
|
2155
|
-
Builtin modules do not need to import internal engine files directly. Eyeling passes a helper API into module registration,
|
|
2204
|
+
Builtin modules do not need to import internal engine files directly. Eyeling passes a helper API into module registration, and that helper surface is now treated as an **exact public contract**.
|
|
2205
|
+
|
|
2206
|
+
The current builtin API version is exposed as:
|
|
2207
|
+
|
|
2208
|
+
- `getBuiltinApiVersion()`
|
|
2209
|
+
|
|
2210
|
+
The stable helper function set is:
|
|
2211
|
+
|
|
2212
|
+
- `registerBuiltin`, `unregisterBuiltin`, `listBuiltinIris`
|
|
2213
|
+
- `internIri`, `internLiteral`, `literalParts`
|
|
2214
|
+
- `termToJsString`, `termToJsStringDecoded`, `termToN3`, `iriValue`
|
|
2215
|
+
- `unifyTerm`, `applySubstTerm`, `applySubstTriple`, `proveGoals`, `isGroundTerm`
|
|
2216
|
+
- `computeConclusionFromFormula`, `skolemIriFromGroundTerm`
|
|
2217
|
+
- `parseBooleanLiteralInfo`, `parseNumericLiteralInfo`, `parseXsdDecimalToBigIntScale`, `pow10n`
|
|
2218
|
+
- `normalizeLiteralForFastKey`, `literalsEquivalentAsXsdString`, `materializeRdfLists`
|
|
2219
|
+
|
|
2220
|
+
The stable namespace bags are:
|
|
2221
|
+
|
|
2222
|
+
- `terms`: `Literal`, `Iri`, `Var`, `Blank`, `ListTerm`, `OpenListTerm`, `GraphTerm`, `Triple`, `Rule`
|
|
2223
|
+
- `ns`: `RDF_NS`, `XSD_NS`, `CRYPTO_NS`, `MATH_NS`, `TIME_NS`, `LIST_NS`, `LOG_NS`, `STRING_NS`
|
|
2156
2224
|
|
|
2157
|
-
|
|
2158
|
-
- term constructors via `terms` (`Literal`, `Iri`, `Var`, `Blank`, `ListTerm`, `GraphTerm`, `Triple`, `Rule`, ...)
|
|
2159
|
-
- literal/term helpers such as `internLiteral`, `internIri`, `literalParts`, `termToN3`, `termToJsString`
|
|
2160
|
-
- reasoning helpers such as `unifyTerm`, `applySubstTerm`, `applySubstTriple`, `proveGoals`
|
|
2161
|
-
- namespace constants via `ns`
|
|
2225
|
+
The contract is intentionally strict: if a helper is added, removed, renamed, or its callable shape changes, the builtin contract tests fail until the change is made explicit and the version is bumped.
|
|
2162
2226
|
|
|
2163
2227
|
That API keeps the extension boundary explicit: custom builtins get the operations they need without reaching into Eyeling’s private module graph.
|
|
2164
2228
|
|
package/eyeling.js
CHANGED
|
@@ -9,6 +9,130 @@
|
|
|
9
9
|
const __cache = Object.create(null);
|
|
10
10
|
|
|
11
11
|
// ---- bundled modules ----
|
|
12
|
+
__modules["lib/builtin-contract.js"] = function(require, module, exports){
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const CONTRACT = {
|
|
16
|
+
version: 1,
|
|
17
|
+
moduleExportForms: [
|
|
18
|
+
'function',
|
|
19
|
+
'object.register',
|
|
20
|
+
'object.builtins',
|
|
21
|
+
'plain-object-map',
|
|
22
|
+
'object.default-object-map',
|
|
23
|
+
],
|
|
24
|
+
api: {
|
|
25
|
+
functions: {
|
|
26
|
+
getBuiltinApiVersion: { arity: 0, required: true },
|
|
27
|
+
registerBuiltin: { arity: 2, required: true },
|
|
28
|
+
unregisterBuiltin: { arity: 1, required: true },
|
|
29
|
+
listBuiltinIris: { arity: 0, required: true },
|
|
30
|
+
internIri: { arity: 1, required: true },
|
|
31
|
+
internLiteral: { arity: 1, required: true },
|
|
32
|
+
literalParts: { arity: 1, required: true },
|
|
33
|
+
termToJsString: { arity: 1, required: true },
|
|
34
|
+
termToJsStringDecoded: { arity: 1, required: true },
|
|
35
|
+
termToN3: { arity: 2, required: true },
|
|
36
|
+
iriValue: { arity: 1, required: true },
|
|
37
|
+
unifyTerm: { arity: 3, required: true },
|
|
38
|
+
applySubstTerm: { arity: 2, required: true },
|
|
39
|
+
applySubstTriple: { arity: 2, required: true },
|
|
40
|
+
proveGoals: { arity: 9, required: true },
|
|
41
|
+
isGroundTerm: { arity: 1, required: true },
|
|
42
|
+
computeConclusionFromFormula: { arity: 1, required: true },
|
|
43
|
+
skolemIriFromGroundTerm: { arity: 1, required: true },
|
|
44
|
+
parseBooleanLiteralInfo: { arity: 1, required: true },
|
|
45
|
+
parseNumericLiteralInfo: { arity: 1, required: true },
|
|
46
|
+
parseXsdDecimalToBigIntScale: { arity: 1, required: true },
|
|
47
|
+
pow10n: { arity: 1, required: true },
|
|
48
|
+
normalizeLiteralForFastKey: { arity: 1, required: true },
|
|
49
|
+
literalsEquivalentAsXsdString: { arity: 2, required: true },
|
|
50
|
+
materializeRdfLists: { arity: 3, required: true },
|
|
51
|
+
},
|
|
52
|
+
namespaces: {
|
|
53
|
+
terms: ['Literal', 'Iri', 'Var', 'Blank', 'ListTerm', 'OpenListTerm', 'GraphTerm', 'Triple', 'Rule'],
|
|
54
|
+
ns: ['RDF_NS', 'XSD_NS', 'CRYPTO_NS', 'MATH_NS', 'TIME_NS', 'LIST_NS', 'LOG_NS', 'STRING_NS'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
handler: {
|
|
58
|
+
ctxKeys: ['iri', 'goal', 'subst', 'facts', 'backRules', 'depth', 'varGen', 'maxResults', 'api'],
|
|
59
|
+
return: 'array-of-substitution-deltas',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const EXACT_API_KEYS = new Set([...Object.keys(CONTRACT.api.functions), ...Object.keys(CONTRACT.api.namespaces)]);
|
|
64
|
+
|
|
65
|
+
function assertExactKeys(obj, expectedKeys, label) {
|
|
66
|
+
const got = Object.keys(obj).sort();
|
|
67
|
+
const exp = Array.from(expectedKeys).sort();
|
|
68
|
+
if (got.length !== exp.length || got.some((k, i) => k !== exp[i])) {
|
|
69
|
+
throw new Error(`${label} keys changed. expected: ${exp.join(', ')}; got: ${got.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertFunctionArity(name, fn, spec) {
|
|
74
|
+
if (typeof fn !== 'function') throw new TypeError(`Builtin API member ${name} must be a function`);
|
|
75
|
+
if (Number.isInteger(spec.arity) && fn.length !== spec.arity) {
|
|
76
|
+
throw new Error(`Builtin API member ${name} arity changed: expected ${spec.arity}, got ${fn.length}`);
|
|
77
|
+
}
|
|
78
|
+
if (Number.isInteger(spec.arityMin) && fn.length < spec.arityMin) {
|
|
79
|
+
throw new Error(`Builtin API member ${name} arity too small: expected >= ${spec.arityMin}, got ${fn.length}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function deepFreezeBuiltinApi(api) {
|
|
84
|
+
Object.freeze(api.terms);
|
|
85
|
+
Object.freeze(api.ns);
|
|
86
|
+
return Object.freeze(api);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertBuiltinApiShape(api) {
|
|
90
|
+
assertExactKeys(api, EXACT_API_KEYS, 'Builtin registration API');
|
|
91
|
+
|
|
92
|
+
for (const [name, spec] of Object.entries(CONTRACT.api.functions)) {
|
|
93
|
+
assertFunctionArity(name, api[name], spec);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const name of CONTRACT.api.namespaces.terms) {
|
|
97
|
+
if (!api.terms || typeof api.terms[name] !== 'function') {
|
|
98
|
+
throw new TypeError(`Builtin API terms.${name} missing or invalid`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const name of CONTRACT.api.namespaces.ns) {
|
|
103
|
+
if (!api.ns || typeof api.ns[name] !== 'string') {
|
|
104
|
+
throw new TypeError(`Builtin API ns.${name} missing or invalid`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return api;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertBuiltinCtxShape(ctx) {
|
|
112
|
+
assertExactKeys(ctx, new Set(CONTRACT.handler.ctxKeys), 'Builtin handler ctx');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertBuiltinResultShape(out, iri) {
|
|
116
|
+
if (out == null) return;
|
|
117
|
+
if (!Array.isArray(out)) {
|
|
118
|
+
throw new TypeError(`Custom builtin ${iri} must return an array of substitution deltas`);
|
|
119
|
+
}
|
|
120
|
+
for (const delta of out) {
|
|
121
|
+
if (delta === null || typeof delta !== 'object' || Array.isArray(delta)) {
|
|
122
|
+
throw new TypeError(`Custom builtin ${iri} returned a non-object substitution delta`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
CONTRACT,
|
|
129
|
+
assertBuiltinApiShape,
|
|
130
|
+
assertBuiltinCtxShape,
|
|
131
|
+
assertBuiltinResultShape,
|
|
132
|
+
deepFreezeBuiltinApi,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
};
|
|
12
136
|
__modules["lib/builtin-sudoku.js"] = function(require, module, exports){
|
|
13
137
|
'use strict';
|
|
14
138
|
|
|
@@ -515,6 +639,13 @@ const { termToN3 } = require('./printing');
|
|
|
515
639
|
const trace = require('./trace');
|
|
516
640
|
const time = require('./time');
|
|
517
641
|
const deref = require('./deref');
|
|
642
|
+
const {
|
|
643
|
+
CONTRACT: BUILTIN_CONTRACT,
|
|
644
|
+
assertBuiltinApiShape,
|
|
645
|
+
assertBuiltinCtxShape,
|
|
646
|
+
assertBuiltinResultShape,
|
|
647
|
+
deepFreezeBuiltinApi,
|
|
648
|
+
} = require('./builtin-contract');
|
|
518
649
|
|
|
519
650
|
let nodeCrypto = null;
|
|
520
651
|
try {
|
|
@@ -558,8 +689,21 @@ function registerBuiltin(iri, handler) {
|
|
|
558
689
|
if (typeof handler !== 'function') {
|
|
559
690
|
throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
|
|
560
691
|
}
|
|
561
|
-
|
|
562
|
-
|
|
692
|
+
|
|
693
|
+
const wrapped = function builtinContractWrapper(ctx) {
|
|
694
|
+
assertBuiltinCtxShape(ctx);
|
|
695
|
+
const out = handler(ctx);
|
|
696
|
+
assertBuiltinResultShape(out, iri);
|
|
697
|
+
return out;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
Object.defineProperty(wrapped, '__builtinContractWrapped', {
|
|
701
|
+
value: true,
|
|
702
|
+
enumerable: false,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
__customBuiltinHandlers.set(iri, wrapped);
|
|
706
|
+
return wrapped;
|
|
563
707
|
}
|
|
564
708
|
|
|
565
709
|
function unregisterBuiltin(iri) {
|
|
@@ -570,8 +714,17 @@ function listBuiltinIris() {
|
|
|
570
714
|
return Array.from(__customBuiltinHandlers.keys()).sort();
|
|
571
715
|
}
|
|
572
716
|
|
|
717
|
+
let __builtinApiSingleton = null;
|
|
718
|
+
|
|
719
|
+
function getBuiltinApiVersion() {
|
|
720
|
+
return BUILTIN_CONTRACT.version;
|
|
721
|
+
}
|
|
722
|
+
|
|
573
723
|
function __buildBuiltinRegistrationApi() {
|
|
574
|
-
return
|
|
724
|
+
if (__builtinApiSingleton) return __builtinApiSingleton;
|
|
725
|
+
|
|
726
|
+
const api = {
|
|
727
|
+
getBuiltinApiVersion,
|
|
575
728
|
registerBuiltin,
|
|
576
729
|
unregisterBuiltin,
|
|
577
730
|
listBuiltinIris,
|
|
@@ -599,6 +752,9 @@ function __buildBuiltinRegistrationApi() {
|
|
|
599
752
|
terms: { Literal, Iri, Var, Blank, ListTerm, OpenListTerm, GraphTerm, Triple, Rule },
|
|
600
753
|
ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
|
|
601
754
|
};
|
|
755
|
+
|
|
756
|
+
__builtinApiSingleton = deepFreezeBuiltinApi(assertBuiltinApiShape(api));
|
|
757
|
+
return __builtinApiSingleton;
|
|
602
758
|
}
|
|
603
759
|
|
|
604
760
|
function registerBuiltinModule(mod, origin = '<builtin-module>') {
|
|
@@ -675,9 +831,7 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
|
|
|
675
831
|
try {
|
|
676
832
|
const out = handler(ctx);
|
|
677
833
|
if (out == null) return [];
|
|
678
|
-
|
|
679
|
-
throw new TypeError(`Custom builtin ${pv} must return an array of substitution deltas`);
|
|
680
|
-
}
|
|
834
|
+
assertBuiltinResultShape(out, pv);
|
|
681
835
|
return out;
|
|
682
836
|
} catch (err) {
|
|
683
837
|
if (err && typeof err === 'object' && typeof err.message === 'string') {
|
|
@@ -3670,7 +3824,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
3670
3824
|
// - subject = GraphTerm: explicit scope, run immediately (no closure gating)
|
|
3671
3825
|
// - subject = positive integer literal N (>= 1): delay until saturated closure level >= N
|
|
3672
3826
|
// - subject = Var: treat as priority 1 (do not bind)
|
|
3673
|
-
// - any other subject:
|
|
3827
|
+
// - any other subject: invalid, so the builtin fails
|
|
3674
3828
|
if (pv === LOG_NS + 'includes') {
|
|
3675
3829
|
let scopeFacts = null;
|
|
3676
3830
|
let scopeBackRules = backRules;
|
|
@@ -3698,7 +3852,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
3698
3852
|
prio = 1; // do not bind
|
|
3699
3853
|
} else {
|
|
3700
3854
|
const p0 = __logNaturalPriorityFromTerm(g.s);
|
|
3701
|
-
if (p0
|
|
3855
|
+
if (p0 === null) return [];
|
|
3856
|
+
prio = p0;
|
|
3702
3857
|
}
|
|
3703
3858
|
|
|
3704
3859
|
const snap = facts.__scopedSnapshot || null;
|
|
@@ -3781,7 +3936,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
3781
3936
|
prio = 1; // do not bind
|
|
3782
3937
|
} else {
|
|
3783
3938
|
const p0 = __logNaturalPriorityFromTerm(g.s);
|
|
3784
|
-
if (p0
|
|
3939
|
+
if (p0 === null) return [];
|
|
3940
|
+
prio = p0;
|
|
3785
3941
|
}
|
|
3786
3942
|
|
|
3787
3943
|
const snap = facts.__scopedSnapshot || null;
|
|
@@ -4470,6 +4626,8 @@ module.exports = {
|
|
|
4470
4626
|
registerBuiltinModule,
|
|
4471
4627
|
loadBuiltinModule,
|
|
4472
4628
|
listBuiltinIris,
|
|
4629
|
+
getBuiltinApiVersion,
|
|
4630
|
+
__testBuildBuiltinApi: __buildBuiltinRegistrationApi,
|
|
4473
4631
|
// shared helpers used by engine/explain
|
|
4474
4632
|
parseBooleanLiteralInfo,
|
|
4475
4633
|
parseNumericLiteralInfo,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CONTRACT = {
|
|
4
|
+
version: 1,
|
|
5
|
+
moduleExportForms: [
|
|
6
|
+
'function',
|
|
7
|
+
'object.register',
|
|
8
|
+
'object.builtins',
|
|
9
|
+
'plain-object-map',
|
|
10
|
+
'object.default-object-map',
|
|
11
|
+
],
|
|
12
|
+
api: {
|
|
13
|
+
functions: {
|
|
14
|
+
getBuiltinApiVersion: { arity: 0, required: true },
|
|
15
|
+
registerBuiltin: { arity: 2, required: true },
|
|
16
|
+
unregisterBuiltin: { arity: 1, required: true },
|
|
17
|
+
listBuiltinIris: { arity: 0, required: true },
|
|
18
|
+
internIri: { arity: 1, required: true },
|
|
19
|
+
internLiteral: { arity: 1, required: true },
|
|
20
|
+
literalParts: { arity: 1, required: true },
|
|
21
|
+
termToJsString: { arity: 1, required: true },
|
|
22
|
+
termToJsStringDecoded: { arity: 1, required: true },
|
|
23
|
+
termToN3: { arity: 2, required: true },
|
|
24
|
+
iriValue: { arity: 1, required: true },
|
|
25
|
+
unifyTerm: { arity: 3, required: true },
|
|
26
|
+
applySubstTerm: { arity: 2, required: true },
|
|
27
|
+
applySubstTriple: { arity: 2, required: true },
|
|
28
|
+
proveGoals: { arity: 9, required: true },
|
|
29
|
+
isGroundTerm: { arity: 1, required: true },
|
|
30
|
+
computeConclusionFromFormula: { arity: 1, required: true },
|
|
31
|
+
skolemIriFromGroundTerm: { arity: 1, required: true },
|
|
32
|
+
parseBooleanLiteralInfo: { arity: 1, required: true },
|
|
33
|
+
parseNumericLiteralInfo: { arity: 1, required: true },
|
|
34
|
+
parseXsdDecimalToBigIntScale: { arity: 1, required: true },
|
|
35
|
+
pow10n: { arity: 1, required: true },
|
|
36
|
+
normalizeLiteralForFastKey: { arity: 1, required: true },
|
|
37
|
+
literalsEquivalentAsXsdString: { arity: 2, required: true },
|
|
38
|
+
materializeRdfLists: { arity: 3, required: true },
|
|
39
|
+
},
|
|
40
|
+
namespaces: {
|
|
41
|
+
terms: ['Literal', 'Iri', 'Var', 'Blank', 'ListTerm', 'OpenListTerm', 'GraphTerm', 'Triple', 'Rule'],
|
|
42
|
+
ns: ['RDF_NS', 'XSD_NS', 'CRYPTO_NS', 'MATH_NS', 'TIME_NS', 'LIST_NS', 'LOG_NS', 'STRING_NS'],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
handler: {
|
|
46
|
+
ctxKeys: ['iri', 'goal', 'subst', 'facts', 'backRules', 'depth', 'varGen', 'maxResults', 'api'],
|
|
47
|
+
return: 'array-of-substitution-deltas',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const EXACT_API_KEYS = new Set([...Object.keys(CONTRACT.api.functions), ...Object.keys(CONTRACT.api.namespaces)]);
|
|
52
|
+
|
|
53
|
+
function assertExactKeys(obj, expectedKeys, label) {
|
|
54
|
+
const got = Object.keys(obj).sort();
|
|
55
|
+
const exp = Array.from(expectedKeys).sort();
|
|
56
|
+
if (got.length !== exp.length || got.some((k, i) => k !== exp[i])) {
|
|
57
|
+
throw new Error(`${label} keys changed. expected: ${exp.join(', ')}; got: ${got.join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertFunctionArity(name, fn, spec) {
|
|
62
|
+
if (typeof fn !== 'function') throw new TypeError(`Builtin API member ${name} must be a function`);
|
|
63
|
+
if (Number.isInteger(spec.arity) && fn.length !== spec.arity) {
|
|
64
|
+
throw new Error(`Builtin API member ${name} arity changed: expected ${spec.arity}, got ${fn.length}`);
|
|
65
|
+
}
|
|
66
|
+
if (Number.isInteger(spec.arityMin) && fn.length < spec.arityMin) {
|
|
67
|
+
throw new Error(`Builtin API member ${name} arity too small: expected >= ${spec.arityMin}, got ${fn.length}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deepFreezeBuiltinApi(api) {
|
|
72
|
+
Object.freeze(api.terms);
|
|
73
|
+
Object.freeze(api.ns);
|
|
74
|
+
return Object.freeze(api);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertBuiltinApiShape(api) {
|
|
78
|
+
assertExactKeys(api, EXACT_API_KEYS, 'Builtin registration API');
|
|
79
|
+
|
|
80
|
+
for (const [name, spec] of Object.entries(CONTRACT.api.functions)) {
|
|
81
|
+
assertFunctionArity(name, api[name], spec);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const name of CONTRACT.api.namespaces.terms) {
|
|
85
|
+
if (!api.terms || typeof api.terms[name] !== 'function') {
|
|
86
|
+
throw new TypeError(`Builtin API terms.${name} missing or invalid`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const name of CONTRACT.api.namespaces.ns) {
|
|
91
|
+
if (!api.ns || typeof api.ns[name] !== 'string') {
|
|
92
|
+
throw new TypeError(`Builtin API ns.${name} missing or invalid`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return api;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function assertBuiltinCtxShape(ctx) {
|
|
100
|
+
assertExactKeys(ctx, new Set(CONTRACT.handler.ctxKeys), 'Builtin handler ctx');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertBuiltinResultShape(out, iri) {
|
|
104
|
+
if (out == null) return;
|
|
105
|
+
if (!Array.isArray(out)) {
|
|
106
|
+
throw new TypeError(`Custom builtin ${iri} must return an array of substitution deltas`);
|
|
107
|
+
}
|
|
108
|
+
for (const delta of out) {
|
|
109
|
+
if (delta === null || typeof delta !== 'object' || Array.isArray(delta)) {
|
|
110
|
+
throw new TypeError(`Custom builtin ${iri} returned a non-object substitution delta`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
CONTRACT,
|
|
117
|
+
assertBuiltinApiShape,
|
|
118
|
+
assertBuiltinCtxShape,
|
|
119
|
+
assertBuiltinResultShape,
|
|
120
|
+
deepFreezeBuiltinApi,
|
|
121
|
+
};
|
package/lib/builtins.js
CHANGED
|
@@ -35,6 +35,13 @@ const { termToN3 } = require('./printing');
|
|
|
35
35
|
const trace = require('./trace');
|
|
36
36
|
const time = require('./time');
|
|
37
37
|
const deref = require('./deref');
|
|
38
|
+
const {
|
|
39
|
+
CONTRACT: BUILTIN_CONTRACT,
|
|
40
|
+
assertBuiltinApiShape,
|
|
41
|
+
assertBuiltinCtxShape,
|
|
42
|
+
assertBuiltinResultShape,
|
|
43
|
+
deepFreezeBuiltinApi,
|
|
44
|
+
} = require('./builtin-contract');
|
|
38
45
|
|
|
39
46
|
let nodeCrypto = null;
|
|
40
47
|
try {
|
|
@@ -78,8 +85,21 @@ function registerBuiltin(iri, handler) {
|
|
|
78
85
|
if (typeof handler !== 'function') {
|
|
79
86
|
throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
|
|
89
|
+
const wrapped = function builtinContractWrapper(ctx) {
|
|
90
|
+
assertBuiltinCtxShape(ctx);
|
|
91
|
+
const out = handler(ctx);
|
|
92
|
+
assertBuiltinResultShape(out, iri);
|
|
93
|
+
return out;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
Object.defineProperty(wrapped, '__builtinContractWrapped', {
|
|
97
|
+
value: true,
|
|
98
|
+
enumerable: false,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
__customBuiltinHandlers.set(iri, wrapped);
|
|
102
|
+
return wrapped;
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
function unregisterBuiltin(iri) {
|
|
@@ -90,8 +110,17 @@ function listBuiltinIris() {
|
|
|
90
110
|
return Array.from(__customBuiltinHandlers.keys()).sort();
|
|
91
111
|
}
|
|
92
112
|
|
|
113
|
+
let __builtinApiSingleton = null;
|
|
114
|
+
|
|
115
|
+
function getBuiltinApiVersion() {
|
|
116
|
+
return BUILTIN_CONTRACT.version;
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
function __buildBuiltinRegistrationApi() {
|
|
94
|
-
return
|
|
120
|
+
if (__builtinApiSingleton) return __builtinApiSingleton;
|
|
121
|
+
|
|
122
|
+
const api = {
|
|
123
|
+
getBuiltinApiVersion,
|
|
95
124
|
registerBuiltin,
|
|
96
125
|
unregisterBuiltin,
|
|
97
126
|
listBuiltinIris,
|
|
@@ -119,6 +148,9 @@ function __buildBuiltinRegistrationApi() {
|
|
|
119
148
|
terms: { Literal, Iri, Var, Blank, ListTerm, OpenListTerm, GraphTerm, Triple, Rule },
|
|
120
149
|
ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
|
|
121
150
|
};
|
|
151
|
+
|
|
152
|
+
__builtinApiSingleton = deepFreezeBuiltinApi(assertBuiltinApiShape(api));
|
|
153
|
+
return __builtinApiSingleton;
|
|
122
154
|
}
|
|
123
155
|
|
|
124
156
|
function registerBuiltinModule(mod, origin = '<builtin-module>') {
|
|
@@ -195,9 +227,7 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
|
|
|
195
227
|
try {
|
|
196
228
|
const out = handler(ctx);
|
|
197
229
|
if (out == null) return [];
|
|
198
|
-
|
|
199
|
-
throw new TypeError(`Custom builtin ${pv} must return an array of substitution deltas`);
|
|
200
|
-
}
|
|
230
|
+
assertBuiltinResultShape(out, pv);
|
|
201
231
|
return out;
|
|
202
232
|
} catch (err) {
|
|
203
233
|
if (err && typeof err === 'object' && typeof err.message === 'string') {
|
|
@@ -3190,7 +3220,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
3190
3220
|
// - subject = GraphTerm: explicit scope, run immediately (no closure gating)
|
|
3191
3221
|
// - subject = positive integer literal N (>= 1): delay until saturated closure level >= N
|
|
3192
3222
|
// - subject = Var: treat as priority 1 (do not bind)
|
|
3193
|
-
// - any other subject:
|
|
3223
|
+
// - any other subject: invalid, so the builtin fails
|
|
3194
3224
|
if (pv === LOG_NS + 'includes') {
|
|
3195
3225
|
let scopeFacts = null;
|
|
3196
3226
|
let scopeBackRules = backRules;
|
|
@@ -3218,7 +3248,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
3218
3248
|
prio = 1; // do not bind
|
|
3219
3249
|
} else {
|
|
3220
3250
|
const p0 = __logNaturalPriorityFromTerm(g.s);
|
|
3221
|
-
if (p0
|
|
3251
|
+
if (p0 === null) return [];
|
|
3252
|
+
prio = p0;
|
|
3222
3253
|
}
|
|
3223
3254
|
|
|
3224
3255
|
const snap = facts.__scopedSnapshot || null;
|
|
@@ -3301,7 +3332,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
3301
3332
|
prio = 1; // do not bind
|
|
3302
3333
|
} else {
|
|
3303
3334
|
const p0 = __logNaturalPriorityFromTerm(g.s);
|
|
3304
|
-
if (p0
|
|
3335
|
+
if (p0 === null) return [];
|
|
3336
|
+
prio = p0;
|
|
3305
3337
|
}
|
|
3306
3338
|
|
|
3307
3339
|
const snap = facts.__scopedSnapshot || null;
|
|
@@ -3990,6 +4022,8 @@ module.exports = {
|
|
|
3990
4022
|
registerBuiltinModule,
|
|
3991
4023
|
loadBuiltinModule,
|
|
3992
4024
|
listBuiltinIris,
|
|
4025
|
+
getBuiltinApiVersion,
|
|
4026
|
+
__testBuildBuiltinApi: __buildBuiltinRegistrationApi,
|
|
3993
4027
|
// shared helpers used by engine/explain
|
|
3994
4028
|
parseBooleanLiteralInfo,
|
|
3995
4029
|
parseNumericLiteralInfo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyeling",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.2",
|
|
4
4
|
"description": "A minimal Notation3 (N3) reasoner in JavaScript.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -40,12 +40,13 @@
|
|
|
40
40
|
"build": "node tools/bundle.js",
|
|
41
41
|
"test:packlist": "node test/packlist.test.js",
|
|
42
42
|
"test:api": "node test/api.test.js",
|
|
43
|
+
"test:builtin-contract": "node test/builtin-contract.test.js",
|
|
43
44
|
"test:n3gen": "node test/n3gen.test.js",
|
|
44
45
|
"test:examples": "node test/examples.test.js",
|
|
45
46
|
"test:manifest": "node test/manifest.test.js",
|
|
46
47
|
"test:playground": "node test/playground.test.js",
|
|
47
48
|
"test:package": "node test/package.test.js",
|
|
48
|
-
"test:all": "npm run test:api && npm run test:n3gen && npm run test:examples && npm run test:manifest && npm run test:playground",
|
|
49
|
+
"test:all": "npm run test:api && npm run test:builtin-contract && npm run test:n3gen && npm run test:examples && npm run test:manifest && npm run test:playground",
|
|
49
50
|
"pretest": "npm run build && npm run test:packlist",
|
|
50
51
|
"test": "npm run test:all",
|
|
51
52
|
"posttest": "npm run test:package",
|
package/test/api.test.js
CHANGED
|
@@ -1462,6 +1462,42 @@ _:x :hates { _:foo :making :mess }.
|
|
|
1462
1462
|
notExpect: [/:(?:test)\s+:(?:is)\s+false\s*\./],
|
|
1463
1463
|
},
|
|
1464
1464
|
|
|
1465
|
+
{
|
|
1466
|
+
name: '59b regression: log:includes rejects non-scope literal or term subjects',
|
|
1467
|
+
opt: { proofComments: false },
|
|
1468
|
+
input: `@prefix : <http://example.org/> .
|
|
1469
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
1470
|
+
@base <http://example.org/> .
|
|
1471
|
+
|
|
1472
|
+
{ false log:includes true. } => { :result :has :fail-literal-1. }.
|
|
1473
|
+
{ "foo" log:includes true. } => { :result :has :fail-literal-2. }.
|
|
1474
|
+
{ :foo log:includes true. } => { :result :has :fail-literal-3. }.
|
|
1475
|
+
{ 0 log:includes true. } => { :result :has :fail-literal-4. }.
|
|
1476
|
+
{ 42.3 log:includes true. } => { :result :has :fail-literal-5. }.
|
|
1477
|
+
{ (:foo 1 _:x) log:includes true. } => { :result :has :fail-literal-6. }.
|
|
1478
|
+
|
|
1479
|
+
{ } => {
|
|
1480
|
+
:test :contains :fail-literal-1, :fail-literal-2, :fail-literal-3, :fail-literal-4, :fail-literal-5, :fail-literal-6.
|
|
1481
|
+
}.
|
|
1482
|
+
`,
|
|
1483
|
+
expect: [
|
|
1484
|
+
/:(?:test)\s+:(?:contains)\s+:(?:fail-literal-1)\s*\./,
|
|
1485
|
+
/:(?:test)\s+:(?:contains)\s+:(?:fail-literal-2)\s*\./,
|
|
1486
|
+
/:(?:test)\s+:(?:contains)\s+:(?:fail-literal-3)\s*\./,
|
|
1487
|
+
/:(?:test)\s+:(?:contains)\s+:(?:fail-literal-4)\s*\./,
|
|
1488
|
+
/:(?:test)\s+:(?:contains)\s+:(?:fail-literal-5)\s*\./,
|
|
1489
|
+
/:(?:test)\s+:(?:contains)\s+:(?:fail-literal-6)\s*\./,
|
|
1490
|
+
],
|
|
1491
|
+
notExpect: [
|
|
1492
|
+
/:(?:result)\s+:(?:has)\s+:(?:fail-literal-1)\s*\./,
|
|
1493
|
+
/:(?:result)\s+:(?:has)\s+:(?:fail-literal-2)\s*\./,
|
|
1494
|
+
/:(?:result)\s+:(?:has)\s+:(?:fail-literal-3)\s*\./,
|
|
1495
|
+
/:(?:result)\s+:(?:has)\s+:(?:fail-literal-4)\s*\./,
|
|
1496
|
+
/:(?:result)\s+:(?:has)\s+:(?:fail-literal-5)\s*\./,
|
|
1497
|
+
/:(?:result)\s+:(?:has)\s+:(?:fail-literal-6)\s*\./,
|
|
1498
|
+
],
|
|
1499
|
+
},
|
|
1500
|
+
|
|
1465
1501
|
{
|
|
1466
1502
|
name: '60 regression: log:includes must match quoted triples with variable predicates',
|
|
1467
1503
|
opt: { proofComments: false },
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const TTY = process.stdout.isTTY;
|
|
7
|
+
const C = TTY
|
|
8
|
+
? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
|
|
9
|
+
: { g: '', r: '', y: '', dim: '', n: '' };
|
|
10
|
+
|
|
11
|
+
function ok(msg) {
|
|
12
|
+
console.log(`${C.g}OK ${C.n} ${msg}`);
|
|
13
|
+
}
|
|
14
|
+
function info(msg) {
|
|
15
|
+
console.log(`${C.y}==${C.n} ${msg}`);
|
|
16
|
+
}
|
|
17
|
+
function fail(msg) {
|
|
18
|
+
console.error(`${C.r}FAIL${C.n} ${msg}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fixtures = path.join(__dirname, 'fixtures', 'builtins');
|
|
22
|
+
const builtins = require('../lib/builtins');
|
|
23
|
+
const { CONTRACT } = require('../lib/builtin-contract');
|
|
24
|
+
require('../lib/engine');
|
|
25
|
+
|
|
26
|
+
const expectedApiKeys = [...Object.keys(CONTRACT.api.functions), ...Object.keys(CONTRACT.api.namespaces)].sort();
|
|
27
|
+
|
|
28
|
+
const expectedFunctionArities = Object.fromEntries(
|
|
29
|
+
Object.entries(CONTRACT.api.functions).map(([name, spec]) => [name, spec]),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const cases = [
|
|
33
|
+
{
|
|
34
|
+
name: 'builtin API exact helper surface is stable',
|
|
35
|
+
run() {
|
|
36
|
+
const api = builtins.__testBuildBuiltinApi();
|
|
37
|
+
assert.deepEqual(Object.keys(api).sort(), expectedApiKeys);
|
|
38
|
+
assert.equal(Object.isFrozen(api), true);
|
|
39
|
+
assert.equal(Object.isFrozen(api.terms), true);
|
|
40
|
+
assert.equal(Object.isFrozen(api.ns), true);
|
|
41
|
+
for (const [name, spec] of Object.entries(expectedFunctionArities)) {
|
|
42
|
+
assert.equal(typeof api[name], 'function', `${name} must be a function`);
|
|
43
|
+
if (Number.isInteger(spec.arity)) assert.equal(api[name].length, spec.arity, `${name} arity drifted`);
|
|
44
|
+
if (Number.isInteger(spec.arityMin)) assert.ok(api[name].length >= spec.arityMin, `${name} arity drifted`);
|
|
45
|
+
}
|
|
46
|
+
assert.deepEqual(Object.keys(api.terms).sort(), CONTRACT.api.namespaces.terms.slice().sort());
|
|
47
|
+
assert.deepEqual(Object.keys(api.ns).sort(), CONTRACT.api.namespaces.ns.slice().sort());
|
|
48
|
+
assert.equal(api.getBuiltinApiVersion(), CONTRACT.version);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'registerBuiltinModule accepts all declared module export forms',
|
|
53
|
+
run() {
|
|
54
|
+
assert.doesNotThrow(() => builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-map.js')), 'ok-map'));
|
|
55
|
+
assert.doesNotThrow(() =>
|
|
56
|
+
builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-register.js')), 'ok-register'),
|
|
57
|
+
);
|
|
58
|
+
assert.doesNotThrow(() =>
|
|
59
|
+
builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-builtins.js')), 'ok-builtins'),
|
|
60
|
+
);
|
|
61
|
+
assert.doesNotThrow(() =>
|
|
62
|
+
builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-default-map.js')), 'ok-default-map'),
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'registerBuiltinModule rejects unsupported module exports',
|
|
68
|
+
run() {
|
|
69
|
+
assert.throws(
|
|
70
|
+
() => builtins.registerBuiltinModule(require(path.join(fixtures, 'bad-export.js')), 'bad-export'),
|
|
71
|
+
/must export a function, a \{ register\(\) \} object, or an object mapping predicate IRIs to handlers/,
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'registered builtin handlers must return substitution-delta arrays',
|
|
77
|
+
run() {
|
|
78
|
+
builtins.registerBuiltinModule(require(path.join(fixtures, 'bad-return.js')), 'bad-return');
|
|
79
|
+
assert.throws(() => {
|
|
80
|
+
const h = builtins.registerBuiltin('http://example.org/test#shape-check', () => ({ nope: true }));
|
|
81
|
+
h({
|
|
82
|
+
iri: 'http://example.org/test#shape-check',
|
|
83
|
+
goal: {},
|
|
84
|
+
subst: {},
|
|
85
|
+
facts: [],
|
|
86
|
+
backRules: [],
|
|
87
|
+
depth: 0,
|
|
88
|
+
varGen: 0,
|
|
89
|
+
maxResults: 1,
|
|
90
|
+
api: builtins.__testBuildBuiltinApi(),
|
|
91
|
+
});
|
|
92
|
+
}, /must return an array of substitution deltas/);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'registered builtin handlers receive the exact stable ctx shape',
|
|
97
|
+
run() {
|
|
98
|
+
const wrapped = builtins.registerBuiltin('http://example.org/test#ctx-shape', ({ subst }) => [subst]);
|
|
99
|
+
assert.throws(
|
|
100
|
+
() =>
|
|
101
|
+
wrapped({
|
|
102
|
+
iri: 'http://example.org/test#ctx-shape',
|
|
103
|
+
goal: {},
|
|
104
|
+
subst: {},
|
|
105
|
+
facts: [],
|
|
106
|
+
backRules: [],
|
|
107
|
+
depth: 0,
|
|
108
|
+
varGen: 0,
|
|
109
|
+
maxResults: 1,
|
|
110
|
+
api: builtins.__testBuildBuiltinApi(),
|
|
111
|
+
extra: true,
|
|
112
|
+
}),
|
|
113
|
+
/Builtin handler ctx keys changed|Builtin handler ctx shape changed/,
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
let passed = 0;
|
|
120
|
+
let failed = 0;
|
|
121
|
+
|
|
122
|
+
(function main() {
|
|
123
|
+
const suiteStart = Date.now();
|
|
124
|
+
info(`Running ${cases.length} builtin contract tests`);
|
|
125
|
+
|
|
126
|
+
for (const tc of cases) {
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
try {
|
|
129
|
+
tc.run();
|
|
130
|
+
ok(`${tc.name} ${C.dim}(${Date.now() - start} ms)${C.n}`);
|
|
131
|
+
passed++;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
fail(`${tc.name} ${C.dim}(${Date.now() - start} ms)${C.n}`);
|
|
134
|
+
fail(e && e.stack ? e.stack : String(e));
|
|
135
|
+
failed++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log(`${C.y}==${C.n} Total elapsed: ${Date.now() - suiteStart} ms`);
|
|
141
|
+
if (failed === 0) {
|
|
142
|
+
ok(`All builtin contract tests passed (${passed}/${cases.length})`);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
fail(`Some builtin contract tests failed (${passed}/${cases.length})`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
})();
|