eyeling 1.24.0 → 1.24.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 +98 -0
- package/README.md +4 -4
- package/dist/browser/eyeling.browser.js +103 -0
- package/eyeling.js +103 -0
- package/lib/lexer.js +103 -0
- package/package.json +1 -1
- package/see/README.md +13 -11
- package/see/examples/_see.js +33 -2
- package/see/examples/age.js +37 -11
- package/see/examples/annotation.js +37 -11
- package/see/examples/backward.js +37 -11
- package/see/examples/backward_recursion.js +37 -11
- package/see/examples/bayes_diagnosis.js +37 -11
- package/see/examples/bayes_therapy.js +37 -11
- package/see/examples/bmi.js +37 -11
- package/see/examples/builtin_coverage.js +37 -11
- package/see/examples/collection.js +37 -11
- package/see/examples/complex.js +37 -11
- package/see/examples/complex_matrix_stability.js +37 -11
- package/see/examples/composition_of_injective_functions_is_injective.js +37 -11
- package/see/examples/control_system.js +37 -11
- package/see/examples/crypto_builtins_tests.js +37 -11
- package/see/examples/delfour.js +37 -11
- package/see/examples/digital_product_passport.js +37 -11
- package/see/examples/dijkstra.js +37 -11
- package/see/examples/dijkstra_risk_path.js +37 -11
- package/see/examples/doc/age.md +1 -1
- package/see/examples/doc/annotation.md +1 -1
- package/see/examples/doc/backward.md +1 -1
- package/see/examples/doc/backward_recursion.md +1 -1
- package/see/examples/doc/bayes_diagnosis.md +1 -1
- package/see/examples/doc/bayes_therapy.md +1 -1
- package/see/examples/doc/bmi.md +1 -1
- package/see/examples/doc/builtin_coverage.md +1 -1
- package/see/examples/doc/collection.md +1 -1
- package/see/examples/doc/complex.md +1 -1
- package/see/examples/doc/complex_matrix_stability.md +1 -1
- package/see/examples/doc/composition_of_injective_functions_is_injective.md +1 -1
- package/see/examples/doc/control_system.md +1 -1
- package/see/examples/doc/crypto_builtins_tests.md +1 -1
- package/see/examples/doc/delfour.md +1 -1
- package/see/examples/doc/digital_product_passport.md +1 -1
- package/see/examples/doc/dijkstra.md +1 -1
- package/see/examples/doc/dijkstra_risk_path.md +1 -1
- package/see/examples/doc/dog.md +1 -1
- package/see/examples/doc/eco_route_insight.md +1 -1
- package/see/examples/doc/equals.md +1 -1
- package/see/examples/doc/equivalence_classes_overlap_implies_same_class.md +1 -1
- package/see/examples/doc/euler_identity.md +1 -1
- package/see/examples/doc/ev_roundtrip_planner.md +1 -1
- package/see/examples/doc/existential_rule.md +1 -1
- package/see/examples/doc/expression_eval.md +1 -1
- package/see/examples/doc/family_cousins.md +1 -1
- package/see/examples/doc/fastpow.md +1 -1
- package/see/examples/doc/fibonacci.md +1 -1
- package/see/examples/doc/french_cities.md +1 -1
- package/see/examples/doc/fundamental_theorem_arithmetic.md +1 -1
- package/see/examples/doc/genetic_knapsack_selection.md +1 -1
- package/see/examples/doc/goldbach_1000.md +1 -1
- package/see/examples/doc/good_cobbler.md +1 -1
- package/see/examples/doc/gps.md +1 -1
- package/see/examples/doc/gray_code_counter.md +1 -1
- package/see/examples/doc/greatest_lower_bound_uniqueness.md +1 -1
- package/see/examples/doc/group_inverse_uniqueness.md +1 -1
- package/see/examples/doc/hadamard_approx.md +1 -1
- package/see/examples/doc/hanoi.md +1 -1
- package/see/examples/doc/odrl_dpv_risk_ranked.md +1 -1
- package/see/examples/doc/path_discovery.md +1 -1
- package/see/examples/doc/rc_discharge_envelope.md +1 -1
- package/see/examples/doc/rdf_message_flow.md +1 -1
- package/see/examples/doc/rdf_messages.md +1 -1
- package/see/examples/doc/school_placement_audit.md +1 -1
- package/see/examples/doc/smoke_arithmetic.md +1 -1
- package/see/examples/doc/socrates.md +1 -1
- package/see/examples/doc/triple_terms.md +26 -0
- package/see/examples/doc/wind_turbine.md +1 -1
- package/see/examples/doc/witch.md +1 -1
- package/see/examples/dog.js +37 -11
- package/see/examples/eco_route_insight.js +37 -11
- package/see/examples/equals.js +37 -11
- package/see/examples/equivalence_classes_overlap_implies_same_class.js +37 -11
- package/see/examples/euler_identity.js +37 -11
- package/see/examples/ev_roundtrip_planner.js +37 -11
- package/see/examples/existential_rule.js +37 -11
- package/see/examples/expression_eval.js +37 -11
- package/see/examples/family_cousins.js +37 -11
- package/see/examples/fastpow.js +37 -11
- package/see/examples/fibonacci.js +37 -11
- package/see/examples/french_cities.js +37 -11
- package/see/examples/fundamental_theorem_arithmetic.js +37 -11
- package/see/examples/genetic_knapsack_selection.js +37 -11
- package/see/examples/goldbach_1000.js +37 -11
- package/see/examples/good_cobbler.js +37 -11
- package/see/examples/gps.js +37 -11
- package/see/examples/gray_code_counter.js +37 -11
- package/see/examples/greatest_lower_bound_uniqueness.js +37 -11
- package/see/examples/group_inverse_uniqueness.js +37 -11
- package/see/examples/hadamard_approx.js +37 -11
- package/see/examples/hanoi.js +37 -11
- package/see/examples/input/triple_terms.trig +28 -0
- package/see/examples/n3/triple_terms.n3 +23 -0
- package/see/examples/odrl_dpv_risk_ranked.js +37 -11
- package/see/examples/output/age.md +3 -3
- package/see/examples/output/annotation.md +4 -4
- package/see/examples/output/backward.md +3 -3
- package/see/examples/output/backward_recursion.md +3 -3
- package/see/examples/output/bayes_diagnosis.md +1 -1
- package/see/examples/output/bayes_therapy.md +1 -1
- package/see/examples/output/bmi.md +1 -1
- package/see/examples/output/builtin_coverage.md +3 -3
- package/see/examples/output/collection.md +3 -3
- package/see/examples/output/complex.md +4 -4
- package/see/examples/output/complex_matrix_stability.md +1 -1
- package/see/examples/output/composition_of_injective_functions_is_injective.md +3 -3
- package/see/examples/output/control_system.md +3 -3
- package/see/examples/output/crypto_builtins_tests.md +3 -3
- package/see/examples/output/delfour.md +1 -1
- package/see/examples/output/digital_product_passport.md +1 -1
- package/see/examples/output/dijkstra.md +3 -3
- package/see/examples/output/dijkstra_risk_path.md +1 -1
- package/see/examples/output/dog.md +3 -3
- package/see/examples/output/eco_route_insight.md +1 -1
- package/see/examples/output/equals.md +3 -3
- package/see/examples/output/equivalence_classes_overlap_implies_same_class.md +3 -3
- package/see/examples/output/euler_identity.md +3 -3
- package/see/examples/output/ev_roundtrip_planner.md +1 -1
- package/see/examples/output/existential_rule.md +3 -3
- package/see/examples/output/expression_eval.md +3 -3
- package/see/examples/output/family_cousins.md +3 -3
- package/see/examples/output/fastpow.md +1 -1
- package/see/examples/output/fibonacci.md +1 -1
- package/see/examples/output/french_cities.md +3 -3
- package/see/examples/output/fundamental_theorem_arithmetic.md +1 -1
- package/see/examples/output/genetic_knapsack_selection.md +1 -1
- package/see/examples/output/goldbach_1000.md +1 -1
- package/see/examples/output/good_cobbler.md +4 -4
- package/see/examples/output/gps.md +1 -1
- package/see/examples/output/gray_code_counter.md +1 -1
- package/see/examples/output/greatest_lower_bound_uniqueness.md +3 -3
- package/see/examples/output/group_inverse_uniqueness.md +3 -3
- package/see/examples/output/hadamard_approx.md +3 -3
- package/see/examples/output/hanoi.md +3 -3
- package/see/examples/output/odrl_dpv_risk_ranked.md +3 -3
- package/see/examples/output/path_discovery.md +3 -3
- package/see/examples/output/rc_discharge_envelope.md +1 -1
- package/see/examples/output/rdf_message_flow.md +1 -1
- package/see/examples/output/rdf_messages.md +1 -1
- package/see/examples/output/school_placement_audit.md +1 -1
- package/see/examples/output/smoke_arithmetic.md +1 -1
- package/see/examples/output/socrates.md +3 -3
- package/see/examples/output/triple_terms.md +53 -0
- package/see/examples/output/wind_turbine.md +1 -1
- package/see/examples/output/witch.md +3 -3
- package/see/examples/path_discovery.js +37 -11
- package/see/examples/rc_discharge_envelope.js +37 -11
- package/see/examples/rdf_message_flow.js +37 -11
- package/see/examples/rdf_messages.js +37 -11
- package/see/examples/school_placement_audit.js +37 -11
- package/see/examples/smoke_arithmetic.js +37 -11
- package/see/examples/socrates.js +37 -11
- package/see/examples/triple_terms.js +1442 -0
- package/see/examples/wind_turbine.js +37 -11
- package/see/examples/witch.js +37 -11
- package/see/see.js +455 -94
- package/test/api.test.js +20 -0
package/see/see.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// SEE, Specialized Eyeling Executables, compiles a small, practical Notation3
|
|
5
|
+
// subset into standalone JavaScript examples. The compiler runs once at
|
|
6
|
+
// generation time: it extracts source facts into formal TriG evidence and bakes
|
|
7
|
+
// the supported rules, queries, and fuses into the generated runner.
|
|
8
|
+
//
|
|
9
|
+
// The generated examples intentionally do not call Eyeling or another reasoner
|
|
10
|
+
// at runtime. They read their .trig evidence directly and perform a local
|
|
11
|
+
// fixpoint derivation, which makes the resulting programs easy to inspect,
|
|
12
|
+
// snapshot, and publish as self-contained executable explanations.
|
|
13
|
+
|
|
4
14
|
const crypto = require('crypto');
|
|
5
15
|
const fs = require('fs');
|
|
6
16
|
const path = require('path');
|
|
7
17
|
const { spawnSync } = require('child_process');
|
|
8
18
|
|
|
19
|
+
// All SEE-owned artefacts stay below /see/examples so the directory can be
|
|
20
|
+
// generated, tested, and documented from the eyeling repository root.
|
|
9
21
|
const ROOT = __dirname;
|
|
10
22
|
const EXAMPLES_DIR = path.join(ROOT, 'examples');
|
|
11
23
|
const INPUT_DIR = path.join(EXAMPLES_DIR, 'input');
|
|
@@ -32,8 +44,12 @@ rule/query/fuse subset into JavaScript, and the resulting examples/<name>.js
|
|
|
32
44
|
loads the TriG evidence directly and performs the forward derivation itself.`;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
function ensureDir(dir) {
|
|
36
|
-
|
|
47
|
+
function ensureDir(dir) {
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
function readText(file) {
|
|
51
|
+
return fs.readFileSync(file, 'utf8');
|
|
52
|
+
}
|
|
37
53
|
function writeText(file, text, force) {
|
|
38
54
|
if (!force && fs.existsSync(file)) {
|
|
39
55
|
throw new Error(`${path.relative(ROOT, file)} already exists; pass --force to overwrite`);
|
|
@@ -41,8 +57,12 @@ function writeText(file, text, force) {
|
|
|
41
57
|
ensureDir(path.dirname(file));
|
|
42
58
|
fs.writeFileSync(file, text, 'utf8');
|
|
43
59
|
}
|
|
44
|
-
function sha256(text) {
|
|
45
|
-
|
|
60
|
+
function sha256(text) {
|
|
61
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
62
|
+
}
|
|
63
|
+
function js(value) {
|
|
64
|
+
return JSON.stringify(value, null, 2);
|
|
65
|
+
}
|
|
46
66
|
|
|
47
67
|
function slugify(value) {
|
|
48
68
|
const base = String(value || 'example')
|
|
@@ -54,38 +74,73 @@ function slugify(value) {
|
|
|
54
74
|
return base || 'example';
|
|
55
75
|
}
|
|
56
76
|
function titleFromSlug(slug) {
|
|
57
|
-
return slug
|
|
58
|
-
.
|
|
77
|
+
return slug
|
|
78
|
+
.split(/[_-]+/)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
81
|
+
.join(' ');
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
// A leading comment block in each source .n3 file becomes the public example
|
|
85
|
+
// title and description used in generated documentation and metadata.
|
|
86
|
+
function stripComment(line) {
|
|
87
|
+
return line.replace(/^\s*#\s?/, '').trimEnd();
|
|
88
|
+
}
|
|
89
|
+
function isSeparator(line) {
|
|
90
|
+
const t = line.trim();
|
|
91
|
+
return /^[-=]{3,}$/.test(t) || t === '';
|
|
92
|
+
}
|
|
63
93
|
function parseHeader(n3, fallbackTitle) {
|
|
64
94
|
const raw = [];
|
|
65
95
|
for (const line of n3.split(/\r?\n/)) {
|
|
66
|
-
if (/^\s*#/.test(line)) {
|
|
67
|
-
|
|
96
|
+
if (/^\s*#/.test(line)) {
|
|
97
|
+
raw.push(stripComment(line));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (/^\s*$/.test(line) && raw.length) {
|
|
101
|
+
raw.push('');
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
68
104
|
if (/^\s*$/.test(line)) continue;
|
|
69
105
|
break;
|
|
70
106
|
}
|
|
71
107
|
const useful = raw.map((line) => line.trim()).filter((line) => !isSeparator(line));
|
|
72
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
title: useful[0] || fallbackTitle,
|
|
110
|
+
description: useful
|
|
111
|
+
.slice(1)
|
|
112
|
+
.join('\n')
|
|
113
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
114
|
+
.trim(),
|
|
115
|
+
headerComments: raw,
|
|
116
|
+
};
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
function removeComments(n3) {
|
|
76
|
-
return n3
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
return n3
|
|
121
|
+
.split(/\r?\n/)
|
|
122
|
+
.map((line) => {
|
|
123
|
+
let inString = false,
|
|
124
|
+
escaped = false,
|
|
125
|
+
inIri = false;
|
|
126
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
127
|
+
const ch = line[i];
|
|
128
|
+
if (escaped) {
|
|
129
|
+
escaped = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === '\\' && inString) {
|
|
133
|
+
escaped = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (ch === '"' && !inIri) inString = !inString;
|
|
137
|
+
if (ch === '<' && !inString) inIri = true;
|
|
138
|
+
if (ch === '>' && inIri) inIri = false;
|
|
139
|
+
if (ch === '#' && !inString && !inIri) return line.slice(0, i);
|
|
140
|
+
}
|
|
141
|
+
return line;
|
|
142
|
+
})
|
|
143
|
+
.join('\n');
|
|
89
144
|
}
|
|
90
145
|
|
|
91
146
|
function decodeEscapes(value) {
|
|
@@ -104,13 +159,31 @@ function decodeEscapes(value) {
|
|
|
104
159
|
});
|
|
105
160
|
}
|
|
106
161
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
162
|
+
// Internal terms use a tiny AST shared by the compiler and the generated
|
|
163
|
+
// runtime. Variables are stored without the leading '?'; IRIs and compact
|
|
164
|
+
// QNames are preserved as source-facing strings for readable snapshots.
|
|
165
|
+
function t(kind, value) {
|
|
166
|
+
return { kind, value };
|
|
167
|
+
}
|
|
168
|
+
function I(value) {
|
|
169
|
+
return t('iri', value);
|
|
170
|
+
}
|
|
171
|
+
function V(value) {
|
|
172
|
+
return t('var', value);
|
|
173
|
+
}
|
|
174
|
+
function L(value) {
|
|
175
|
+
return t('lit', value);
|
|
176
|
+
}
|
|
177
|
+
function List(items) {
|
|
178
|
+
return { kind: 'list', items };
|
|
179
|
+
}
|
|
180
|
+
function Blank(id) {
|
|
181
|
+
return { kind: 'blank', value: id };
|
|
182
|
+
}
|
|
113
183
|
|
|
184
|
+
// This tokenizer/parser is deliberately smaller than a complete N3 parser. It
|
|
185
|
+
// accepts the SEE example subset: triples, lists, blank-node property lists,
|
|
186
|
+
// quoted formulas, RDF 1.2 triple terms, implication arrows, variables, literals, and prefix/version lines.
|
|
114
187
|
function tokenize(source) {
|
|
115
188
|
const s = removeComments(source);
|
|
116
189
|
const tokens = [];
|
|
@@ -119,9 +192,24 @@ function tokenize(source) {
|
|
|
119
192
|
const one = new Set(['{', '}', '[', ']', '(', ')', ';', ',', '.']);
|
|
120
193
|
while (i < s.length) {
|
|
121
194
|
const ch = s[i];
|
|
122
|
-
if (isWs(ch)) {
|
|
195
|
+
if (isWs(ch)) {
|
|
196
|
+
i += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (s.startsWith('<<(', i)) {
|
|
200
|
+
tokens.push({ type: '<<(', value: '<<(' });
|
|
201
|
+
i += 3;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (s.startsWith(')>>', i)) {
|
|
205
|
+
tokens.push({ type: ')>>', value: ')>>' });
|
|
206
|
+
i += 3;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
123
209
|
if (s.startsWith('=>', i) || s.startsWith('<=', i) || s.startsWith('^^', i)) {
|
|
124
|
-
tokens.push({ type: s.slice(i, i + 2), value: s.slice(i, i + 2) });
|
|
210
|
+
tokens.push({ type: s.slice(i, i + 2), value: s.slice(i, i + 2) });
|
|
211
|
+
i += 2;
|
|
212
|
+
continue;
|
|
125
213
|
}
|
|
126
214
|
if (/^[+-]?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/.test(s.slice(i)) && /[+\-0-9.]/.test(ch)) {
|
|
127
215
|
const m = s.slice(i).match(/^[+-]?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/)[0];
|
|
@@ -130,13 +218,26 @@ function tokenize(source) {
|
|
|
130
218
|
i += m.length;
|
|
131
219
|
continue;
|
|
132
220
|
}
|
|
133
|
-
if (one.has(ch)) {
|
|
221
|
+
if (one.has(ch)) {
|
|
222
|
+
tokens.push({ type: ch, value: ch });
|
|
223
|
+
i += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
134
226
|
if (ch === '"') {
|
|
135
|
-
let out = '',
|
|
227
|
+
let out = '',
|
|
228
|
+
escaped = false;
|
|
229
|
+
i += 1;
|
|
136
230
|
while (i < s.length) {
|
|
137
231
|
const c = s[i++];
|
|
138
|
-
if (escaped) {
|
|
139
|
-
|
|
232
|
+
if (escaped) {
|
|
233
|
+
out += `\\${c}`;
|
|
234
|
+
escaped = false;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (c === '\\') {
|
|
238
|
+
escaped = true;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
140
241
|
if (c === '"') break;
|
|
141
242
|
out += c;
|
|
142
243
|
}
|
|
@@ -144,7 +245,8 @@ function tokenize(source) {
|
|
|
144
245
|
continue;
|
|
145
246
|
}
|
|
146
247
|
if (ch === '<') {
|
|
147
|
-
let out = '';
|
|
248
|
+
let out = '';
|
|
249
|
+
i += 1;
|
|
148
250
|
while (i < s.length && s[i] !== '>') out += s[i++];
|
|
149
251
|
if (s[i] !== '>') throw new Error('Unterminated IRI');
|
|
150
252
|
i += 1;
|
|
@@ -163,11 +265,13 @@ function tokenize(source) {
|
|
|
163
265
|
|
|
164
266
|
function classifyToken(raw) {
|
|
165
267
|
if (raw === '@prefix') return { type: '@prefix', value: raw };
|
|
268
|
+
if (/^VERSION$/i.test(raw)) return { type: 'VERSION', value: raw };
|
|
166
269
|
if (raw === 'a') return { type: 'qname', value: 'rdf:type' };
|
|
167
270
|
if (raw.startsWith('?')) return { type: 'var', value: raw.slice(1) };
|
|
168
271
|
if (/^(true|false)$/i.test(raw)) return { type: 'boolean', value: /^true$/i.test(raw) };
|
|
169
272
|
if (/^[+-]?\d+$/.test(raw)) return { type: 'number', value: Number.parseInt(raw, 10) };
|
|
170
|
-
if (/^[+-]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?$/.test(raw) || /^[+-]?\d+[eE][+-]?\d+$/.test(raw))
|
|
273
|
+
if (/^[+-]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?$/.test(raw) || /^[+-]?\d+[eE][+-]?\d+$/.test(raw))
|
|
274
|
+
return { type: 'number', value: Number(raw) };
|
|
171
275
|
return { type: 'qname', value: raw };
|
|
172
276
|
}
|
|
173
277
|
|
|
@@ -177,7 +281,9 @@ class Parser {
|
|
|
177
281
|
this.pos = 0;
|
|
178
282
|
this.blankCounter = 0;
|
|
179
283
|
}
|
|
180
|
-
eof() {
|
|
284
|
+
eof() {
|
|
285
|
+
return this.pos >= this.tokens.length;
|
|
286
|
+
}
|
|
181
287
|
peek(value = undefined) {
|
|
182
288
|
const tok = this.tokens[this.pos];
|
|
183
289
|
if (value === undefined) return tok;
|
|
@@ -187,46 +293,77 @@ class Parser {
|
|
|
187
293
|
if (this.eof()) throw new Error('Unexpected end of input');
|
|
188
294
|
return this.tokens[this.pos++];
|
|
189
295
|
}
|
|
190
|
-
accept(type) {
|
|
296
|
+
accept(type) {
|
|
297
|
+
if (this.peek(type)) return this.next();
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
191
300
|
expect(type) {
|
|
192
301
|
const tok = this.next();
|
|
193
302
|
if (tok.type !== type) throw new Error(`Expected ${type}, got ${tok.type} (${tok.value})`);
|
|
194
303
|
return tok;
|
|
195
304
|
}
|
|
196
|
-
freshBlank(prefix = 'b') {
|
|
305
|
+
freshBlank(prefix = 'b') {
|
|
306
|
+
this.blankCounter += 1;
|
|
307
|
+
return `_${prefix}${this.blankCounter}`;
|
|
308
|
+
}
|
|
197
309
|
skipPrefix() {
|
|
198
310
|
this.expect('@prefix');
|
|
199
311
|
// Prefix declaration is irrelevant after QName compaction; skip until final dot.
|
|
200
312
|
while (!this.eof() && !this.accept('.')) this.next();
|
|
201
313
|
}
|
|
314
|
+
skipVersion() {
|
|
315
|
+
this.expect('VERSION');
|
|
316
|
+
if (this.peek('string')) this.next();
|
|
317
|
+
this.accept('.');
|
|
318
|
+
}
|
|
202
319
|
parseProgram() {
|
|
203
|
-
const facts = [],
|
|
320
|
+
const facts = [],
|
|
321
|
+
rules = [],
|
|
322
|
+
queries = [],
|
|
323
|
+
prefixes = {};
|
|
204
324
|
while (!this.eof()) {
|
|
205
325
|
if (this.accept('@prefix')) {
|
|
206
326
|
this.pos -= 1;
|
|
207
327
|
const start = this.pos;
|
|
208
328
|
this.skipPrefix();
|
|
209
|
-
const slice = this.tokens
|
|
329
|
+
const slice = this.tokens
|
|
330
|
+
.slice(start, this.pos)
|
|
331
|
+
.map((tok) => tok.value)
|
|
332
|
+
.join(' ');
|
|
210
333
|
const m = slice.match(/@prefix\s+([^\s]*)\s+<([^>]+)>/);
|
|
211
334
|
if (m) prefixes[(m[1] || ':').replace(/:$/, '')] = m[2];
|
|
212
335
|
continue;
|
|
213
336
|
}
|
|
337
|
+
if (this.peek('VERSION')) {
|
|
338
|
+
this.skipVersion();
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
214
341
|
if (this.peek('{')) {
|
|
215
342
|
const lhs = this.parseFormula('body');
|
|
216
343
|
if (this.accept('=>')) {
|
|
217
|
-
if (
|
|
218
|
-
this.
|
|
344
|
+
if (
|
|
345
|
+
(this.peek('qname') && this.tokens[this.pos].value === 'false') ||
|
|
346
|
+
(this.peek('boolean') && this.tokens[this.pos].value === false)
|
|
347
|
+
) {
|
|
348
|
+
this.next();
|
|
349
|
+
this.accept('.');
|
|
219
350
|
rules.push({ kind: 'fuse', id: rules.length + 1, body: lhs });
|
|
220
351
|
} else {
|
|
221
|
-
const head = this.parseFormula('head');
|
|
352
|
+
const head = this.parseFormula('head');
|
|
353
|
+
this.accept('.');
|
|
222
354
|
rules.push({ kind: 'rule', id: rules.length + 1, body: lhs, head });
|
|
223
355
|
}
|
|
224
356
|
} else if (this.accept('<=')) {
|
|
225
|
-
if (
|
|
226
|
-
this.
|
|
357
|
+
if (
|
|
358
|
+
(this.peek('qname') && this.tokens[this.pos].value === 'true') ||
|
|
359
|
+
(this.peek('boolean') && this.tokens[this.pos].value === true)
|
|
360
|
+
) {
|
|
361
|
+
this.next();
|
|
362
|
+
this.accept('.');
|
|
227
363
|
rules.push({ kind: 'backward', id: rules.length + 1, body: [], head: lhs });
|
|
228
364
|
} else {
|
|
229
|
-
const rhs = this.parseFormula('body');
|
|
365
|
+
const rhs = this.parseFormula('body');
|
|
366
|
+
this.accept('.');
|
|
230
367
|
rules.push({ kind: 'backward', id: rules.length + 1, body: rhs, head: lhs });
|
|
231
368
|
}
|
|
232
369
|
} else {
|
|
@@ -234,7 +371,12 @@ class Parser {
|
|
|
234
371
|
const triples = this.parseStatementRest('fact', subject);
|
|
235
372
|
this.accept('.');
|
|
236
373
|
for (const triple of triples) {
|
|
237
|
-
if (
|
|
374
|
+
if (
|
|
375
|
+
triple.s?.kind === 'formula' &&
|
|
376
|
+
triple.p?.kind === 'iri' &&
|
|
377
|
+
triple.p.value === 'log:query' &&
|
|
378
|
+
triple.o?.kind === 'formula'
|
|
379
|
+
) {
|
|
238
380
|
queries.push({ id: queries.length + 1, premise: triple.s.atoms, conclusion: triple.o.atoms });
|
|
239
381
|
} else {
|
|
240
382
|
facts.push(triple);
|
|
@@ -295,6 +437,13 @@ class Parser {
|
|
|
295
437
|
}
|
|
296
438
|
if (tok.type === 'number' || tok.type === 'boolean') return L(tok.value);
|
|
297
439
|
if (tok.type === 'iri' || tok.type === 'qname') return I(tok.value);
|
|
440
|
+
if (tok.type === '<<(') {
|
|
441
|
+
const subject = this.parseTerm(mode, sink);
|
|
442
|
+
const predicate = this.parsePredicate();
|
|
443
|
+
const object = this.parseTerm(mode, sink);
|
|
444
|
+
this.expect(')>>');
|
|
445
|
+
return { kind: 'triple', s: subject, p: predicate, o: object };
|
|
446
|
+
}
|
|
298
447
|
if (tok.type === '(') {
|
|
299
448
|
const items = [];
|
|
300
449
|
while (!this.accept(')')) items.push(this.parseTerm(mode, sink));
|
|
@@ -310,7 +459,10 @@ class Parser {
|
|
|
310
459
|
return { kind: 'formula', atoms };
|
|
311
460
|
}
|
|
312
461
|
if (tok.type === '[') {
|
|
313
|
-
const id =
|
|
462
|
+
const id =
|
|
463
|
+
mode === 'body'
|
|
464
|
+
? V(this.freshBlank('bodyBlank'))
|
|
465
|
+
: Blank(this.freshBlank(mode === 'head' ? 'headBlank' : 'blank'));
|
|
314
466
|
if (!this.accept(']')) {
|
|
315
467
|
while (true) {
|
|
316
468
|
const predicate = this.parsePredicate();
|
|
@@ -329,6 +481,11 @@ class Parser {
|
|
|
329
481
|
}
|
|
330
482
|
}
|
|
331
483
|
|
|
484
|
+
// parseN3 separates the source file into four compiler inputs:
|
|
485
|
+
// facts -> serialized as examples/input/<name>.trig
|
|
486
|
+
// rules -> compiled into JavaScript fixpoint code
|
|
487
|
+
// queries -> rendered as selected output checks
|
|
488
|
+
// prefixes -> carried into generated TriG evidence
|
|
332
489
|
function parseN3(n3) {
|
|
333
490
|
const parser = new Parser(tokenize(n3));
|
|
334
491
|
return parser.parseProgram();
|
|
@@ -340,15 +497,22 @@ function termToJsComment(term) {
|
|
|
340
497
|
if (term.kind === 'var') return `?${term.value}`;
|
|
341
498
|
if (term.kind === 'blank') return term.value;
|
|
342
499
|
if (term.kind === 'list') return `(${term.items.map(termToJsComment).join(' ')})`;
|
|
500
|
+
if (term.kind === 'triple') return `<<( ${termToJsComment(term.s)} ${termToJsComment(term.p)} ${termToJsComment(term.o)} )>>`;
|
|
343
501
|
if (term.kind === 'formula') return `{ ${term.atoms.map(atomToComment).join(' . ')} }`;
|
|
344
502
|
return String(term.value ?? term);
|
|
345
503
|
}
|
|
346
|
-
function atomToComment(atom) {
|
|
504
|
+
function atomToComment(atom) {
|
|
505
|
+
return `${termToJsComment(atom.s)} ${termToJsComment(atom.p)} ${termToJsComment(atom.o)}`;
|
|
506
|
+
}
|
|
347
507
|
|
|
348
508
|
function compilationStats(program) {
|
|
349
509
|
const predicates = new Set();
|
|
350
510
|
const builtins = new Set();
|
|
351
|
-
for (const atom of [
|
|
511
|
+
for (const atom of [
|
|
512
|
+
...program.facts,
|
|
513
|
+
...program.rules.flatMap((r) => [...(r.body || []), ...(r.head || [])]),
|
|
514
|
+
...(program.queries || []).flatMap((q) => [...(q.premise || []), ...(q.conclusion || [])]),
|
|
515
|
+
]) {
|
|
352
516
|
const p = atom.p?.value;
|
|
353
517
|
if (p) predicates.add(p);
|
|
354
518
|
if (/^(math|string|list|log|crypto):/.test(p)) builtins.add(p);
|
|
@@ -364,23 +528,130 @@ function compilationStats(program) {
|
|
|
364
528
|
};
|
|
365
529
|
}
|
|
366
530
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
531
|
+
// Source facts are emitted as RDF 1.2 TriG. Formulas that appear as objects are
|
|
532
|
+
// lifted into named graphs so the generated runner can load evidence directly
|
|
533
|
+
// from .trig without going through an intermediate n3gen conversion step.
|
|
534
|
+
function trigString(value) {
|
|
535
|
+
return JSON.stringify(String(value));
|
|
536
|
+
}
|
|
537
|
+
function trigNumber(value) {
|
|
538
|
+
if (Object.is(value, -0)) return '0';
|
|
539
|
+
if (Number.isInteger(value)) return String(value);
|
|
540
|
+
return Number(value.toPrecision(15)).toString();
|
|
541
|
+
}
|
|
542
|
+
function inputLiteralToN3(value) {
|
|
543
|
+
if (typeof value === 'string') return trigString(value);
|
|
544
|
+
if (typeof value === 'number') return trigNumber(value);
|
|
545
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
546
|
+
return trigString(value);
|
|
547
|
+
}
|
|
548
|
+
function inputTermToN3(term) {
|
|
549
|
+
if (!term) return 'undefined';
|
|
550
|
+
if (term.kind === 'iri') return term.value;
|
|
551
|
+
if (term.kind === 'lit') return inputLiteralToN3(term.value);
|
|
552
|
+
if (term.kind === 'var') return '?' + term.value;
|
|
553
|
+
if (term.kind === 'blank') return term.value.startsWith('_:') ? term.value : '_:' + term.value.replace(/^_+/, '');
|
|
554
|
+
if (term.kind === 'list') return '(' + term.items.map(inputTermToN3).join(' ') + ')';
|
|
555
|
+
if (term.kind === 'triple') return '<<( ' + inputTermToN3(term.s) + ' ' + inputTermToN3(term.p) + ' ' + inputTermToN3(term.o) + ' )>>';
|
|
556
|
+
if (term.kind === 'formula') return '{ ' + term.atoms.map(inputAtomToN3).join(' . ') + ' }';
|
|
557
|
+
return String(term.value ?? term);
|
|
558
|
+
}
|
|
559
|
+
function inputAtomToN3(atom) {
|
|
560
|
+
return inputTermToN3(atom.s) + ' ' + inputTermToN3(atom.p) + ' ' + inputTermToN3(atom.o);
|
|
561
|
+
}
|
|
562
|
+
function inputTermHasTripleTerm(term) {
|
|
563
|
+
if (!term) return false;
|
|
564
|
+
if (term.kind === 'triple') return true;
|
|
565
|
+
if (term.kind === 'list') return term.items.some(inputTermHasTripleTerm);
|
|
566
|
+
if (term.kind === 'formula') return term.atoms.some(inputAtomHasTripleTerm);
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
function inputAtomHasTripleTerm(atom) {
|
|
570
|
+
return inputTermHasTripleTerm(atom.s) || inputTermHasTripleTerm(atom.p) || inputTermHasTripleTerm(atom.o);
|
|
571
|
+
}
|
|
572
|
+
function programHasTripleTerms(program) {
|
|
573
|
+
return [
|
|
574
|
+
...(program.facts || []),
|
|
575
|
+
...(program.rules || []).flatMap((r) => [...(r.body || []), ...(r.head || [])]),
|
|
576
|
+
...(program.queries || []).flatMap((q) => [...(q.premise || []), ...(q.conclusion || [])]),
|
|
577
|
+
].some(inputAtomHasTripleTerm);
|
|
578
|
+
}
|
|
579
|
+
function formulaBlock(label, atoms) {
|
|
580
|
+
const lines = [label + ' {'];
|
|
581
|
+
for (const atom of atoms) lines.push(' ' + inputAtomToN3(atom) + ' .');
|
|
582
|
+
lines.push('}');
|
|
583
|
+
return lines.join('\n');
|
|
584
|
+
}
|
|
585
|
+
function atomToTrig(atom, state) {
|
|
586
|
+
if (atom.o && atom.o.kind === 'formula') {
|
|
587
|
+
if (atom.p && atom.p.kind === 'iri' && atom.p.value === 'log:nameOf') {
|
|
588
|
+
state.graphs.push(formulaBlock(inputTermToN3(atom.s), atom.o.atoms));
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
state.formulaCounter += 1;
|
|
592
|
+
const label = `in:formula${state.formulaCounter}`;
|
|
593
|
+
state.graphs.push(formulaBlock(label, atom.o.atoms));
|
|
594
|
+
return inputTermToN3(atom.s) + ' ' + inputTermToN3(atom.p) + ' ' + label + ' .';
|
|
595
|
+
}
|
|
596
|
+
return inputAtomToN3(atom) + ' .';
|
|
597
|
+
}
|
|
598
|
+
function inputFactsToTrig(facts) {
|
|
599
|
+
const state = { formulaCounter: 0, graphs: [] };
|
|
600
|
+
const triples = [];
|
|
601
|
+
for (const atom of facts) {
|
|
602
|
+
const line = atomToTrig(atom, state);
|
|
603
|
+
if (line) triples.push(line);
|
|
604
|
+
}
|
|
605
|
+
return { triples, graphs: state.graphs };
|
|
606
|
+
}
|
|
607
|
+
function prefixLines(prefixes) {
|
|
608
|
+
const merged = { ...(prefixes || {}) };
|
|
609
|
+
if (!Object.hasOwn(merged, 'log')) merged.log = 'http://www.w3.org/2000/10/swap/log#';
|
|
610
|
+
if (!Object.hasOwn(merged, 'see')) merged.see = 'https://example.org/see#';
|
|
611
|
+
if (!Object.hasOwn(merged, 'in')) merged.in = 'https://example.org/see/input#';
|
|
612
|
+
return Object.entries(merged).map(([prefix, iri]) => `@prefix ${prefix ? prefix + ':' : ':'} <${iri}> .`);
|
|
613
|
+
}
|
|
614
|
+
function generateInputTrig(n3Path, name, title, header, stats, program) {
|
|
615
|
+
const { triples, graphs } = inputFactsToTrig(program.facts);
|
|
616
|
+
const metadata = [
|
|
617
|
+
'in:metadata {',
|
|
618
|
+
' in:run a see:InputDataset .',
|
|
619
|
+
` in:run see:name ${trigString(name)} .`,
|
|
620
|
+
` in:run see:title ${trigString(title)} .`,
|
|
621
|
+
` in:run see:sourceFile ${trigString(path.relative(ROOT, path.resolve(n3Path)))} .`,
|
|
622
|
+
` in:run see:sourceSHA256 ${trigString(stats.sourceHash)} .`,
|
|
623
|
+
` in:run see:description ${trigString(header.description || '')} .`,
|
|
624
|
+
' in:run see:compiler "see.js N3-to-JS compiler" .',
|
|
625
|
+
` in:run see:inputFacts ${stats.facts} .`,
|
|
626
|
+
` in:run see:compiledRules ${stats.rules} .`,
|
|
627
|
+
` in:run see:compiledBackwardRules ${stats.backwardRules} .`,
|
|
628
|
+
` in:run see:compiledFuses ${stats.fuses} .`,
|
|
629
|
+
` in:run see:compiledQueries ${stats.queries} .`,
|
|
630
|
+
'}',
|
|
631
|
+
].join('\n');
|
|
632
|
+
const sections = [
|
|
633
|
+
...(programHasTripleTerms(program) ? ['VERSION "1.2"', ''] : []),
|
|
634
|
+
...prefixLines(program.prefixes),
|
|
635
|
+
'',
|
|
636
|
+
'# Formal SEE input evidence in RDF 1.2 TriG.',
|
|
637
|
+
'# The generated runner reads this TriG evidence directly.',
|
|
638
|
+
'',
|
|
639
|
+
triples.length ? triples.join('\n') : '# No source facts were present in the N3 program.',
|
|
640
|
+
];
|
|
641
|
+
if (graphs.length) sections.push('', graphs.join('\n\n'));
|
|
642
|
+
sections.push('', metadata, '');
|
|
643
|
+
return sections.join('\n');
|
|
644
|
+
}
|
|
645
|
+
// The runtime below is copied verbatim into each generated example. Keep it
|
|
646
|
+
// dependency-light: generated examples should be executable with Node alone plus
|
|
647
|
+
// the local examples/_see.js TriG loader.
|
|
378
648
|
function runtimeSource() {
|
|
379
649
|
return String.raw`
|
|
380
650
|
const crypto = require('crypto');
|
|
381
651
|
|
|
382
652
|
function canonical(term) {
|
|
383
653
|
if (term.kind === 'list') return ['list', term.items.map(canonical)];
|
|
654
|
+
if (term.kind === 'triple') return ['triple', canonical(term.s), canonical(term.p), canonical(term.o)];
|
|
384
655
|
if (term.kind === 'formula') return ['formula', term.atoms.map((a) => [canonical(a.s), canonical(a.p), canonical(a.o)])];
|
|
385
656
|
return [term.kind, term.value];
|
|
386
657
|
}
|
|
@@ -390,6 +661,7 @@ function compoundIndexKey() { return Array.from(arguments).map(termIndexKey).joi
|
|
|
390
661
|
function termIsConcrete(t) {
|
|
391
662
|
if (!t || t.kind === 'var') return false;
|
|
392
663
|
if (t.kind === 'list') return t.items.every(termIsConcrete);
|
|
664
|
+
if (t.kind === 'triple') return termIsConcrete(t.s) && termIsConcrete(t.p) && termIsConcrete(t.o);
|
|
393
665
|
if (t.kind === 'formula') return t.atoms.every((a) => termIsConcrete(a.s) && termIsConcrete(a.p) && termIsConcrete(a.o));
|
|
394
666
|
return true;
|
|
395
667
|
}
|
|
@@ -405,6 +677,7 @@ function primitive(t) {
|
|
|
405
677
|
if (t.kind === 'iri') return t.value.replace(/^:/, '');
|
|
406
678
|
if (t.kind === 'blank') return t.value;
|
|
407
679
|
if (t.kind === 'list') return t.items.map(primitive);
|
|
680
|
+
if (t.kind === 'triple') return termToN3(t);
|
|
408
681
|
if (t.kind === 'formula') return termToN3(t);
|
|
409
682
|
return undefined;
|
|
410
683
|
}
|
|
@@ -425,6 +698,7 @@ function termToN3(t) {
|
|
|
425
698
|
if (t.kind === 'var') return '?' + t.value;
|
|
426
699
|
if (t.kind === 'blank') return t.value.startsWith('_:') ? t.value : '_:' + t.value.replace(/^_+/, '');
|
|
427
700
|
if (t.kind === 'list') return '(' + t.items.map(termToN3).join(' ') + ')';
|
|
701
|
+
if (t.kind === 'triple') return '<<( ' + termToN3(t.s) + ' ' + termToN3(t.p) + ' ' + termToN3(t.o) + ' )>>';
|
|
428
702
|
if (t.kind === 'formula') return '{ ' + t.atoms.map(atomToN3).join(' . ') + ' }';
|
|
429
703
|
return String(t.value ?? t);
|
|
430
704
|
}
|
|
@@ -447,6 +721,7 @@ function resolve(term, env, seen = new Set()) {
|
|
|
447
721
|
return resolve(env[term.value], env, seen);
|
|
448
722
|
}
|
|
449
723
|
if (term.kind === 'list') return list(term.items.map((item) => resolve(item, env, seen)));
|
|
724
|
+
if (term.kind === 'triple') return { kind: 'triple', s: resolve(term.s, env), p: resolve(term.p, env), o: resolve(term.o, env) };
|
|
450
725
|
if (term.kind === 'formula') return { kind: 'formula', atoms: term.atoms.map((a) => ({ s: resolve(a.s, env), p: resolve(a.p, env), o: resolve(a.o, env) })) };
|
|
451
726
|
return term;
|
|
452
727
|
}
|
|
@@ -464,6 +739,14 @@ function unify(a, b, env) {
|
|
|
464
739
|
}
|
|
465
740
|
return out;
|
|
466
741
|
}
|
|
742
|
+
if (a.kind === 'triple' || b.kind === 'triple') {
|
|
743
|
+
if (a.kind !== 'triple' || b.kind !== 'triple') return null;
|
|
744
|
+
let out = unify(a.s, b.s, env);
|
|
745
|
+
if (!out) return null;
|
|
746
|
+
out = unify(a.p, b.p, out);
|
|
747
|
+
if (!out) return null;
|
|
748
|
+
return unify(a.o, b.o, out);
|
|
749
|
+
}
|
|
467
750
|
return deepEqual(a, b) ? env : null;
|
|
468
751
|
}
|
|
469
752
|
function bind(pattern, value, env) { return unify(pattern, value, env); }
|
|
@@ -479,6 +762,7 @@ function termIsGround(t, env) {
|
|
|
479
762
|
const r = resolve(t, env);
|
|
480
763
|
if (r.kind === 'var') return false;
|
|
481
764
|
if (r.kind === 'list') return r.items.every((item) => termIsGround(item, env));
|
|
765
|
+
if (r.kind === 'triple') return termIsGround(r.s, env) && termIsGround(r.p, env) && termIsGround(r.o, env);
|
|
482
766
|
if (r.kind === 'formula') return r.atoms.every((atom) => atomIsGround(atom, env));
|
|
483
767
|
return true;
|
|
484
768
|
}
|
|
@@ -1038,6 +1322,7 @@ function instantiate(term, env, ruleId) {
|
|
|
1038
1322
|
}
|
|
1039
1323
|
if (term.kind === 'blank') return blank('_:r' + ruleId + '_' + envSignature(env) + '_' + term.value.replace(/^_/, ''));
|
|
1040
1324
|
if (term.kind === 'list') return list(term.items.map((item) => instantiate(item, env, ruleId)));
|
|
1325
|
+
if (term.kind === 'triple') return { kind: 'triple', s: instantiate(term.s, env, ruleId), p: instantiate(term.p, env, ruleId), o: instantiate(term.o, env, ruleId) };
|
|
1041
1326
|
if (term.kind === 'formula') return { kind: 'formula', atoms: term.atoms.map((a) => ({ s: instantiate(a.s, env, ruleId), p: instantiate(a.p, env, ruleId), o: instantiate(a.o, env, ruleId) })) };
|
|
1042
1327
|
return cloneTerm(term);
|
|
1043
1328
|
}
|
|
@@ -1413,19 +1698,19 @@ function renderStructuredOutput({ title, graph, queries = [], rules = [], initia
|
|
|
1413
1698
|
const lines = [];
|
|
1414
1699
|
lines.push('# ' + title);
|
|
1415
1700
|
lines.push('');
|
|
1416
|
-
lines.push('##
|
|
1701
|
+
lines.push('## Entailment');
|
|
1417
1702
|
if (mode === 'query') {
|
|
1418
1703
|
lines.push('The compiled query selected ' + selected.length + ' fact(s) after the rule closure was computed.');
|
|
1419
1704
|
} else if (mode === 'formula') {
|
|
1420
|
-
lines.push('The derivation produced ' + selected.length + ' formula-valued
|
|
1705
|
+
lines.push('The derivation produced ' + selected.length + ' formula-valued entailment(s).');
|
|
1421
1706
|
} else {
|
|
1422
1707
|
lines.push('The derivation produced ' + derived.length + ' new fact(s) from ' + initialFacts.length + ' stated fact(s).');
|
|
1423
1708
|
}
|
|
1424
|
-
if (keyFact) lines.push('Main
|
|
1709
|
+
if (keyFact) lines.push('Main entailment: **' + factSentence(keyFact) + '**');
|
|
1425
1710
|
const bullets = selected.slice(-6).reverse();
|
|
1426
1711
|
if (bullets.length) {
|
|
1427
1712
|
lines.push('');
|
|
1428
|
-
lines.push('Selected
|
|
1713
|
+
lines.push('Selected entailments:');
|
|
1429
1714
|
for (const fact of bullets) lines.push('- ' + codeFact(fact));
|
|
1430
1715
|
}
|
|
1431
1716
|
lines.push('');
|
|
@@ -1487,15 +1772,15 @@ function dedupeExplanationHeadings(text) {
|
|
|
1487
1772
|
function normalizePublicReport(markdown, title) {
|
|
1488
1773
|
let text = String(markdown || '').trimEnd();
|
|
1489
1774
|
if (!/^\s*#\s+/m.test(text)) text = '# ' + title + '\n\n' + text;
|
|
1490
|
-
if (!/^##\s+
|
|
1491
|
-
text = text.replace(/^(#\s+[^\n]+\n*)/, '$1\n##
|
|
1775
|
+
if (!/^##\s+Entailment\s*$/mi.test(text)) {
|
|
1776
|
+
text = text.replace(/^(#\s+[^\n]+\n*)/, '$1\n## Entailment\n');
|
|
1492
1777
|
}
|
|
1493
1778
|
if (!/^##\s+Explanation\s*$/mi.test(text)) {
|
|
1494
1779
|
text += '\n\n## Explanation\nNo additional explanation was provided by the generated output.';
|
|
1495
1780
|
}
|
|
1496
1781
|
text = text.replace(/^##\s+([^\n]+?)\s*$/gm, (line, heading) => {
|
|
1497
1782
|
const normalized = heading.trim().toLowerCase();
|
|
1498
|
-
if (normalized === 'insight' || normalized === 'explanation') return '## ' + (normalized === '
|
|
1783
|
+
if (normalized === 'insight' || normalized === 'conclusion' || normalized === 'entailment' || normalized === 'explanation') return '## ' + (normalized === 'explanation' ? 'Explanation' : 'Entailment');
|
|
1499
1784
|
return '**' + heading.trim() + '**';
|
|
1500
1785
|
});
|
|
1501
1786
|
text = dedupeExplanationHeadings(text);
|
|
@@ -1504,13 +1789,13 @@ function normalizePublicReport(markdown, title) {
|
|
|
1504
1789
|
function markdownize(raw, title) {
|
|
1505
1790
|
let text = String(raw || '');
|
|
1506
1791
|
text = text
|
|
1507
|
-
.replace(/===\s*Answer\s*===/g, '##
|
|
1792
|
+
.replace(/===\s*Answer\s*===/g, '## Entailment')
|
|
1508
1793
|
.replace(/===\s*Reason\s+Why\s*===/gi, '## Explanation')
|
|
1509
1794
|
.replace(/===\s*Explanation\s*===/gi, '## Explanation')
|
|
1510
1795
|
.replace(/===\s*([^=]+?)\s*===/g, (_, h) => '**' + h.trim() + '**');
|
|
1511
1796
|
text = text.replace(/^C(\d+)\s+OK\s*-\s*/gm, 'C$1: ');
|
|
1512
1797
|
text = dedupeExplanationHeadings(text);
|
|
1513
|
-
if (!text.trim()) text = '##
|
|
1798
|
+
if (!text.trim()) text = '## Entailment\nNo log:outputString facts were derived.\n\n## Explanation\nThe compiled derivation did not produce authored report text.';
|
|
1514
1799
|
return normalizePublicReport(text, title);
|
|
1515
1800
|
}
|
|
1516
1801
|
function authoredSupportAppendix(graph, queries, rules, initialFacts, trace) {
|
|
@@ -1565,10 +1850,17 @@ function renderPresentation(graph, queries, rules, initialFacts, title, trace) {
|
|
|
1565
1850
|
`;
|
|
1566
1851
|
}
|
|
1567
1852
|
|
|
1568
|
-
|
|
1569
1853
|
function generateExampleJs(name, title, program, stats, doc) {
|
|
1570
|
-
const rulesWithComments = program.rules.map((rule) => ({
|
|
1571
|
-
|
|
1854
|
+
const rulesWithComments = program.rules.map((rule) => ({
|
|
1855
|
+
...rule,
|
|
1856
|
+
bodyComment: (rule.body || []).map(atomToComment),
|
|
1857
|
+
headComment: (rule.head || []).map(atomToComment),
|
|
1858
|
+
}));
|
|
1859
|
+
const queriesWithComments = (program.queries || []).map((query) => ({
|
|
1860
|
+
...query,
|
|
1861
|
+
premiseComment: (query.premise || []).map(atomToComment),
|
|
1862
|
+
conclusionComment: (query.conclusion || []).map(atomToComment),
|
|
1863
|
+
}));
|
|
1572
1864
|
return `#!/usr/bin/env node
|
|
1573
1865
|
'use strict';
|
|
1574
1866
|
const fs = require('fs');
|
|
@@ -1614,6 +1906,16 @@ function formalOutputFacts(graph, queries, rules, initialFacts) {
|
|
|
1614
1906
|
}
|
|
1615
1907
|
return out;
|
|
1616
1908
|
}
|
|
1909
|
+
function termHasTripleTerm(term) {
|
|
1910
|
+
if (!term) return false;
|
|
1911
|
+
if (term.kind === 'triple') return true;
|
|
1912
|
+
if (term.kind === 'list') return term.items.some(termHasTripleTerm);
|
|
1913
|
+
if (term.kind === 'formula') return term.atoms.some(atomHasTripleTerm);
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
function atomHasTripleTerm(atom) { return termHasTripleTerm(atom.s) || termHasTripleTerm(atom.p) || termHasTripleTerm(atom.o); }
|
|
1917
|
+
function factsHaveTripleTerms(facts) { return (facts || []).some(atomHasTripleTerm); }
|
|
1918
|
+
function trigHasVersion12(trig) { return /^\s*(?:@version|VERSION)\s+["']1\.2["']/mi.test(String(trig || '')); }
|
|
1617
1919
|
function trigGraphBlock(label, atoms) {
|
|
1618
1920
|
const lines = [label + ' {'];
|
|
1619
1921
|
for (const atom of atoms || []) lines.push(' ' + atomToN3(atom) + ' .');
|
|
@@ -1661,7 +1963,8 @@ function formalOutputToTrig(facts, trig) {
|
|
|
1661
1963
|
const prefixes = prefixLinesFromTrig(trig);
|
|
1662
1964
|
if (state.needOutPrefix && !prefixes.some((line) => line.toLowerCase().startsWith('@prefix out:'))) prefixes.push('@prefix out: <https://example.org/see/output#> .');
|
|
1663
1965
|
const nl = String.fromCharCode(10);
|
|
1664
|
-
|
|
1966
|
+
const version = factsHaveTripleTerms(facts) ? 'VERSION "1.2"' + nl + nl : '';
|
|
1967
|
+
return version + prefixes.join(nl) + nl + nl + body.join(nl);
|
|
1665
1968
|
}
|
|
1666
1969
|
function appendFormalTrigOutput(markdown, graph, queries, rules, initialFacts, data) {
|
|
1667
1970
|
const trig = formalOutputToTrig(formalOutputFacts(graph, queries, rules, initialFacts), data && data.trig);
|
|
@@ -1680,19 +1983,43 @@ module.exports = { trustedDerivation, outputMarkdown, documentationMarkdown, wri
|
|
|
1680
1983
|
`;
|
|
1681
1984
|
}
|
|
1682
1985
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1986
|
+
// Documentation is generated from compilation metadata rather than hand-written
|
|
1987
|
+
// per example, keeping examples/output and examples/doc reproducible snapshots.
|
|
1988
|
+
function generateDoc(name, title, header, stats) {
|
|
1989
|
+
const description = header.description
|
|
1990
|
+
? `
|
|
1685
1991
|
${header.description}
|
|
1686
|
-
`
|
|
1992
|
+
`
|
|
1993
|
+
: '';
|
|
1994
|
+
const builtins = stats.builtins.length ? stats.builtins.map((b) => `- \`${b}\``).join('\n') : '- none';
|
|
1995
|
+
return `# ${title}\n\nGenerated by \`see.js\` from a Notation3 source file.\n${description}\n## Compilation summary\n\n- Example name: \`${name}\`\n- Input facts emitted: ${stats.facts}\n- Forward rules compiled: ${stats.rules}\n- Backward predicate rules compiled: ${stats.backwardRules}\n- Fuses compiled: ${stats.fuses}\n- Predicate count: ${stats.predicates}\n\n## Built-ins used\n\n${builtins}\n\n## Runtime model\n\nThe generated \`examples/${name}.js\` is a specialized JavaScript derivation program. For ordinary sources, \`see.js\` emits the source facts as \`examples/input/${name}.trig\`. For rules-only sources, generation can reuse an existing external evidence file such as \`examples/input/${name.replace(/_/g, '-')}.trig\` or \`examples/input/${name}.trig\`. The runner reads that TriG evidence directly and performs a local fixpoint derivation; it does not parse the program source or call an external reasoner.\n\n## Output model\n\nRunning \`node examples/${name}.js\` produces a SEE-style Markdown report with an **Entailment** section, an **Explanation** section, and a **Formal TriG Output** section containing the selected derived/query facts.\n`;
|
|
1996
|
+
}
|
|
1687
1997
|
|
|
1688
1998
|
function runNode(file, cwd = ROOT, args = []) {
|
|
1689
1999
|
const result = spawnSync(process.execPath, [file, ...args], { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
1690
|
-
if (result.status !== 0)
|
|
2000
|
+
if (result.status !== 0)
|
|
2001
|
+
throw new Error(`generated example failed:
|
|
1691
2002
|
${result.stderr || result.stdout}`);
|
|
1692
2003
|
return result.stdout;
|
|
1693
2004
|
}
|
|
1694
2005
|
|
|
1695
|
-
|
|
2006
|
+
// compile is pure with respect to the repository: it reads one source .n3 file
|
|
2007
|
+
// and returns all generated text. The generate/render commands decide whether
|
|
2008
|
+
// those artefacts are written to disk or executed from a temporary directory.
|
|
2009
|
+
function compile(n3Path, options = {}) {
|
|
2010
|
+
const absolute = path.resolve(n3Path);
|
|
2011
|
+
const n3 = readText(absolute);
|
|
2012
|
+
const name = options.name || slugify(path.basename(absolute));
|
|
2013
|
+
const title = parseHeader(n3, titleFromSlug(name)).title;
|
|
2014
|
+
const header = parseHeader(n3, titleFromSlug(name));
|
|
2015
|
+
const program = parseN3(n3);
|
|
2016
|
+
const stats = compilationStats(program);
|
|
2017
|
+
stats.sourceHash = sha256(n3);
|
|
2018
|
+
const inputTrig = generateInputTrig(absolute, name, title, header, stats, program);
|
|
2019
|
+
const doc = generateDoc(name, title, header, stats);
|
|
2020
|
+
const exampleJs = generateExampleJs(name, title, program, stats, doc);
|
|
2021
|
+
return { name, title, program, stats, inputTrig, exampleJs, doc };
|
|
2022
|
+
}
|
|
1696
2023
|
|
|
1697
2024
|
function inputNameCandidates(name) {
|
|
1698
2025
|
const out = [name];
|
|
@@ -1714,19 +2041,21 @@ function inputCandidateScore(file) {
|
|
|
1714
2041
|
return { facts: -1, size: -1 };
|
|
1715
2042
|
}
|
|
1716
2043
|
}
|
|
2044
|
+
// Rules-only examples can reuse an externally authored TriG evidence file. The
|
|
2045
|
+
// scoring prefers the candidate that advertises the most input facts, then the
|
|
2046
|
+
// larger file, so dashed public datasets such as path-discovery.trig win over
|
|
2047
|
+
// empty generated placeholders.
|
|
1717
2048
|
function existingExternalInputName(name) {
|
|
1718
2049
|
const candidates = inputNameCandidates(name)
|
|
1719
2050
|
.map((base, order) => ({ base, order, file: path.join(INPUT_DIR, `${base}.trig`) }))
|
|
1720
2051
|
.filter((c) => fs.existsSync(c.file))
|
|
1721
2052
|
.map((c) => ({ ...c, score: inputCandidateScore(c.file) }));
|
|
1722
2053
|
if (!candidates.length) return null;
|
|
1723
|
-
candidates.sort((a, b) =>
|
|
1724
|
-
(b.score.facts - a.score.facts) ||
|
|
1725
|
-
(b.score.size - a.score.size) ||
|
|
1726
|
-
(a.order - b.order)
|
|
1727
|
-
);
|
|
2054
|
+
candidates.sort((a, b) => b.score.facts - a.score.facts || b.score.size - a.score.size || a.order - b.order);
|
|
1728
2055
|
return candidates[0].base;
|
|
1729
2056
|
}
|
|
2057
|
+
// generate writes the checked-in artefacts and immediately executes the new
|
|
2058
|
+
// example with --write so examples/output and examples/doc remain in sync.
|
|
1730
2059
|
function generate(n3Path, options = {}) {
|
|
1731
2060
|
const compiled = compile(n3Path, options);
|
|
1732
2061
|
const jsFile = path.join(EXAMPLES_DIR, `${compiled.name}.js`);
|
|
@@ -1738,7 +2067,8 @@ function generate(n3Path, options = {}) {
|
|
|
1738
2067
|
if (!options.force) {
|
|
1739
2068
|
const protectedInputs = externalInputName ? [] : [inputTrigFile];
|
|
1740
2069
|
for (const file of [outputFile, docFile, ...protectedInputs]) {
|
|
1741
|
-
if (fs.existsSync(file))
|
|
2070
|
+
if (fs.existsSync(file))
|
|
2071
|
+
throw new Error(`${path.relative(ROOT, file)} already exists; pass --force to overwrite`);
|
|
1742
2072
|
}
|
|
1743
2073
|
}
|
|
1744
2074
|
writeText(jsFile, compiled.exampleJs, options.force);
|
|
@@ -1749,7 +2079,27 @@ function generate(n3Path, options = {}) {
|
|
|
1749
2079
|
return { ...compiled, files: { jsFile, inputTrigFile, outputFile, docFile }, output };
|
|
1750
2080
|
}
|
|
1751
2081
|
|
|
1752
|
-
|
|
2082
|
+
// render is the non-mutating companion to generate. It compiles into a small
|
|
2083
|
+
// temporary /see-shaped tree, runs the generated example, and returns Markdown.
|
|
2084
|
+
function render(n3Path) {
|
|
2085
|
+
const tmpName = `_see_tmp_${process.pid}`;
|
|
2086
|
+
const compiled = compile(n3Path, { name: tmpName });
|
|
2087
|
+
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'see-compile-'));
|
|
2088
|
+
const tmpSeeDir = path.join(tmpDir, 'see');
|
|
2089
|
+
const examplesDir = path.join(tmpSeeDir, 'examples');
|
|
2090
|
+
ensureDir(path.join(examplesDir, 'input'));
|
|
2091
|
+
fs.copyFileSync(path.join(EXAMPLES_DIR, '_see.js'), path.join(examplesDir, '_see.js'));
|
|
2092
|
+
fs.copyFileSync(path.join(ROOT, 'see.js'), path.join(tmpSeeDir, 'see.js'));
|
|
2093
|
+
const jsFile = path.join(examplesDir, `${tmpName}.js`);
|
|
2094
|
+
const trigFile = path.join(examplesDir, 'input', `${tmpName}.trig`);
|
|
2095
|
+
fs.writeFileSync(jsFile, compiled.exampleJs, 'utf8');
|
|
2096
|
+
fs.writeFileSync(trigFile, compiled.inputTrig, 'utf8');
|
|
2097
|
+
try {
|
|
2098
|
+
return runNode(jsFile, tmpSeeDir);
|
|
2099
|
+
} finally {
|
|
2100
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
1753
2103
|
|
|
1754
2104
|
function parseArgs(argv) {
|
|
1755
2105
|
const args = [...argv];
|
|
@@ -1767,7 +2117,10 @@ function parseArgs(argv) {
|
|
|
1767
2117
|
|
|
1768
2118
|
function main() {
|
|
1769
2119
|
const { command, file, opts } = parseArgs(process.argv.slice(2));
|
|
1770
|
-
if (!command || command === 'help' || command === '--help') {
|
|
2120
|
+
if (!command || command === 'help' || command === '--help') {
|
|
2121
|
+
console.log(usage());
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
1771
2124
|
if (!file) throw new Error(`Missing <example.n3>\n\n${usage()}`);
|
|
1772
2125
|
if (command === 'generate') {
|
|
1773
2126
|
const result = generate(file, opts);
|
|
@@ -1775,20 +2128,28 @@ function main() {
|
|
|
1775
2128
|
if (result.files.inputTrigFile) console.log(`generated ${path.relative(ROOT, result.files.inputTrigFile)}`);
|
|
1776
2129
|
console.log(`generated ${path.relative(ROOT, result.files.outputFile)}`);
|
|
1777
2130
|
console.log(`generated ${path.relative(ROOT, result.files.docFile)}`);
|
|
1778
|
-
console.log(
|
|
2131
|
+
console.log(
|
|
2132
|
+
`compiled ${result.stats.facts} facts, ${result.stats.rules} forward rules, ${result.stats.backwardRules} backward rules, ${result.stats.fuses} fuses, ${result.stats.queries} queries`,
|
|
2133
|
+
);
|
|
1779
2134
|
} else if (command === 'render') {
|
|
1780
2135
|
process.stdout.write(render(file));
|
|
1781
2136
|
} else if (command === 'inspect') {
|
|
1782
2137
|
const result = compile(file, opts);
|
|
1783
|
-
console.log(
|
|
2138
|
+
console.log(
|
|
2139
|
+
`OK ${result.name}: ${result.stats.facts} facts, ${result.stats.rules} forward rules, ${result.stats.backwardRules} backward rules, ${result.stats.fuses} fuses, ${result.stats.queries} queries`,
|
|
2140
|
+
);
|
|
1784
2141
|
} else {
|
|
1785
2142
|
throw new Error(`Unknown command: ${command}\n\n${usage()}`);
|
|
1786
2143
|
}
|
|
1787
2144
|
}
|
|
1788
2145
|
|
|
1789
2146
|
if (require.main === module) {
|
|
1790
|
-
try {
|
|
1791
|
-
|
|
2147
|
+
try {
|
|
2148
|
+
main();
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
console.error(err.stack || err.message);
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
1792
2153
|
}
|
|
1793
2154
|
|
|
1794
2155
|
module.exports = { compile, generate, parseN3, render, tokenize };
|