@josd/eyecode 0.0.0-stage → 0.1.3
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/LICENSE.md +9 -0
- package/README.md +36 -2
- package/SPEC.md +921 -0
- package/bin/eyecode.js +28 -0
- package/package.json +43 -4
- package/src/arith.js +432 -0
- package/src/builtins.js +185 -0
- package/src/cli.js +126 -0
- package/src/common.js +120 -0
- package/src/engine.js +867 -0
- package/src/goals.js +44 -0
- package/src/index.js +15 -0
- package/src/lang.js +476 -0
- package/src/node.js +21 -0
- package/src/program.js +278 -0
- package/src/proof.js +679 -0
- package/src/reader.js +396 -0
- package/src/terms.js +502 -0
- package/src/version.js +2 -0
- package/src/writer.js +261 -0
package/src/goals.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Goals as a program, a command line or a checked proof states them.
|
|
2
|
+
//
|
|
3
|
+
// An array in a goal position is the conjunction of its items: [p(X), q(X)]
|
|
4
|
+
// is and(p(X), q(X)). A goal position is a statement's goal, an argument of
|
|
5
|
+
// and, or, not, call or once, or the goal of findall. Anywhere else an array
|
|
6
|
+
// is a list.
|
|
7
|
+
import { EMPTY, Struct, conjunction, properListItems } from './terms.js';
|
|
8
|
+
import { readTerm } from './reader.js';
|
|
9
|
+
|
|
10
|
+
const EMPTY_ENV = { bindings: new Map() };
|
|
11
|
+
|
|
12
|
+
function isList(term) {
|
|
13
|
+
return term === EMPTY || (term instanceof Struct && term.name === '.' && term.args.length === 2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeGoal(term) {
|
|
17
|
+
if (isList(term)) {
|
|
18
|
+
const items = properListItems(term, EMPTY_ENV);
|
|
19
|
+
// An empty or open list stays a list; a goal position rejects it later.
|
|
20
|
+
if (items === null || !items.length) return term;
|
|
21
|
+
return conjunction(items.map(normalizeGoal));
|
|
22
|
+
}
|
|
23
|
+
if (!(term instanceof Struct)) return term;
|
|
24
|
+
const { name, args } = term;
|
|
25
|
+
if ((name === 'and' || name === 'or') && args.length === 2) {
|
|
26
|
+
return rebuild(term, args.map(normalizeGoal));
|
|
27
|
+
}
|
|
28
|
+
if ((name === 'not' || name === 'call' || name === 'once') && args.length === 1) {
|
|
29
|
+
return rebuild(term, [normalizeGoal(args[0])]);
|
|
30
|
+
}
|
|
31
|
+
if (name === 'findall' && args.length === 3) {
|
|
32
|
+
return rebuild(term, [args[0], normalizeGoal(args[1]), args[2]]);
|
|
33
|
+
}
|
|
34
|
+
return term;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rebuild(term, args) {
|
|
38
|
+
return args.every((arg, i) => arg === term.args[i]) ? term : new Struct(term.name, args);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A goal given as text, such as on the command line: [p(X), q(X)] asks both.
|
|
42
|
+
export function readGoal(text) {
|
|
43
|
+
return normalizeGoal(readTerm(text));
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// eyecode: reasoning you can see, in JavaScript.
|
|
2
|
+
//
|
|
3
|
+
// Facts and rules are JavaScript. Forward rules materialize conclusions until
|
|
4
|
+
// a fixpoint, backward rules are decided when a goal asks for them, and every
|
|
5
|
+
// answer can come with a proof that a separate checker verifies against the
|
|
6
|
+
// program, condition by condition (C1-C7).
|
|
7
|
+
export { VERSION } from './version.js';
|
|
8
|
+
export { Result, run, unusedClauses } from './engine.js';
|
|
9
|
+
export { readGoal } from './goals.js';
|
|
10
|
+
export { FloatLiteral, build, loadSources, loadText, toTerm } from './lang.js';
|
|
11
|
+
export { Program } from './program.js';
|
|
12
|
+
export { checkProof, checkReport, publicReport, strict, verdictText } from './proof.js';
|
|
13
|
+
export { readTerm, readTerms } from './reader.js';
|
|
14
|
+
export { EyecodeError, Struct, Var } from './terms.js';
|
|
15
|
+
export { write } from './writer.js';
|
package/src/lang.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// Writing programs in JavaScript.
|
|
2
|
+
//
|
|
3
|
+
// A program is a JavaScript script that states facts and rules:
|
|
4
|
+
//
|
|
5
|
+
// fact(type('socrates', 'human'))
|
|
6
|
+
// fact(subclass_of('human', 'mortal'))
|
|
7
|
+
// implies([type(S, A), subclass_of(A, B)], type(S, B))
|
|
8
|
+
// query(type(X, Y))
|
|
9
|
+
//
|
|
10
|
+
// Atoms are strings, integers are numbers with an integral value or bigints,
|
|
11
|
+
// floats are other numbers, lists are arrays, and [H, ...T] is a list with
|
|
12
|
+
// head H and tail T. An array in a goal position is the conjunction of its
|
|
13
|
+
// goals. Each call states one clause, in source order; loadText runs the
|
|
14
|
+
// script and collects them into a Program.
|
|
15
|
+
//
|
|
16
|
+
// A name the program does not define is supplied as it runs: a name
|
|
17
|
+
// beginning with an uppercase letter or _ is the variable of that name, and
|
|
18
|
+
// any other name a predicate, which builds a compound when called and stands
|
|
19
|
+
// for the atom of its name when not.
|
|
20
|
+
import { PRIMITIVE_KEYS } from './builtins.js';
|
|
21
|
+
import { run } from './engine.js';
|
|
22
|
+
import { normalizeGoal } from './goals.js';
|
|
23
|
+
import { CONTROL_KEYS, Program, RESERVED_KEYS, Source } from './program.js';
|
|
24
|
+
import { checkProof, checkReport, verdictText } from './proof.js';
|
|
25
|
+
import { readTerm, readTerms } from './reader.js';
|
|
26
|
+
import { EMPTY, EyecodeError, Struct, Tail, Var, conjunction, keyName } from './terms.js';
|
|
27
|
+
import { RESERVED_CALLS, RESERVED_WORDS, write } from './writer.js';
|
|
28
|
+
|
|
29
|
+
const PREDICATE = Symbol('eyecode.predicate');
|
|
30
|
+
|
|
31
|
+
// A float written float(N) in a program, kept apart from the integer N.
|
|
32
|
+
class FloatLiteral {
|
|
33
|
+
constructor(value) {
|
|
34
|
+
this.value = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function refuseOperators() {
|
|
39
|
+
throw new EyecodeError('operators do not apply to terms: write add(X, 1) rather than X + 1, gt(X, 1) ' +
|
|
40
|
+
'rather than X > 1, and an array [g1, g2] rather than g1 && g2');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A term has no value as a JavaScript primitive, so an operator such as + or
|
|
44
|
+
// > applied to one is an error rather than a string or false.
|
|
45
|
+
for (const kind of [Var, Struct]) {
|
|
46
|
+
Object.defineProperty(kind.prototype, Symbol.toPrimitive, { value: refuseOperators });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Turn a JavaScript value written in a program into a term.
|
|
50
|
+
export function toTerm(value) {
|
|
51
|
+
const type = typeof value;
|
|
52
|
+
if (type === 'string' || type === 'bigint' || value instanceof Var || value instanceof Struct) return value;
|
|
53
|
+
if (type === 'number') {
|
|
54
|
+
if (!Number.isFinite(value)) throw new EyecodeError(`a float term must be finite, not ${value}`);
|
|
55
|
+
return Number.isInteger(value) ? BigInt(value) : value;
|
|
56
|
+
}
|
|
57
|
+
if (value instanceof FloatLiteral) return value.value;
|
|
58
|
+
if (Array.isArray(value)) return fromArray(value);
|
|
59
|
+
if (type === 'function' && value[PREDICATE] !== undefined) return value[PREDICATE];
|
|
60
|
+
if (type === 'boolean') {
|
|
61
|
+
// A comparison of two plain values, such as 1 < 2 or N > 1, is decided by
|
|
62
|
+
// JavaScript before eyecode sees it.
|
|
63
|
+
throw new EyecodeError(`${value} is not a term: a comparison of plain JavaScript values is decided by ` +
|
|
64
|
+
`JavaScript before eyecode sees it; write gt(X, Y) and the like, or an atom such as 'true'`);
|
|
65
|
+
}
|
|
66
|
+
if (value === null || value === undefined) {
|
|
67
|
+
throw new EyecodeError(`${value} is not a term; write an atom such as 'true' instead`);
|
|
68
|
+
}
|
|
69
|
+
throw new EyecodeError(`a value of type ${value?.constructor?.name ?? type} is not a term`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fromArray(items) {
|
|
73
|
+
let tail = EMPTY;
|
|
74
|
+
let count = items.length;
|
|
75
|
+
if (count && items[count - 1] instanceof Tail) {
|
|
76
|
+
tail = toTerm(items[count - 1].term);
|
|
77
|
+
count--;
|
|
78
|
+
}
|
|
79
|
+
let result = tail;
|
|
80
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
81
|
+
if (items[i] instanceof Tail) throw new EyecodeError('a spread tail can only end a list');
|
|
82
|
+
result = new Struct('.', [toTerm(items[i]), result]);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const goalOf = (value) => normalizeGoal(toTerm(value));
|
|
88
|
+
|
|
89
|
+
// The goals a statement or control joins: one goal, or several as and(...).
|
|
90
|
+
function joined(values) {
|
|
91
|
+
return conjunction(values.map(goalOf));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Names for the _ of one clause: _0, _1, ..., skipping names the clause
|
|
95
|
+
// already uses, which are looked up only once a _ is met.
|
|
96
|
+
class Anonymous {
|
|
97
|
+
constructor(terms) {
|
|
98
|
+
this.terms = terms;
|
|
99
|
+
this.used = null;
|
|
100
|
+
this.count = 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
next() {
|
|
104
|
+
if (this.used === null) {
|
|
105
|
+
this.used = new Set();
|
|
106
|
+
const pending = [...this.terms];
|
|
107
|
+
while (pending.length) {
|
|
108
|
+
const term = pending.pop();
|
|
109
|
+
if (term instanceof Var) this.used.add(term.name);
|
|
110
|
+
else if (term instanceof Struct) pending.push(...term.args);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
while (true) {
|
|
114
|
+
const name = `_${this.count}`;
|
|
115
|
+
this.count++;
|
|
116
|
+
if (!this.used.has(name)) return name;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Every _ in a clause is a variable of its own. A term without one is
|
|
122
|
+
// returned as it is.
|
|
123
|
+
function renameAnonymous(term, counter) {
|
|
124
|
+
if (term instanceof Var) return term.name === '_' ? new Var(counter.next()) : term;
|
|
125
|
+
if (!(term instanceof Struct)) return term;
|
|
126
|
+
let args = null;
|
|
127
|
+
term.args.forEach((arg, index) => {
|
|
128
|
+
if (arg instanceof Struct || (arg instanceof Var && arg.name === '_')) {
|
|
129
|
+
const renamed = renameAnonymous(arg, counter);
|
|
130
|
+
if (renamed !== arg) {
|
|
131
|
+
if (args === null) args = [...term.args];
|
|
132
|
+
args[index] = renamed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return args === null ? term : new Struct(term.name, args);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Collects the clauses a program states, in order.
|
|
140
|
+
class Builder {
|
|
141
|
+
constructor(locate) {
|
|
142
|
+
this.sources = [];
|
|
143
|
+
this.locate = locate;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
add(kind, head, body, line = null) {
|
|
147
|
+
if (line === null) line = this.locate();
|
|
148
|
+
const counter = new Anonymous([head, ...body]);
|
|
149
|
+
// In the order the statement writes them: a forward rule, implies(),
|
|
150
|
+
// writes its premise first.
|
|
151
|
+
if (kind !== 'backward') {
|
|
152
|
+
body = body.map((goal) => renameAnonymous(goal, counter));
|
|
153
|
+
head = renameAnonymous(head, counter);
|
|
154
|
+
} else {
|
|
155
|
+
head = renameAnonymous(head, counter);
|
|
156
|
+
body = body.map((goal) => renameAnonymous(goal, counter));
|
|
157
|
+
}
|
|
158
|
+
for (const goal of body) {
|
|
159
|
+
if (!(goal instanceof Var) && typeof goal !== 'string' && !(goal instanceof Struct)) {
|
|
160
|
+
throw new EyecodeError('a goal must be an atom, a compound term or a variable, not a number');
|
|
161
|
+
}
|
|
162
|
+
if (goal instanceof Struct && goal.name === '.' && goal.args.length === 2) {
|
|
163
|
+
throw new EyecodeError('an open list is not a goal');
|
|
164
|
+
}
|
|
165
|
+
if (goal === EMPTY) throw new EyecodeError('an empty array is not a goal');
|
|
166
|
+
}
|
|
167
|
+
this.sources.push(new Source(kind, head, body, line));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const NAME_LIST = /[\s,]+/;
|
|
172
|
+
|
|
173
|
+
function names(spec) {
|
|
174
|
+
return String(spec).split(NAME_LIST).filter(Boolean);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const PLAIN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
178
|
+
const RESERVED_NAMES = new Set([...PRIMITIVE_KEYS, ...CONTROL_KEYS, ...RESERVED_KEYS].map(keyName));
|
|
179
|
+
for (const name of RESERVED_CALLS) RESERVED_NAMES.add(name);
|
|
180
|
+
|
|
181
|
+
function predicate(name) {
|
|
182
|
+
const make = (...args) => {
|
|
183
|
+
if (!args.length) return name;
|
|
184
|
+
// float(N) on a number is the float N, which an integral number could
|
|
185
|
+
// not otherwise write.
|
|
186
|
+
if (name === 'float' && args.length === 1 && typeof args[0] === 'number' && Number.isFinite(args[0])) {
|
|
187
|
+
return new FloatLiteral(args[0]);
|
|
188
|
+
}
|
|
189
|
+
return new Struct(name, args.map(toTerm));
|
|
190
|
+
};
|
|
191
|
+
Object.defineProperty(make, PREDICATE, { value: name });
|
|
192
|
+
Object.defineProperty(make, Symbol.toPrimitive, { value: refuseOperators });
|
|
193
|
+
return make;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function control(name, arity) {
|
|
197
|
+
return (...args) => {
|
|
198
|
+
if (args.length !== arity) throw new EyecodeError(`${name} takes ${arity} arguments, got ${args.length}`);
|
|
199
|
+
return new Struct(name, args.map(toTerm));
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const TRUE_ATOM = 'true';
|
|
204
|
+
|
|
205
|
+
// The programs being built, innermost last: a statement states its clause
|
|
206
|
+
// into the innermost, so a program can build another with eyecode.build().
|
|
207
|
+
const builders = [];
|
|
208
|
+
|
|
209
|
+
function active() {
|
|
210
|
+
if (!builders.length) throw new EyecodeError('a statement states a clause only while a program is built');
|
|
211
|
+
return builders[builders.length - 1];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Build a program with builder innermost while statements() runs.
|
|
215
|
+
function within(builder, statements) {
|
|
216
|
+
builders.push(builder);
|
|
217
|
+
try {
|
|
218
|
+
statements();
|
|
219
|
+
} finally {
|
|
220
|
+
builders.pop();
|
|
221
|
+
}
|
|
222
|
+
return builder;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// The library, as a program reaches it: eyecode.run(eyecode.build(rules)), and
|
|
226
|
+
// the readers and writers of documents, so a program can reason over the
|
|
227
|
+
// proofs and reports of another in the same language.
|
|
228
|
+
const LIBRARY = Object.freeze({
|
|
229
|
+
run,
|
|
230
|
+
build: (statements) => build(statements),
|
|
231
|
+
checkProof,
|
|
232
|
+
checkReport,
|
|
233
|
+
verdictText,
|
|
234
|
+
readTerm,
|
|
235
|
+
readTerms,
|
|
236
|
+
write,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// The names a program is given: the statements and the terms it builds goals
|
|
240
|
+
// with. The statements report to the innermost program being built.
|
|
241
|
+
function exportsFor(io) {
|
|
242
|
+
const builder = { add: (...args) => active().add(...args), locate: () => active().locate() };
|
|
243
|
+
const statements = {
|
|
244
|
+
fact: (...terms) => {
|
|
245
|
+
for (const term of terms) builder.add('fact', toTerm(term), []);
|
|
246
|
+
},
|
|
247
|
+
implies: (premise, conclusion) => builder.add('forward', goalOf(conclusion), [goalOf(premise)]),
|
|
248
|
+
implied_by: (conclusion, premise) => builder.add('backward', toTerm(conclusion), [goalOf(premise)]),
|
|
249
|
+
query: (...body) => builder.add('query', TRUE_ATOM, body.map(goalOf)),
|
|
250
|
+
contradiction: (...body) => builder.add('contradiction', 'false', body.map(goalOf)),
|
|
251
|
+
facts_from: (source) => {
|
|
252
|
+
const line = builder.locate();
|
|
253
|
+
let documentText;
|
|
254
|
+
let label;
|
|
255
|
+
if (source !== null && typeof source === 'object' && typeof source.text === 'string') {
|
|
256
|
+
documentText = source.text;
|
|
257
|
+
label = 'the text';
|
|
258
|
+
} else if (typeof source === 'string') {
|
|
259
|
+
if (!io || !io.readFile) throw new EyecodeError('facts_from(path) needs a file system; pass {text: ...} instead');
|
|
260
|
+
documentText = io.readFile(source);
|
|
261
|
+
label = source;
|
|
262
|
+
} else {
|
|
263
|
+
throw new EyecodeError('facts_from needs a path or {text: ...}');
|
|
264
|
+
}
|
|
265
|
+
let terms;
|
|
266
|
+
try {
|
|
267
|
+
terms = readTerms(documentText);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// The document's own line, named so it is not taken for the program's.
|
|
270
|
+
if (error instanceof EyecodeError) throw new EyecodeError(`${label}: ${error.message}`);
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
// Each fact is stated by this call, so it records the call's line, as
|
|
274
|
+
// every clause records the line of the statement that states it.
|
|
275
|
+
for (const [term] of terms) builder.add('fact', term, [], line);
|
|
276
|
+
},
|
|
277
|
+
struct: (name, ...args) => {
|
|
278
|
+
if (typeof name !== 'string') throw new EyecodeError('struct() needs a name string first');
|
|
279
|
+
return args.length ? new Struct(name, args.map(toTerm)) : name;
|
|
280
|
+
},
|
|
281
|
+
preds: (spec) => {
|
|
282
|
+
const made = names(spec).map((name) => {
|
|
283
|
+
if (!PLAIN.test(name) || RESERVED_WORDS.has(name)) throw new EyecodeError(`'${name}' is not a valid predicate name`);
|
|
284
|
+
if (RESERVED_NAMES.has(name)) throw new EyecodeError(`'${name}' is a reserved name`);
|
|
285
|
+
return predicate(name);
|
|
286
|
+
});
|
|
287
|
+
return made.length === 1 ? made[0] : made;
|
|
288
|
+
},
|
|
289
|
+
vars: (spec) => {
|
|
290
|
+
const made = names(spec).map((name) => {
|
|
291
|
+
if (!PLAIN.test(name) || RESERVED_WORDS.has(name)) throw new EyecodeError(`'${name}' is not a valid variable name`);
|
|
292
|
+
return new Var(name);
|
|
293
|
+
});
|
|
294
|
+
return made.length === 1 ? made[0] : made;
|
|
295
|
+
},
|
|
296
|
+
// The controls. An array or several goals are their conjunction.
|
|
297
|
+
and: (...goals) => joined(goals),
|
|
298
|
+
or: (...goals) => {
|
|
299
|
+
if (!goals.length) return 'fail';
|
|
300
|
+
let result = goalOf(goals[0]);
|
|
301
|
+
for (const goal of goals.slice(1)) result = new Struct('or', [result, goalOf(goal)]);
|
|
302
|
+
return result;
|
|
303
|
+
},
|
|
304
|
+
not: (...goals) => new Struct('not', [joined(goals)]),
|
|
305
|
+
call: (...goals) => new Struct('call', [joined(goals)]),
|
|
306
|
+
once: (...goals) => new Struct('once', [joined(goals)]),
|
|
307
|
+
findall: (template, goal, result) => new Struct('findall', [toTerm(template), goalOf(goal), toTerm(result)]),
|
|
308
|
+
fail: 'fail',
|
|
309
|
+
eyecode: LIBRARY,
|
|
310
|
+
};
|
|
311
|
+
for (const primitiveKey of PRIMITIVE_KEYS) {
|
|
312
|
+
const name = keyName(primitiveKey);
|
|
313
|
+
const arity = Number(primitiveKey.slice(name.length + 1));
|
|
314
|
+
if (arity > 0) statements[name] = control(name, arity);
|
|
315
|
+
}
|
|
316
|
+
return statements;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// JavaScript globals a program keeps, to compute its clauses with. Any other
|
|
320
|
+
// name it does not define is a variable or a predicate: Date, Map or Object
|
|
321
|
+
// are variables, as logic programs name them; globalThis.Date still reaches
|
|
322
|
+
// the global.
|
|
323
|
+
const KEPT = new Set([
|
|
324
|
+
'Math', 'JSON', 'BigInt', 'Infinity', 'NaN', 'undefined', 'console', 'globalThis', 'parseInt',
|
|
325
|
+
'parseFloat', 'isNaN', 'isFinite',
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
// The scope a program runs in: every name it does not define resolves here.
|
|
329
|
+
function programScope(exported) {
|
|
330
|
+
const locals = new Map();
|
|
331
|
+
const predicates = new Map();
|
|
332
|
+
return new Proxy(Object.create(null), {
|
|
333
|
+
has(target, name) {
|
|
334
|
+
return typeof name === 'string' && !KEPT.has(name);
|
|
335
|
+
},
|
|
336
|
+
get(target, name) {
|
|
337
|
+
if (typeof name !== 'string') return undefined;
|
|
338
|
+
if (locals.has(name)) return locals.get(name);
|
|
339
|
+
if (Object.hasOwn(exported, name)) return exported[name];
|
|
340
|
+
if (/^[A-Z_]/.test(name)) return new Var(name);
|
|
341
|
+
let made = predicates.get(name);
|
|
342
|
+
if (made === undefined) {
|
|
343
|
+
made = predicate(name);
|
|
344
|
+
predicates.set(name, made);
|
|
345
|
+
}
|
|
346
|
+
return made;
|
|
347
|
+
},
|
|
348
|
+
set(target, name, value) {
|
|
349
|
+
// var declarations and plain assignments of a sloppy script land here.
|
|
350
|
+
locals.set(name, value);
|
|
351
|
+
return true;
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Where the statement being run is: the line of the innermost program frame
|
|
357
|
+
// on the stack.
|
|
358
|
+
let serial = 0;
|
|
359
|
+
const FRAME = /eyecode-program-(\d+)\.js:(\d+):\d+/;
|
|
360
|
+
|
|
361
|
+
function frameLines(stack, id) {
|
|
362
|
+
const lines = [];
|
|
363
|
+
for (const line of String(stack).split('\n')) {
|
|
364
|
+
const match = FRAME.exec(line);
|
|
365
|
+
if (match && Number(match[1]) === id) lines.push(Number(match[2]));
|
|
366
|
+
}
|
|
367
|
+
return lines;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// The line a program's first line is reported as, found once by compiling a
|
|
371
|
+
// probe the same way programs are compiled.
|
|
372
|
+
let firstLine = null;
|
|
373
|
+
function lineOffset() {
|
|
374
|
+
if (firstLine === null) {
|
|
375
|
+
const probe = compile('return new Error().stack', 'eyecode-program-0.js');
|
|
376
|
+
const lines = frameLines(probe({}), 0);
|
|
377
|
+
firstLine = lines.length ? lines[0] : 3;
|
|
378
|
+
}
|
|
379
|
+
return firstLine;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function compile(source, url) {
|
|
383
|
+
// eslint-disable-next-line no-new-func
|
|
384
|
+
return new Function('__eyecode_scope__', `with (__eyecode_scope__) {\n${source}\n}\n//# sourceURL=${url}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const PROOF_LINE = /^step\(/m;
|
|
388
|
+
const LEADING_CALL = /^([A-Za-z_$][\w$]*)[ \t]*\(/gm;
|
|
389
|
+
const DECLARED = /\b(?:function\s*\*?\s*|class\s+|(?:const|let|var)\s+)([A-Za-z_$][\w$]*)|^[ \t]*([A-Za-z_$][\w$]*)[ \t]*=(?!=)/gm;
|
|
390
|
+
// Statements of earlier versions, and what states the same clause now.
|
|
391
|
+
const RENAMED = { forward: 'implies(Premise, Conclusion), premise first', backward: 'implied_by(Conclusion, Premise)' };
|
|
392
|
+
|
|
393
|
+
// A top-level call of an implicit predicate states nothing: a misspelled fact?
|
|
394
|
+
function checkStatements(source, exported) {
|
|
395
|
+
const declared = new Set();
|
|
396
|
+
for (const match of source.matchAll(DECLARED)) declared.add(match[1] || match[2]);
|
|
397
|
+
for (const match of source.matchAll(LEADING_CALL)) {
|
|
398
|
+
const name = match[1];
|
|
399
|
+
if (Object.hasOwn(exported, name) || KEPT.has(name) || declared.has(name) || /^[A-Z_]/.test(name)) continue;
|
|
400
|
+
if (RESERVED_WORDS.has(name)) continue;
|
|
401
|
+
const line = source.slice(0, match.index).split('\n').length;
|
|
402
|
+
if (Object.hasOwn(RENAMED, name)) throw new EyecodeError(`line ${line}: ${name}(...) is now ${RENAMED[name]}`);
|
|
403
|
+
throw new EyecodeError(`line ${line}: ${name}(...) on its own states nothing; ` +
|
|
404
|
+
`write fact(${name}(...)) to state it, or check the spelling`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// The program line an error came from, unless it already names one.
|
|
409
|
+
function withLine(error, stack, id) {
|
|
410
|
+
const lines = frameLines(stack, id).map((line) => line - lineOffset() + 1);
|
|
411
|
+
const where = lines.length ? `line ${lines[0]}: ` : '';
|
|
412
|
+
if (error instanceof EyecodeError) {
|
|
413
|
+
if (error.message.startsWith('line ') || !where) return error;
|
|
414
|
+
return new EyecodeError(`${where}${error.message}`);
|
|
415
|
+
}
|
|
416
|
+
return new EyecodeError(`${where}${error.name || 'Error'}: ${error.message}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Run one program's source, stating its clauses into builder.
|
|
420
|
+
function execute(source, filename, builder, io) {
|
|
421
|
+
if (PROOF_LINE.test(source)) {
|
|
422
|
+
throw new EyecodeError('this is a proof document, not a program: check it with --check-proof PROOF PROGRAM');
|
|
423
|
+
}
|
|
424
|
+
if (io && io.checkSyntax) io.checkSyntax(source, filename);
|
|
425
|
+
const exported = exportsFor(io);
|
|
426
|
+
checkStatements(source, exported);
|
|
427
|
+
serial++;
|
|
428
|
+
const id = serial;
|
|
429
|
+
const url = `eyecode-program-${id}.js`;
|
|
430
|
+
let fn;
|
|
431
|
+
try {
|
|
432
|
+
fn = compile(source, url);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
throw new EyecodeError(`${filename}: ${error.name}: ${error.message}`);
|
|
435
|
+
}
|
|
436
|
+
builder.locate = () => {
|
|
437
|
+
const lines = frameLines(new Error().stack, id);
|
|
438
|
+
return lines.length ? lines[0] - lineOffset() + 1 : null;
|
|
439
|
+
};
|
|
440
|
+
try {
|
|
441
|
+
within(builder, () => fn(programScope(exported)));
|
|
442
|
+
} catch (error) {
|
|
443
|
+
throw withLine(error, error && error.stack, id);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// A Program from JavaScript source text. io supplies readFile for
|
|
448
|
+
// facts_from(path) and, where the host can, checkSyntax for better syntax
|
|
449
|
+
// error messages.
|
|
450
|
+
export function loadText(source, filename = '<program>', io = null) {
|
|
451
|
+
const builder = new Builder(() => null);
|
|
452
|
+
execute(String(source).replace(/^#![^\n]*/, ''), filename, builder, io);
|
|
453
|
+
return new Program(builder.sources);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// One Program from several sources, given as [text, filename] pairs, run in
|
|
457
|
+
// order.
|
|
458
|
+
export function loadSources(sources, io = null) {
|
|
459
|
+
const builder = new Builder(() => null);
|
|
460
|
+
for (const [source, filename] of sources) {
|
|
461
|
+
execute(String(source).replace(/^#![^\n]*/, ''), filename, builder, io);
|
|
462
|
+
}
|
|
463
|
+
return new Program(builder.sources);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// A Program from a function that states clauses when called: a function of a
|
|
467
|
+
// program, which states them with the names the program uses, or one of
|
|
468
|
+
// library code, which is handed those names.
|
|
469
|
+
export function build(statements) {
|
|
470
|
+
const outer = builders.length ? builders[builders.length - 1] : null;
|
|
471
|
+
const builder = new Builder(outer ? outer.locate : () => null);
|
|
472
|
+
within(builder, () => statements(exportsFor(null)));
|
|
473
|
+
return new Program(builder.sources);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export { FloatLiteral, PREDICATE };
|
package/src/node.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// What the program loader and the command line need from Node: files, and
|
|
2
|
+
// syntax errors with their line.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import vm from 'node:vm';
|
|
5
|
+
import { EyecodeError } from './terms.js';
|
|
6
|
+
|
|
7
|
+
export const nodeIO = {
|
|
8
|
+
readFile: (path) => readFileSync(path, 'utf8'),
|
|
9
|
+
checkSyntax(source, filename) {
|
|
10
|
+
try {
|
|
11
|
+
// Parsed only, never run: a script with the program's source.
|
|
12
|
+
new vm.Script(source, { filename }); // eslint-disable-line no-new
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
15
|
+
const match = String(error.stack).match(/:(\d+)\n/);
|
|
16
|
+
const where = match ? `line ${match[1]}: ` : '';
|
|
17
|
+
throw new EyecodeError(`${where}${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|