@josd/eyecode 0.0.0-stage → 0.1.4

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/bin/eyecode.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // The eyecode command: reason over JavaScript rule programs.
3
+ import { readFileSync, writeSync } from 'node:fs';
4
+ import { main } from '../src/cli.js';
5
+ import { nodeIO } from '../src/node.js';
6
+
7
+ function write(fd, text) {
8
+ // Written synchronously, so a large proof is complete before the exit.
9
+ const buffer = Buffer.from(text, 'utf8');
10
+ let offset = 0;
11
+ while (offset < buffer.length) {
12
+ try {
13
+ offset += writeSync(fd, buffer, offset);
14
+ } catch (error) {
15
+ if (error.code === 'EAGAIN') continue;
16
+ if (error.code === 'EPIPE') return;
17
+ throw error;
18
+ }
19
+ }
20
+ }
21
+
22
+ const code = main(process.argv.slice(2), {
23
+ ...nodeIO,
24
+ stdout: (text) => write(1, text),
25
+ stderr: (text) => write(2, text),
26
+ stdin: () => readFileSync(0, 'utf8'),
27
+ });
28
+ process.exitCode = code;
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@josd/eyecode",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.4",
4
+ "description": "A dependency-free JavaScript rule language with forward and backward reasoning and checkable proofs.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./node": "./src/node.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "bin": {
13
+ "eyecode": "bin/eyecode.js"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "src/",
18
+ "README.md",
19
+ "SPEC.md",
20
+ "LICENSE.md"
21
+ ],
22
+ "scripts": {
23
+ "test": "node test/run.js"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "keywords": [
29
+ "reasoner",
30
+ "rules",
31
+ "logic",
32
+ "proof",
33
+ "datalog"
34
+ ],
35
+ "author": "Jos De Roo",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/eyereasoner/eyecode.git"
40
+ },
41
+ "homepage": "https://eyereasoner.github.io/eyecode/",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
package/src/arith.js ADDED
@@ -0,0 +1,432 @@
1
+ // Arithmetic evaluation with exact integers and IEEE-754 floats.
2
+ //
3
+ // An expression term has the meaning the same operation has in Python, the
4
+ // reference for eyecode's arithmetic: integers are unbounded and exact, floats
5
+ // are doubles, div is true division, floordiv and mod are floor division and
6
+ // modulo, pow is exact on integers with a nonnegative exponent, and and, or,
7
+ // xor, not, shl and shr are bitwise on integers. A result that is not a finite
8
+ // real number is an error, never a value.
9
+ import { EyecodeError, Var, compareNumbers, deref } from './terms.js';
10
+
11
+ // A power or shift whose result would need more bits than this is refused
12
+ // rather than allowed to exhaust memory.
13
+ export const MAX_POWER_BITS = 1n << 26n;
14
+
15
+ class Problem extends Error {}
16
+
17
+ const isInt = (value) => typeof value === 'bigint';
18
+
19
+ function bitLength(n) {
20
+ if (n < 0n) n = -n;
21
+ return n === 0n ? 0 : n.toString(2).length;
22
+ }
23
+
24
+ // An integer as a float, correctly rounded; too large is an overflow.
25
+ function toFloat(value) {
26
+ if (!isInt(value)) return value;
27
+ const result = Number(value);
28
+ if (!Number.isFinite(result)) throw new Problem('overflow');
29
+ return result;
30
+ }
31
+
32
+ function needInts(name, values) {
33
+ for (const value of values) {
34
+ if (!isInt(value)) throw new Problem('needs integers');
35
+ }
36
+ }
37
+
38
+ // a / b of two integers, correctly rounded to the nearest float.
39
+ function intTrueDivide(a, b) {
40
+ if (b === 0n) throw new Problem('division by zero');
41
+ const negative = (a < 0n) !== (b < 0n);
42
+ if (a < 0n) a = -a;
43
+ if (b < 0n) b = -b;
44
+ if (a === 0n) return negative ? -0 : 0;
45
+ if (a <= 9007199254740992n && b <= 9007199254740992n) {
46
+ const result = Number(a) / Number(b);
47
+ return negative ? -result : result;
48
+ }
49
+ // Scale so the quotient has 55 to 56 significant bits, then round half to
50
+ // even on the exact remainder.
51
+ const shift = bitLength(b) - bitLength(a) + 55;
52
+ const numerator = shift > 0 ? a << BigInt(shift) : a;
53
+ const denominator = shift < 0 ? b << BigInt(-shift) : b;
54
+ let quotient = numerator / denominator;
55
+ const remainder = numerator % denominator;
56
+ let exponent = -shift;
57
+ // Keep 53 bits plus round and sticky information.
58
+ const extra = bitLength(quotient) - 53;
59
+ let sticky = remainder !== 0n;
60
+ if (extra > 0) {
61
+ const dropped = quotient & ((1n << BigInt(extra)) - 1n);
62
+ const half = 1n << BigInt(extra - 1);
63
+ quotient >>= BigInt(extra);
64
+ exponent += extra;
65
+ if (dropped > half || (dropped === half && (sticky || (quotient & 1n) === 1n))) quotient += 1n;
66
+ sticky = false;
67
+ }
68
+ if (exponent > 1023 + 64) throw new Problem('integer division result too large for a float');
69
+ let result = Number(quotient) * 2 ** exponent;
70
+ if (!Number.isFinite(result)) {
71
+ // Split the scaling so an exponent below the float range rounds once.
72
+ result = Number(quotient) * 2 ** Math.floor(exponent / 2) * 2 ** Math.ceil(exponent / 2);
73
+ }
74
+ if (!Number.isFinite(result)) throw new Problem('integer division result too large for a float');
75
+ return negative ? -result : result;
76
+ }
77
+
78
+ function floatDivmod(x, y) {
79
+ if (y === 0) throw new Problem('division by zero');
80
+ let mod = x % y;
81
+ let div = (x - mod) / y;
82
+ if (mod) {
83
+ if ((y < 0) !== (mod < 0)) {
84
+ mod += y;
85
+ div -= 1;
86
+ }
87
+ } else {
88
+ mod = Math.sign(y) < 0 || Object.is(y, -0) ? -0 : 0;
89
+ }
90
+ let floordiv;
91
+ if (div) {
92
+ floordiv = Math.floor(div);
93
+ if (div - floordiv > 0.5) floordiv += 1;
94
+ } else {
95
+ floordiv = (x / y < 0 || Object.is(x / y, -0)) ? -0 : 0;
96
+ }
97
+ return [floordiv, mod];
98
+ }
99
+
100
+ function intFloorDiv(a, b) {
101
+ if (b === 0n) throw new Problem('division by zero');
102
+ const q = a / b;
103
+ return (a % b !== 0n && (a < 0n) !== (b < 0n)) ? q - 1n : q;
104
+ }
105
+
106
+ function intMod(a, b) {
107
+ if (b === 0n) throw new Problem('division by zero');
108
+ const r = a % b;
109
+ return r !== 0n && (r < 0n) !== (b < 0n) ? r + b : r;
110
+ }
111
+
112
+ function add(a, b) {
113
+ return isInt(a) && isInt(b) ? a + b : toFloat(a) + toFloat(b);
114
+ }
115
+
116
+ function sub(a, b) {
117
+ return isInt(a) && isInt(b) ? a - b : toFloat(a) - toFloat(b);
118
+ }
119
+
120
+ function mul(a, b) {
121
+ return isInt(a) && isInt(b) ? a * b : toFloat(a) * toFloat(b);
122
+ }
123
+
124
+ function trueDivide(a, b) {
125
+ if (isInt(a) && isInt(b)) return intTrueDivide(a, b);
126
+ const y = toFloat(b);
127
+ if (y === 0) throw new Problem('division by zero');
128
+ return toFloat(a) / y;
129
+ }
130
+
131
+ function floorDivide(a, b) {
132
+ if (isInt(a) && isInt(b)) return intFloorDiv(a, b);
133
+ return floatDivmod(toFloat(a), toFloat(b))[0];
134
+ }
135
+
136
+ function modulo(a, b) {
137
+ if (isInt(a) && isInt(b)) return intMod(a, b);
138
+ return floatDivmod(toFloat(a), toFloat(b))[1];
139
+ }
140
+
141
+ function power(a, b) {
142
+ if (isInt(a) && isInt(b)) {
143
+ if (b >= 0n) {
144
+ const magnitude = a < 0n ? -a : a;
145
+ if (b > 0n && magnitude > 1n && b * BigInt(bitLength(a)) > MAX_POWER_BITS) {
146
+ throw new Problem(`resource: ${a} ** ${b} is too large`);
147
+ }
148
+ return a ** b;
149
+ }
150
+ if (a === 0n) throw new Problem('division by zero');
151
+ }
152
+ const x = toFloat(a);
153
+ const y = toFloat(b);
154
+ if (x === 0 && y < 0) throw new Problem('division by zero');
155
+ if (x < 0 && !Number.isInteger(y)) throw new Problem('has no real result');
156
+ const result = x ** y;
157
+ if (!Number.isFinite(result)) throw new Problem('overflow');
158
+ return result;
159
+ }
160
+
161
+ function shiftLeft(a, b) {
162
+ needInts('shl', [a, b]);
163
+ if (b < 0n) throw new Problem('negative shift count');
164
+ if (b > MAX_POWER_BITS) throw new Problem(`resource: ${a} << ${b} is too large`);
165
+ return a << b;
166
+ }
167
+
168
+ function shiftRight(a, b) {
169
+ needInts('shr', [a, b]);
170
+ if (b < 0n) throw new Problem('negative shift count');
171
+ return a >> b;
172
+ }
173
+
174
+ function bitwise(name, fn) {
175
+ return (a, b) => {
176
+ needInts(name, [a, b]);
177
+ return fn(a, b);
178
+ };
179
+ }
180
+
181
+ function minmax(name, pick) {
182
+ return (...values) => {
183
+ if (values.length < 2) throw new Problem(`${name} needs at least two arguments`);
184
+ let best = values[0];
185
+ for (const value of values.slice(1)) {
186
+ if (pick(compareNumbers(value, best))) best = value;
187
+ }
188
+ return best;
189
+ };
190
+ }
191
+
192
+ // Round half to even, exactly: a float is a dyadic rational, so x * 10**n
193
+ // is compared with the half-way point without rounding error.
194
+ function exactParts(x) {
195
+ // x = mantissa * 2 ** exponent, exactly.
196
+ const buffer = new DataView(new ArrayBuffer(8));
197
+ buffer.setFloat64(0, x);
198
+ const high = buffer.getUint32(0);
199
+ const low = buffer.getUint32(4);
200
+ const biased = (high >>> 20) & 0x7ff;
201
+ let mantissa = (BigInt(high & 0xfffff) << 32n) | BigInt(low);
202
+ let exponent;
203
+ if (biased === 0) {
204
+ exponent = -1074;
205
+ } else {
206
+ mantissa |= 1n << 52n;
207
+ exponent = biased - 1075;
208
+ }
209
+ if (x < 0) mantissa = -mantissa;
210
+ return [mantissa, exponent];
211
+ }
212
+
213
+ function roundHalfEven(numerator, denominator) {
214
+ // numerator / denominator rounded half to even; denominator > 0.
215
+ let q = numerator / denominator;
216
+ let r = numerator % denominator;
217
+ if (r < 0n) {
218
+ q -= 1n;
219
+ r += denominator;
220
+ }
221
+ const twice = r * 2n;
222
+ if (twice > denominator || (twice === denominator && (q & 1n) === 1n)) q += 1n;
223
+ return q;
224
+ }
225
+
226
+ function round(x, digits) {
227
+ if (digits === undefined) {
228
+ if (isInt(x)) return x;
229
+ const [mantissa, exponent] = exactParts(x);
230
+ if (exponent >= 0) return mantissa << BigInt(exponent);
231
+ return roundHalfEven(mantissa, 1n << BigInt(-exponent));
232
+ }
233
+ if (!isInt(digits)) throw new Problem('round needs an integer number of digits');
234
+ if (isInt(x)) {
235
+ if (digits >= 0n) return x;
236
+ const unit = 10n ** -digits;
237
+ return roundHalfEven(x, unit) * unit;
238
+ }
239
+ if (digits > 400n) return x;
240
+ if (digits < -400n) return x < 0 || Object.is(x, -0) ? -0 : 0;
241
+ const [mantissa, exponent] = exactParts(x);
242
+ let numerator = mantissa;
243
+ let denominator = 1n;
244
+ if (exponent >= 0) numerator <<= BigInt(exponent);
245
+ else denominator <<= BigInt(-exponent);
246
+ if (digits >= 0n) numerator *= 10n ** digits;
247
+ else denominator *= 10n ** -digits;
248
+ const rounded = roundHalfEven(numerator, denominator);
249
+ let result = digits >= 0n ? Number(`${rounded}e-${digits}`) : Number(`${rounded}e${-digits}`);
250
+ if (result === 0 && (x < 0 || Object.is(x, -0))) result = -0;
251
+ if (!Number.isFinite(result)) throw new Problem('rounded value too large');
252
+ return result;
253
+ }
254
+
255
+ function truncate(x) {
256
+ if (isInt(x)) return x;
257
+ return BigInt(Math.trunc(x));
258
+ }
259
+
260
+ function realFunction(fn, valid = () => true) {
261
+ return (...values) => {
262
+ const floats = values.map(toFloat);
263
+ if (!valid(...floats)) throw new Problem('math domain error');
264
+ const result = fn(...floats);
265
+ if (Number.isNaN(result)) throw new Problem('math domain error');
266
+ if (!Number.isFinite(result)) throw new Problem('overflow');
267
+ return result;
268
+ };
269
+ }
270
+
271
+ // A logarithm of a positive value of any size, with fn the host's logarithm
272
+ // in the base wanted. An integer too large for a float is split as m * 2**e
273
+ // with m in [0.5, 1), and its logarithm is fn(m) + fn(2) * e.
274
+ function logOf(value, fn) {
275
+ if (isInt(value)) {
276
+ if (value <= 0n) throw new Problem('math domain error');
277
+ const x = Number(value);
278
+ if (Number.isFinite(x)) return fn(x);
279
+ const bits = bitLength(value);
280
+ const mantissa = Number(value >> BigInt(bits - 64)) / 2 ** 64;
281
+ return fn(mantissa) + fn(2) * bits;
282
+ }
283
+ if (!(value > 0)) throw new Problem('math domain error');
284
+ return fn(value);
285
+ }
286
+
287
+ function gcdPair(a, b) {
288
+ if (a < 0n) a = -a;
289
+ if (b < 0n) b = -b;
290
+ while (b) [a, b] = [b, a % b];
291
+ return a;
292
+ }
293
+
294
+ function isqrt(n) {
295
+ if (n < 0n) throw new Problem('isqrt needs a nonnegative integer');
296
+ if (n < 2n) return n;
297
+ let x = 1n << BigInt(Math.ceil(bitLength(n) / 2));
298
+ while (true) {
299
+ const y = (x + n / x) >> 1n;
300
+ if (y >= x) return x;
301
+ x = y;
302
+ }
303
+ }
304
+
305
+ const BINARY = {
306
+ add, sub, mul, div: trueDivide, floordiv: floorDivide, mod: modulo, pow: power,
307
+ shl: shiftLeft, shr: shiftRight,
308
+ xor: bitwise('xor', (a, b) => a ^ b),
309
+ and: bitwise('and', (a, b) => a & b),
310
+ or: bitwise('or', (a, b) => a | b),
311
+ };
312
+ const UNARY = {
313
+ neg: (a) => (isInt(a) ? -a : -a),
314
+ pos: (a) => a,
315
+ not: (a) => {
316
+ needInts('not', [a]);
317
+ return ~a;
318
+ },
319
+ };
320
+ const FUNCTIONS = {
321
+ abs: (a) => (isInt(a) ? (a < 0n ? -a : a) : Math.abs(a)),
322
+ min: minmax('min', (order) => order < 0),
323
+ max: minmax('max', (order) => order > 0),
324
+ round,
325
+ int: truncate,
326
+ float: (a) => toFloat(a),
327
+ pow: power,
328
+ gcd: (...values) => {
329
+ needInts('gcd', values);
330
+ return values.reduce((a, b) => gcdPair(a, b), 0n);
331
+ },
332
+ lcm: (...values) => {
333
+ needInts('lcm', values);
334
+ return values.reduce((a, b) => {
335
+ if (a === 0n || b === 0n) return 0n;
336
+ const product = (a * b) / gcdPair(a, b);
337
+ return product < 0n ? -product : product;
338
+ }, 1n);
339
+ },
340
+ floor: (a) => (isInt(a) ? a : BigInt(Math.floor(a))),
341
+ ceil: (a) => (isInt(a) ? a : BigInt(Math.ceil(a))),
342
+ trunc: truncate,
343
+ sqrt: realFunction(Math.sqrt, (x) => x >= 0),
344
+ isqrt: (a) => {
345
+ needInts('isqrt', [a]);
346
+ return isqrt(a);
347
+ },
348
+ exp: realFunction(Math.exp),
349
+ log: (value, base) => {
350
+ const result = base === undefined ? logOf(value, Math.log) : logOf(value, Math.log) / logOf(base, Math.log);
351
+ if (!Number.isFinite(result)) throw new Problem('division by zero');
352
+ return result;
353
+ },
354
+ log2: (value) => logOf(value, Math.log2),
355
+ log10: (value) => logOf(value, Math.log10),
356
+ sin: realFunction(Math.sin),
357
+ cos: realFunction(Math.cos),
358
+ tan: realFunction(Math.tan),
359
+ asin: realFunction(Math.asin, (x) => x >= -1 && x <= 1),
360
+ acos: realFunction(Math.acos, (x) => x >= -1 && x <= 1),
361
+ atan: realFunction(Math.atan),
362
+ atan2: realFunction(Math.atan2),
363
+ sinh: realFunction(Math.sinh),
364
+ cosh: realFunction(Math.cosh),
365
+ tanh: realFunction(Math.tanh),
366
+ hypot: realFunction(Math.hypot),
367
+ degrees: realFunction((x) => (x * 180) / Math.PI),
368
+ radians: realFunction((x) => (x * Math.PI) / 180),
369
+ fmod: realFunction((x, y) => x % y, (x, y) => y !== 0),
370
+ copysign: realFunction((x, y) => (Math.sign(y) < 0 || Object.is(y, -0) ? -Math.abs(x) : Math.abs(x))),
371
+ };
372
+ // How many arguments each function takes: a number, or [least, most].
373
+ const ARITIES = {
374
+ abs: 1, min: [2, Infinity], max: [2, Infinity], round: [1, 2], int: 1, float: 1, pow: 2,
375
+ gcd: [0, Infinity], lcm: [0, Infinity], floor: 1, ceil: 1, trunc: 1, sqrt: 1, isqrt: 1, exp: 1,
376
+ log: [1, 2], log2: 1, log10: 1, sin: 1, cos: 1, tan: 1, asin: 1, acos: 1, atan: 1, atan2: 2,
377
+ sinh: 1, cosh: 1, tanh: 1, hypot: [0, Infinity], degrees: 1, radians: 1, fmod: 2, copysign: 2,
378
+ };
379
+ export const CONSTANTS = { pi: Math.PI, e: Math.E, tau: 2 * Math.PI };
380
+
381
+ function accepts(name, arity) {
382
+ const allowed = ARITIES[name];
383
+ if (allowed === undefined) return false;
384
+ if (typeof allowed === 'number') return arity === allowed;
385
+ return arity >= allowed[0] && arity <= allowed[1];
386
+ }
387
+
388
+ export function evaluate(term, env) {
389
+ term = deref(term, env);
390
+ const type = typeof term;
391
+ if (type === 'bigint' || type === 'number') return term;
392
+ if (term instanceof Var) throw new EyecodeError('arithmetic needs a bound expression, found an unbound variable');
393
+ if (type === 'string') {
394
+ if (Object.hasOwn(CONSTANTS, term)) return CONSTANTS[term];
395
+ throw new EyecodeError(`'${term}' is not an arithmetic value`);
396
+ }
397
+ const values = term.args.map((arg) => evaluate(arg, env));
398
+ return apply(term.name, values);
399
+ }
400
+
401
+ export function apply(name, values) {
402
+ const arity = values.length;
403
+ let fn;
404
+ if (arity === 2 && Object.hasOwn(BINARY, name)) fn = BINARY[name];
405
+ else if (arity === 1 && Object.hasOwn(UNARY, name)) fn = UNARY[name];
406
+ else if (Object.hasOwn(FUNCTIONS, name) && accepts(name, arity)) fn = FUNCTIONS[name];
407
+ else throw new EyecodeError(`${name}/${arity} is not an arithmetic function`);
408
+ let result;
409
+ try {
410
+ result = fn(...values);
411
+ } catch (error) {
412
+ if (!(error instanceof Problem)) throw error;
413
+ const reason = error.message;
414
+ if (reason.startsWith('resource: ')) throw new EyecodeError(`arithmetic ${reason}`);
415
+ if (reason === 'division by zero') throw new EyecodeError(`arithmetic: division by zero in ${name}`);
416
+ if (reason === 'overflow') throw new EyecodeError(`arithmetic: overflow in ${name}`);
417
+ throw new EyecodeError(`arithmetic: ${name} ${reason}`);
418
+ }
419
+ if (typeof result === 'number' && !Number.isFinite(result)) {
420
+ throw new EyecodeError(`arithmetic: ${name} has no finite result`);
421
+ }
422
+ return result;
423
+ }
424
+
425
+ // -1, 0 or 1: values compared exactly, across integers and floats.
426
+ export function compare(left, right, env) {
427
+ return compareNumbers(evaluate(left, env), evaluate(right, env));
428
+ }
429
+
430
+ export const ARITHMETIC_NAMES = new Set([
431
+ ...Object.keys(BINARY), ...Object.keys(UNARY), ...Object.keys(FUNCTIONS),
432
+ ]);
@@ -0,0 +1,185 @@
1
+ // The pure primitives.
2
+ //
3
+ // Only pure primitives belong here. Collections, call and negation are
4
+ // controls handled by the solver and explicitly distinguished in proof
5
+ // documents.
6
+ //
7
+ // A test decides a goal at most once: it is handed the goal's arguments, the
8
+ // same arguments dereferenced, the substitution and a bind function, and says
9
+ // whether the goal holds. Any bindings it made on the way to failing are
10
+ // undone for it. A relation can hold several times: it is a generator that
11
+ // yields once per solution, and the solver undoes the bindings of one
12
+ // solution before asking for the next.
13
+ import * as arith from './arith.js';
14
+ import {
15
+ EyecodeError, Struct, Var, codePoints, compareTerms, copyResolved, deref, isGround, key,
16
+ listFromItems, properListItems, unify,
17
+ } from './terms.js';
18
+
19
+ function comparison(holds) {
20
+ return (args, resolved, env) => holds(arith.compare(args[0], args[1], env));
21
+ }
22
+
23
+ function same(args, env) {
24
+ return compareTerms(copyResolved(args[0], env), copyResolved(args[1], env)) === 0;
25
+ }
26
+
27
+ function notUnify(args, resolved, env) {
28
+ const mark = env.mark();
29
+ const unifies = unify(args[0], args[1], env);
30
+ env.undo(mark);
31
+ return !unifies;
32
+ }
33
+
34
+ function boundedInteger(term, low, high) {
35
+ if (typeof term !== 'bigint' || term < 0n) throw new EyecodeError('expected a nonnegative integer');
36
+ if (term < BigInt(low) || term > BigInt(high)) throw new EyecodeError(`integer outside ${low}..${high}`);
37
+ return Number(term);
38
+ }
39
+
40
+ let functorSerial = 0;
41
+
42
+ function functor(args, resolved, env, bind) {
43
+ const [term, name, arity] = resolved;
44
+ if (!(term instanceof Var)) {
45
+ if (term instanceof Struct) return bind(args[1], term.name) && bind(args[2], BigInt(term.args.length));
46
+ return bind(args[1], term) && bind(args[2], 0n);
47
+ }
48
+ const count = boundedInteger(arity, 0, 1024);
49
+ if (name instanceof Var) throw new EyecodeError('functor/3 needs a bound name');
50
+ if (count > 0 && typeof name !== 'string') throw new EyecodeError('functor/3 needs an atom name');
51
+ // Unique names across primitive invocations preserve variable isolation.
52
+ functorSerial++;
53
+ if (count === 0) return bind(args[0], name);
54
+ const fresh = [];
55
+ for (let i = 0; i < count; i++) fresh.push(new Var(`_functor${functorSerial}_${i}`));
56
+ return bind(args[0], new Struct(name, fresh));
57
+ }
58
+
59
+ function arg(args, resolved, env, bind) {
60
+ const [position, term] = resolved;
61
+ const index = boundedInteger(position, 1, 1024);
62
+ return term instanceof Struct && index <= term.args.length && bind(args[2], term.args[index - 1]);
63
+ }
64
+
65
+ function univ(args, resolved, env, bind) {
66
+ const term = resolved[0];
67
+ if (!(term instanceof Var)) {
68
+ if (term instanceof Struct) return bind(args[1], listFromItems([term.name, ...term.args]));
69
+ return bind(args[1], listFromItems([term]));
70
+ }
71
+ let items = properListItems(args[1], env);
72
+ items = items === null ? null : items.map((item) => deref(item, env));
73
+ if (!items || !items.length || items[0] instanceof Var || (items.length > 1 && typeof items[0] !== 'string')) {
74
+ throw new EyecodeError('univ/2 needs a nonempty bound term list');
75
+ }
76
+ return bind(args[0], items.length === 1 ? items[0] : new Struct(items[0], items.slice(1)));
77
+ }
78
+
79
+ function atomText(name, codes) {
80
+ return (args, resolved, env, bind) => {
81
+ const term = resolved[0];
82
+ if (typeof term === 'string') {
83
+ const items = codes ? codePoints(term).map(BigInt) : Array.from(term);
84
+ return bind(args[1], listFromItems(items));
85
+ }
86
+ const items = properListItems(args[1], env);
87
+ if (items === null) throw new EyecodeError(`${name} needs a proper list`);
88
+ const chars = [];
89
+ for (let item of items) {
90
+ item = deref(item, env);
91
+ if (codes) {
92
+ chars.push(String.fromCodePoint(boundedInteger(item, 0, 0x10ffff)));
93
+ } else {
94
+ if (typeof item !== 'string' || Array.from(item).length !== 1) {
95
+ throw new EyecodeError('atom_chars/2 needs characters');
96
+ }
97
+ chars.push(item);
98
+ }
99
+ }
100
+ return bind(args[0], chars.join(''));
101
+ };
102
+ }
103
+
104
+ function atomLength(args, resolved, env, bind) {
105
+ if (typeof resolved[0] !== 'string') throw new EyecodeError('atom_length/2 needs a bound atom');
106
+ return bind(args[1], BigInt(Array.from(resolved[0]).length));
107
+ }
108
+
109
+ function compare(args, resolved, env, bind) {
110
+ const order = compareTerms(copyResolved(args[1], env), copyResolved(args[2], env));
111
+ return bind(args[0], order < 0 ? '<' : order > 0 ? '>' : '=');
112
+ }
113
+
114
+ function is(args, resolved, env, bind) {
115
+ return bind(args[0], arith.evaluate(args[1], env));
116
+ }
117
+
118
+ const isNumberValue = (term) => typeof term === 'bigint' || typeof term === 'number';
119
+
120
+ export const TESTS = new Map([
121
+ ['true/0', () => true],
122
+ ['fail/0', () => false],
123
+ ['false/0', () => false],
124
+ ['unify/2', (args, resolved, env, bind) => bind(args[0], args[1])],
125
+ ['not_unify/2', notUnify],
126
+ ['identical/2', (args, resolved, env) => same(args, env)],
127
+ ['not_identical/2', (args, resolved, env) => !same(args, env)],
128
+ ['is/2', is],
129
+ ['eq/2', comparison((n) => n === 0)],
130
+ ['ne/2', comparison((n) => n !== 0)],
131
+ ['lt/2', comparison((n) => n < 0)],
132
+ ['le/2', comparison((n) => n <= 0)],
133
+ ['gt/2', comparison((n) => n > 0)],
134
+ ['ge/2', comparison((n) => n >= 0)],
135
+ ['is_var/1', (args, resolved) => resolved[0] instanceof Var],
136
+ ['is_nonvar/1', (args, resolved) => !(resolved[0] instanceof Var)],
137
+ ['is_ground/1', (args, resolved, env) => isGround(args[0], env)],
138
+ ['is_atom/1', (args, resolved) => typeof resolved[0] === 'string'],
139
+ ['is_number/1', (args, resolved) => isNumberValue(resolved[0])],
140
+ ['is_int/1', (args, resolved) => typeof resolved[0] === 'bigint'],
141
+ ['is_float/1', (args, resolved) => typeof resolved[0] === 'number'],
142
+ ['is_compound/1', (args, resolved) => resolved[0] instanceof Struct],
143
+ ['functor/3', functor],
144
+ ['arg/3', arg],
145
+ ['univ/2', univ],
146
+ ['atom_chars/2', atomText('atom_chars/2', false)],
147
+ ['atom_codes/2', atomText('atom_codes/2', true)],
148
+ ['atom_length/2', atomLength],
149
+ ['compare/3', compare],
150
+ ]);
151
+
152
+ function* atomConcat(args, resolved, env, mark) {
153
+ const [left, right, whole] = resolved;
154
+ if (typeof left === 'string' && typeof right === 'string') {
155
+ if (unify(args[2], left + right, env)) yield env;
156
+ else env.undo(mark);
157
+ return;
158
+ }
159
+ if (typeof whole !== 'string') throw new EyecodeError('atom_concat/3 needs the result or both operands bound');
160
+ const chars = Array.from(whole);
161
+ for (let i = 0; i <= chars.length; i++) {
162
+ if (unify(args[0], chars.slice(0, i).join(''), env) && unify(args[1], chars.slice(i).join(''), env)) yield env;
163
+ env.undo(mark);
164
+ }
165
+ }
166
+
167
+ export const RELATIONS = new Map([['atom_concat/3', atomConcat]]);
168
+ export const PRIMITIVE_KEYS = new Set([...TESTS.keys(), ...RELATIONS.keys()]);
169
+
170
+ // Solve a primitive goal: a generator yielding env once per solution.
171
+ export function* primitive(goal, env) {
172
+ const goalKey = key(goal);
173
+ const args = goal instanceof Struct ? goal.args : [];
174
+ const resolved = args.map((a) => deref(a, env));
175
+ const mark = env.mark();
176
+ const test = TESTS.get(goalKey);
177
+ if (test === undefined) {
178
+ const relation = RELATIONS.get(goalKey);
179
+ if (relation === undefined) throw new EyecodeError(`unsupported primitive ${goalKey}`);
180
+ yield* relation(args, resolved, env, mark);
181
+ return;
182
+ }
183
+ if (test(args, resolved, env, (a, b) => unify(a, b, env))) yield env;
184
+ else env.undo(mark);
185
+ }