eyeprolog 1.5.8 → 1.5.10

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/ARCHITECTURE.md +18 -11
  4. package/src/arithmetic-host.js +116 -0
  5. package/src/{atts.js → atts-host.js} +3 -1
  6. package/src/charsio-host.js +380 -0
  7. package/src/crypto-host.js +628 -0
  8. package/src/dcgs-host.js +37 -0
  9. package/src/dif-host.js +8 -0
  10. package/src/eyelet-host.js +71 -0
  11. package/src/files-host.js +174 -0
  12. package/src/format-host.js +52 -0
  13. package/src/host-utils.js +67 -0
  14. package/src/iso.js +5 -20
  15. package/src/iso_ext-host.js +180 -0
  16. package/src/lib/arithmetic.pl +32 -0
  17. package/src/lib/charsio.pl +60 -1
  18. package/src/lib/clpb.pl +1 -1
  19. package/src/lib/crypto.pl +91 -0
  20. package/src/lib/dcgs.pl +32 -3
  21. package/src/lib/debug.pl +2 -9
  22. package/src/lib/files.pl +98 -6
  23. package/src/lib/gensym.pl +2 -2
  24. package/src/lib/iso_ext.pl +54 -8
  25. package/src/lib/lists.pl +8 -0
  26. package/src/lib/pio.pl +9 -2
  27. package/src/lib/random.pl +2 -2
  28. package/src/lib/time.pl +8 -2
  29. package/src/library-autoload-index.js +49 -5
  30. package/src/lists-host.js +29 -0
  31. package/src/os-host.js +93 -0
  32. package/src/pio-host.js +30 -0
  33. package/src/random-host.js +29 -0
  34. package/src/standard-library.js +63 -27
  35. package/src/tabling-host.js +22 -0
  36. package/src/time-host.js +36 -0
  37. package/test/run-architecture.mjs +71 -0
  38. package/test/run-regression.mjs +151 -12
  39. package/test/scryer-library-exports.json +391 -0
  40. package/the-art-of-eyeprolog.md +54 -33
  41. package/why-eyeprolog.md +16 -7
  42. package/src/library-host.js +0 -576
  43. package/src/scryer-compat.js +0 -339
package/README.md CHANGED
@@ -61,7 +61,7 @@ Normal mode executes top-level `Conclusion :+ Premise` rules through the bundled
61
61
 
62
62
  Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL; use `--no-autoload` to require explicit imports, and explicitly import libraries that introduce operators before using their syntax. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
63
63
 
64
- The Trealla/Scryer compatibility surface now also includes chars-based term I/O and Base64 helpers, rational-form conversion in `library(arithmetic)`, `sleep/1`, the shared `library(iso_ext)` control/term re-exports, and focused `library(files)` and `library(os)` modules. Filesystem and OS side effects require Node; browser calls report an unavailable host resource instead. Non-integral `number_to_rational/2` results are represented structurally as `rdiv(Numerator,Denominator)` until rational numbers become processor numeric values.
64
+ All 32 bundled modules that overlap Scryer's current `src/lib/` surface now cover Scryer's exported predicates, while the stricter Trealla/Scryer intersection remains a separate portability profile. Runtime-dependent library primitives follow one ownership rule: `src/lib/<module>.pl` calls private adapters from `src/<module>-host.js`; pure Prolog libraries need no host file. This includes the full Scryer `library(files)` surface, Scryer character-list paths in `library(pio)` with atom-path compatibility, chars/UTF-8/term/Base64 helpers, the completed arithmetic/time/`iso_ext` surfaces, and the full Scryer `library(crypto)`. Filesystem and OS side effects require Node; crypto uses Node's backend where needed, with Web Crypto also used for secure random bytes when available. Non-integral rational conversions remain structural `rdiv(Numerator,Denominator)` terms until rational numbers become processor numeric values.
65
65
 
66
66
  ## Links
67
67
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.8",
6
+ "version": "1.5.10",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -15,8 +15,9 @@ higher-level frontends.
15
15
  `cleanup.js` owns lifecycle-aware disposal of protected builtin
16
16
  iterators and registers the normal-profile cleanup controls without making
17
17
  `solver.js` depend back on the language registry.
18
- 4. **Language services** — `iso.js`, `iso-arithmetic.js`, `dcg.js`, `atts.js`,
19
- `scryer-compat.js`, `library-host.js`, `standard-library.js`, and `src/lib/`.
18
+ 4. **Language services** — `iso.js`, `iso-arithmetic.js`, `dcg.js`, `atts-host.js`,
19
+ `standard-library.js`, the module-owned `src/<module>-host.js` adapters, and
20
+ the public Prolog modules under `src/lib/`.
20
21
  5. **Frontends/tools** — `execute.js`, `repl.js`, `cli.js`, `quads.js`,
21
22
  `explain.js`, and the playground worker.
22
23
 
@@ -40,7 +41,7 @@ annotations. Descriptors can optionally define logical subsumption so the
40
41
  store retains only its strongest pending constraints. `dif/2` is the first
41
42
  descriptor-based user of this mechanism.
42
43
 
43
- `atts.js` layers Prolog-visible attributed variables over that same persistent
44
+ `atts-host.js` layers Prolog-visible attributed variables over that same persistent
44
45
  environment. Per-module attributes follow the current variable representative.
45
46
  When an attributed variable is about to be bound, the solver runs that module's
46
47
  `verify_attributes/3` against the still-unbound representative; only a successful
@@ -51,9 +52,9 @@ protocol directly for its domains and propagators.
51
52
  `Env` also owns the small generic backtrackable blackboard used by Scryer-style
52
53
  libraries. Blackboard maps are shared by clones and copied only on write, so
53
54
  `bb_b_put/2` follows ordinary Prolog backtracking without any CLP(Z)-specific
54
- state in the VM. `scryer-compat.js` exposes the private host bridge used by
55
- `library(debug)`; the remaining Scryer compatibility surface lives as Prolog
56
- modules under `src/lib/`.
55
+ state in the VM. Its private runtime bridge is owned by `iso_ext-host.js`, matching
56
+ Scryer's canonical `library(iso_ext)` ownership; `library(debug)` may re-export
57
+ that interface for EyeProlog compatibility without owning another implementation.
57
58
 
58
59
  `source-expansion.js` is the explicit compile-time execution boundary. When an
59
60
  already-loaded `term_expansion/2` or `goal_expansion/2` hook exists, program
@@ -65,11 +66,17 @@ import graph acyclic while avoiding a second meta-interpreter solely for source
65
66
  expansion. `expansion-builtins.js` provides the lower-level `expand_term/2` DCG
66
67
  service without depending on `Solver`.
67
68
 
68
- The JavaScript runtime stays flat directly under `src/`; the existing `src/lib/`
69
- contains Prolog library sources rather than JavaScript runtime modules. The architecture
70
- test rejects JavaScript import cycles under `src/`. Cleanup lifecycle hooks are
71
- installed from the public API and CLI entry paths; `standard-library.js` only
72
- registers the predicates, so the execution layer remains acyclic.
69
+ The JavaScript runtime stays flat directly under `src/`; `src/lib/` contains the
70
+ public Prolog library sources. Runtime-dependent library primitives follow a
71
+ one-module/one-host convention: a private `eyeprolog__*` adapter referenced by
72
+ `src/lib/foo.pl` must be registered from `src/foo-host.js`. Pure Prolog modules,
73
+ such as `library(freeze)`, intentionally have no host file. `standard-library.js`
74
+ registers these module hosts without owning their semantics. The architecture
75
+ test rejects JavaScript import cycles, verifies private adapter ownership, freezes
76
+ the current Scryer export surface for all 32 overlapping bundled modules, and
77
+ rejects the retired `library-host.js` and `scryer-compat.js` grab bags. Cleanup
78
+ lifecycle hooks are installed from the public API and CLI entry paths, so the
79
+ execution layer remains acyclic.
73
80
 
74
81
  ## Performance rule
75
82
 
@@ -0,0 +1,116 @@
1
+ // Runtime arithmetic conversions used by library(arithmetic).
2
+ // Pure arithmetic relations stay in src/lib/arithmetic.pl.
3
+
4
+ import { COMPOUND, NUMBER, VAR, compound, copyResolved, deref, numberTerm, unify } from './term.js';
5
+ import { PrologError } from './errors.js';
6
+
7
+ function gcdBigInt(a, b) {
8
+ a = a < 0n ? -a : a;
9
+ b = b < 0n ? -b : b;
10
+ while (b !== 0n) [a, b] = [b, a % b];
11
+ return a;
12
+ }
13
+
14
+ function approximateRational(value, tolerance = 1e-6) {
15
+ if (!Number.isFinite(value)) throw new PrologError('type_error(number)', numberTerm(String(value)));
16
+ if (!Number.isFinite(tolerance) || tolerance < 0) throw new PrologError('domain_error(not_less_than_zero)', numberTerm(String(tolerance)));
17
+ if (Number.isInteger(value) && Number.isSafeInteger(value)) return [BigInt(value), 1n];
18
+ const sign = value < 0 ? -1n : 1n;
19
+ const target = Math.abs(value);
20
+ let x = target;
21
+ let hPrev2 = 0, hPrev1 = 1, kPrev2 = 1, kPrev1 = 0;
22
+ for (let i = 0; i < 96; i++) {
23
+ const a = Math.floor(x);
24
+ const h = a * hPrev1 + hPrev2;
25
+ const k = a * kPrev1 + kPrev2;
26
+ if (!Number.isSafeInteger(h) || !Number.isSafeInteger(k) || k === 0) break;
27
+ if (Math.abs(target - h / k) <= tolerance) {
28
+ const n = sign * BigInt(h), d = BigInt(k), g = gcdBigInt(n, d);
29
+ return [n / g, d / g];
30
+ }
31
+ const fraction = x - a;
32
+ if (fraction === 0) {
33
+ const n = sign * BigInt(h), d = BigInt(k), g = gcdBigInt(n, d);
34
+ return [n / g, d / g];
35
+ }
36
+ [hPrev2, hPrev1] = [hPrev1, h];
37
+ [kPrev2, kPrev1] = [kPrev1, k];
38
+ x = 1 / fraction;
39
+ }
40
+
41
+ // Exact decimal-token fallback for continued fractions that exceed safe
42
+ // JavaScript integer intermediates.
43
+ const text = target.toString();
44
+ const match = /^(\d+)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(text);
45
+ if (!match) throw new PrologError('representation_error(rational)');
46
+ const fractionDigits = match[2] ?? '';
47
+ const exponent = Number(match[3] ?? 0);
48
+ let numerator = BigInt(match[1] + fractionDigits);
49
+ let denominator = 10n ** BigInt(fractionDigits.length);
50
+ if (exponent > 0) numerator *= 10n ** BigInt(exponent);
51
+ else if (exponent < 0) denominator *= 10n ** BigInt(-exponent);
52
+ numerator *= sign;
53
+ const g = gcdBigInt(numerator, denominator);
54
+ return [numerator / g, denominator / g];
55
+ }
56
+
57
+ function rationalParts(term, env, tolerance = 1e-6) {
58
+ const value = deref(term, env);
59
+ if (value.type === VAR) throw new PrologError('instantiation_error');
60
+ if (value.type === NUMBER) {
61
+ if (/^-?\d+$/.test(value.name)) return [BigInt(value.name), 1n];
62
+ return approximateRational(Number(value.name), tolerance);
63
+ }
64
+ if (value.type === COMPOUND && value.name === 'rdiv' && value.arity === 2) {
65
+ const numerator = deref(value.args[0], env), denominator = deref(value.args[1], env);
66
+ if (numerator.type !== NUMBER || denominator.type !== NUMBER ||
67
+ !/^-?\d+$/.test(numerator.name) || !/^-?\d+$/.test(denominator.name)) {
68
+ throw new PrologError('type_error(rational)', copyResolved(value, env));
69
+ }
70
+ let n = BigInt(numerator.name), d = BigInt(denominator.name);
71
+ if (d === 0n) throw new PrologError('evaluation_error(zero_divisor)');
72
+ if (d < 0n) { n = -n; d = -d; }
73
+ const g = gcdBigInt(n, d);
74
+ return [n / g, d / g];
75
+ }
76
+ throw new PrologError('type_error(number)', copyResolved(value, env));
77
+ }
78
+
79
+ function rationalTerm(numerator, denominator) {
80
+ return denominator === 1n
81
+ ? numberTerm(numerator.toString())
82
+ : compound('rdiv', [numberTerm(numerator.toString()), numberTerm(denominator.toString())]);
83
+ }
84
+
85
+ function* numberToRationalBuiltin({ goal, env }) {
86
+ const [numerator, denominator] = rationalParts(goal.args[0], env);
87
+ const next = env.clone();
88
+ if (unify(goal.args[1], rationalTerm(numerator, denominator), next)) yield next;
89
+ }
90
+
91
+ function* numberToRationalToleranceBuiltin({ goal, env }) {
92
+ const epsilonTerm = deref(goal.args[0], env);
93
+ if (epsilonTerm.type === VAR) throw new PrologError('instantiation_error');
94
+ if (epsilonTerm.type !== NUMBER) throw new PrologError('type_error(number)', copyResolved(epsilonTerm, env));
95
+ const epsilon = Number(epsilonTerm.name);
96
+ if (!Number.isFinite(epsilon)) throw new PrologError('type_error(number)', copyResolved(epsilonTerm, env));
97
+ if (epsilon < 0) throw new PrologError('domain_error(not_less_than_zero)', copyResolved(epsilonTerm, env));
98
+ const [numerator, denominator] = rationalParts(goal.args[1], env, epsilon);
99
+ const next = env.clone();
100
+ if (unify(goal.args[2], rationalTerm(numerator, denominator), next)) yield next;
101
+ }
102
+
103
+ function* rationalPartsBuiltin({ goal, env }) {
104
+ const [numerator, denominator] = rationalParts(goal.args[0], env);
105
+ const next = env.clone();
106
+ if (unify(goal.args[1], numberTerm(numerator.toString()), next) &&
107
+ unify(goal.args[2], numberTerm(denominator.toString()), next)) yield next;
108
+ }
109
+
110
+ export const arithmeticHostBuiltins = {
111
+ register(registry) {
112
+ registry.add('eyeprolog__number_to_rational', 2, numberToRationalBuiltin, { deterministic: true, eyePrologLibrary: true });
113
+ registry.add('eyeprolog__number_to_rational', 3, numberToRationalToleranceBuiltin, { deterministic: true, eyePrologLibrary: true });
114
+ registry.add('eyeprolog__rational_numerator_denominator', 3, rationalPartsBuiltin, { deterministic: true, eyePrologLibrary: true });
115
+ },
116
+ };
@@ -11,6 +11,7 @@ import {
11
11
  properListItems, unify, variable,
12
12
  } from './term.js';
13
13
  import { PrologError } from './errors.js';
14
+ import { callResidueVarsBuiltin } from './iso.js';
14
15
 
15
16
  function callerModule(goal) {
16
17
  return goal.module ?? 'user';
@@ -217,8 +218,9 @@ function* delFromAttrListBuiltin({ goal, env }) {
217
218
  yield next;
218
219
  }
219
220
 
220
- export const attsBuiltins = {
221
+ export const attsHostBuiltins = {
221
222
  register(registry) {
223
+ registry.add('eyeprolog__call_residue_vars', 2, callResidueVarsBuiltin, { eyePrologLibrary: true });
222
224
  registry.add('put_atts', 2, putAttsBuiltin, { deterministic: true, eyePrologLibrary: true });
223
225
  registry.add('get_atts', 2, getAttsBuiltin, { deterministic: true, eyePrologLibrary: true });
224
226
  // Scryer's CLP(Z) sources use these through goal expansion. EyeProlog
@@ -0,0 +1,380 @@
1
+ // Runtime character parsing/writing and character classification for
2
+ // library(charsio). Portable UTF-8 relations remain in src/lib/charsio.pl.
3
+
4
+ import { createParserOperatorState, parseTermText } from './parser.js';
5
+ import { BufferCtor } from './platform.js';
6
+ import {
7
+ ATOM, COMPOUND, NUMBER, VAR, atom, compound, copyResolved, deref,
8
+ listFromItems, numberTerm, properListItems, unify, variable,
9
+ } from './term.js';
10
+ import { PrologError } from './errors.js';
11
+ import { formatTermForWrite } from './write.js';
12
+ import { characterListText, chars } from './host-utils.js';
13
+
14
+ let readFresh = 0;
15
+ function activeCharConverter(solver) {
16
+ if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) {
17
+ return null;
18
+ }
19
+ return (character) => solver.charConversions.get(character) ?? character;
20
+ }
21
+
22
+ function convertTextCharacters(text, solver) {
23
+ const convert = activeCharConverter(solver);
24
+ if (convert == null) return text;
25
+ let result = '', quote = null;
26
+ for (let i = 0; i < text.length; i++) {
27
+ const ch = text[i], next = text[i + 1];
28
+ if (quote != null) {
29
+ result += ch;
30
+ if (ch === '\\') {
31
+ if (i + 1 < text.length) result += text[++i];
32
+ } else if (ch === quote && next === quote) {
33
+ result += text[++i];
34
+ } else if (ch === quote) {
35
+ quote = null;
36
+ }
37
+ continue;
38
+ }
39
+ if (ch === "'" || ch === '"') {
40
+ quote = ch;
41
+ result += ch;
42
+ } else {
43
+ result += convert(ch);
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+
49
+ function scopeReadTerm(term) {
50
+ const scope = ++readFresh;
51
+ const bySourceName = new Map();
52
+ const variables = [];
53
+ const copy = (item) => {
54
+ if (item.type === VAR) {
55
+ let record = bySourceName.get(item.name);
56
+ if (record == null) {
57
+ const scoped = variable(`\u0000chars:${scope}:${variables.length}`);
58
+ scoped.displayName = item.name;
59
+ record = {
60
+ sourceName: item.name,
61
+ term: scoped,
62
+ count: 0,
63
+ anonymous: item.name.startsWith('__anon'),
64
+ };
65
+ bySourceName.set(item.name, record);
66
+ variables.push(record);
67
+ }
68
+ record.count++;
69
+ return record.term;
70
+ }
71
+ if (item.type !== COMPOUND) return item;
72
+ return compound(item.name, item.args.map(copy));
73
+ };
74
+ return { term: copy(term), variables };
75
+ }
76
+
77
+ function readTextTerm(text, solver) {
78
+ const converted = convertTextCharacters(text, solver);
79
+ try {
80
+ const operatorState = createParserOperatorState(solver.program.operators.values(), false);
81
+ const parsed = parseTermText(converted, {
82
+ operatorState,
83
+ isoStrict: solver.isoStrict,
84
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
85
+ readTermEnd: converted.endsWith('.') ? converted.length - 1 : undefined,
86
+ });
87
+ return scopeReadTerm(parsed);
88
+ } catch (error) {
89
+ if (error instanceof PrologError) throw error;
90
+ throw new PrologError('syntax_error(read_term)');
91
+ }
92
+ }
93
+
94
+ function requireOptionList(term, env, kind) {
95
+ const value = deref(term, env);
96
+ if (value.type === VAR) throw new PrologError('instantiation_error');
97
+ const options = properListItems(value, env);
98
+ if (options == null) throw new PrologError('type_error(list)', copyResolved(value, env));
99
+ for (const optionTerm of options) {
100
+ if (deref(optionTerm, env).type === VAR) throw new PrologError('instantiation_error');
101
+ }
102
+ return options.map((option) => deref(option, env));
103
+ }
104
+
105
+ function* readFromCharsBuiltin({ solver, goal, env }) {
106
+ const text = characterListText(goal.args[0], env);
107
+ const { term } = readTextTerm(text, solver);
108
+ const next = env.clone();
109
+ if (unify(goal.args[1], term, next)) yield next;
110
+ }
111
+
112
+ function* readTermFromCharsBuiltin({ solver, goal, env }) {
113
+ const text = characterListText(goal.args[0], env);
114
+ const options = requireOptionList(goal.args[2], env, 'read');
115
+ for (const option of options) {
116
+ if (option.type !== COMPOUND || option.arity !== 1 ||
117
+ !['variables', 'variable_names', 'singletons'].includes(option.name)) {
118
+ throw new PrologError('domain_error(read_option)', copyResolved(option, env));
119
+ }
120
+ }
121
+ const { term, variables } = readTextTerm(text, solver);
122
+ const next = env.clone();
123
+ if (!unify(goal.args[1], term, next)) return;
124
+ for (const option of options) {
125
+ const value = option.name === 'variables'
126
+ ? listFromItems(variables.map((item) => item.term))
127
+ : option.name === 'variable_names'
128
+ ? listFromItems(variables
129
+ .filter((item) => !item.anonymous)
130
+ .map((item) => compound('=', [atom(item.sourceName), item.term])))
131
+ : listFromItems(variables
132
+ .filter((item) => !item.anonymous && item.count === 1)
133
+ .map((item) => compound('=', [atom(item.sourceName), item.term])));
134
+ if (!unify(option.args[0], value, next)) return;
135
+ }
136
+ yield next;
137
+ }
138
+
139
+ function optionBoolean(term, env, option) {
140
+ const value = deref(term, env);
141
+ if (value.type === VAR) throw new PrologError('instantiation_error');
142
+ if (value.type !== ATOM || !['true', 'false'].includes(value.name)) {
143
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
144
+ }
145
+ return value.name === 'true';
146
+ }
147
+
148
+ function variableNamesOption(term, env, option) {
149
+ const value = deref(term, env);
150
+ if (value.type === VAR) throw new PrologError('instantiation_error');
151
+ const items = properListItems(value, env);
152
+ if (items == null) throw new PrologError('domain_error(write_option)', copyResolved(option, env));
153
+ const names = new Map();
154
+ for (const itemTerm of items) {
155
+ const item = deref(itemTerm, env);
156
+ if (item.type !== COMPOUND || item.name !== '=' || item.arity !== 2) {
157
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
158
+ }
159
+ const name = deref(item.args[0], env);
160
+ const target = deref(item.args[1], env);
161
+ if (name.type === VAR) throw new PrologError('instantiation_error');
162
+ if (name.type !== ATOM) throw new PrologError('domain_error(write_option)', copyResolved(option, env));
163
+ if (target.type === VAR && !names.has(target.name)) names.set(target.name, name.name);
164
+ }
165
+ return names;
166
+ }
167
+
168
+ function writeOptions(term, env, solver) {
169
+ const options = requireOptionList(term, env, 'write');
170
+ const result = {
171
+ quoted: false,
172
+ ignoreOps: false,
173
+ numbervars: false,
174
+ variableNames: new Map(),
175
+ compact: true,
176
+ minimalOperatorSpacing: true,
177
+ operatorAtomsAsArgs: true,
178
+ doubleQuotes: null,
179
+ };
180
+ for (const option of options) {
181
+ if (option.type !== COMPOUND || option.arity !== 1) {
182
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
183
+ }
184
+ if (option.name === 'quoted') result.quoted = optionBoolean(option.args[0], env, option);
185
+ else if (option.name === 'ignore_ops') result.ignoreOps = optionBoolean(option.args[0], env, option);
186
+ else if (option.name === 'numbervars') result.numbervars = optionBoolean(option.args[0], env, option);
187
+ else if (option.name === 'variable_names') result.variableNames = variableNamesOption(option.args[0], env, option);
188
+ else if (option.name === 'double_quotes' && !solver.isoStrict) result.doubleQuotes = optionBoolean(option.args[0], env, option);
189
+ else throw new PrologError('domain_error(write_option)', copyResolved(option, env));
190
+ }
191
+ return result;
192
+ }
193
+
194
+ function* writeTermToCharsBuiltin({ solver, goal, env }) {
195
+ if (deref(goal.args[2], env).type !== VAR) {
196
+ throw new PrologError('uninstantiation_error', copyResolved(goal.args[2], env));
197
+ }
198
+ const options = writeOptions(goal.args[1], env, solver);
199
+ const text = formatTermForWrite(goal.args[0], env, {
200
+ ...options,
201
+ generateVariableNames: true,
202
+ variableNameState: solver.writeVariableState,
203
+ operators: solver.program.operators.values(),
204
+ });
205
+ const next = env.clone();
206
+ if (unify(goal.args[2], chars(text), next)) yield next;
207
+ }
208
+
209
+ function base64Options(term, env) {
210
+ const options = requireOptionList(term, env, 'base64');
211
+ let padding = true, charset = 'standard';
212
+ for (const option of options) {
213
+ if (option.type !== COMPOUND || option.arity !== 1) {
214
+ throw new PrologError('domain_error(base64_option)', copyResolved(option, env));
215
+ }
216
+ const value = deref(option.args[0], env);
217
+ if (value.type === VAR) throw new PrologError('instantiation_error');
218
+ if (option.name === 'padding') {
219
+ if (value.type !== ATOM || !['true', 'false'].includes(value.name)) {
220
+ throw new PrologError('domain_error(boolean)', copyResolved(value, env));
221
+ }
222
+ padding = value.name === 'true';
223
+ } else if (option.name === 'charset') {
224
+ if (value.type !== ATOM || !['standard', 'url'].includes(value.name)) {
225
+ throw new PrologError('domain_error(charset)', copyResolved(value, env));
226
+ }
227
+ charset = value.name;
228
+ } else {
229
+ throw new PrologError('domain_error(base64_option)', copyResolved(option, env));
230
+ }
231
+ }
232
+ return { padding, charset };
233
+ }
234
+
235
+ function base64Encode(text, charset, padding) {
236
+ const bytes = [];
237
+ for (const ch of Array.from(text)) {
238
+ const code = ch.codePointAt(0);
239
+ if (code > 255) throw new PrologError('domain_error(octet_character)', numberTerm(code));
240
+ bytes.push(code);
241
+ }
242
+ let encoded;
243
+ if (BufferCtor != null) encoded = BufferCtor.from(bytes).toString('base64');
244
+ else if (typeof btoa === 'function') encoded = btoa(String.fromCharCode(...bytes));
245
+ else throw new PrologError('resource_error(base64)');
246
+ if (charset === 'url') encoded = encoded.replace(/\+/g, '-').replace(/\//g, '_');
247
+ if (!padding) encoded = encoded.replace(/=+$/, '');
248
+ return encoded;
249
+ }
250
+
251
+ function base64Decode(text, charset) {
252
+ let encoded = text;
253
+ if (charset === 'url') encoded = encoded.replace(/-/g, '+').replace(/_/g, '/');
254
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length % 4 === 1) {
255
+ throw new PrologError('domain_error(base64)', chars(text));
256
+ }
257
+ encoded += '='.repeat((4 - encoded.length % 4) % 4);
258
+ let bytes;
259
+ if (BufferCtor != null) bytes = [...BufferCtor.from(encoded, 'base64')];
260
+ else if (typeof atob === 'function') bytes = [...atob(encoded)].map((ch) => ch.charCodeAt(0));
261
+ else throw new PrologError('resource_error(base64)');
262
+ return String.fromCharCode(...bytes);
263
+ }
264
+
265
+ function* charsBase64Builtin({ goal, env }) {
266
+ const source = deref(goal.args[0], env);
267
+ const encoded = deref(goal.args[1], env);
268
+ if (source.type === VAR && encoded.type === VAR) throw new PrologError('instantiation_error');
269
+ const { padding, charset } = base64Options(goal.args[2], env);
270
+ const next = env.clone();
271
+ if (source.type !== VAR) {
272
+ const text = characterListText(goal.args[0], env);
273
+ if (unify(goal.args[1], chars(base64Encode(text, charset, padding)), next)) yield next;
274
+ return;
275
+ }
276
+ const text = characterListText(goal.args[1], env);
277
+ if (unify(goal.args[0], chars(base64Decode(text, charset)), next)) yield next;
278
+ }
279
+
280
+ function charTypeCandidates(ch) {
281
+ const code = ch.codePointAt(0);
282
+ const letter = /\p{L}/u.test(ch);
283
+ const decimal = /\p{Nd}/u.test(ch);
284
+ const numeric = /\p{N}/u.test(ch);
285
+ const whitespace = /\s/u.test(ch);
286
+ const control = /\p{Cc}/u.test(ch);
287
+ const lower = /\p{Ll}/u.test(ch);
288
+ const upper = /\p{Lu}/u.test(ch);
289
+ const asciiGraphic = code >= 0x21 && code <= 0x7e;
290
+ const out = [];
291
+ const add = (name, yes) => { if (yes) out.push(atom(name)); };
292
+ add('alnum', letter || decimal);
293
+ add('alpha', letter);
294
+ add('alphabetic', letter);
295
+ add('alphanumeric', letter || decimal);
296
+ add('ascii', code <= 0x7f);
297
+ add('ascii_graphic', asciiGraphic);
298
+ add('ascii_punctuation', asciiGraphic && !letter && !decimal);
299
+ add('binary_digit', ch === '0' || ch === '1');
300
+ add('control', control);
301
+ add('decimal_digit', decimal);
302
+ add('exponent', ch === 'e' || ch === 'E');
303
+ add('graphic', !whitespace && !control);
304
+ add('graphic_token', '#$&*+-./:<=>?@\\^~'.includes(ch));
305
+ add('hexadecimal_digit', /^[0-9A-Fa-f]$/.test(ch));
306
+ add('layout', whitespace);
307
+ add('lower', lower);
308
+ add('meta', "\\'\"`".includes(ch));
309
+ add('numeric', numeric);
310
+ add('octal_digit', /^[0-7]$/.test(ch));
311
+ add('octet', code <= 0xff);
312
+ add('prolog', true);
313
+ add('sign', ch === '+' || ch === '-');
314
+ add('solo', '!,;[]{}()|'.includes(ch));
315
+ add('symbolic_control', '#$&*+-./:<=>?@\\^~'.includes(ch));
316
+ add('symbolic_hexadecimal', /^[A-Fa-f]$/.test(ch));
317
+ add('upper', upper);
318
+ add('whitespace', whitespace);
319
+ out.push(compound('lower', [listFromItems([...ch.toLowerCase()].map(atom))]));
320
+ out.push(compound('upper', [listFromItems([...ch.toUpperCase()].map(atom))]));
321
+ return out;
322
+ }
323
+
324
+ function validCharType(term) {
325
+ return (term.type === ATOM && charTypeNames.has(term.name)) ||
326
+ (term.type === COMPOUND && ['lower', 'upper'].includes(term.name) && term.arity === 1);
327
+ }
328
+
329
+ function charTypeBuiltin(context) {
330
+ const state = { pending: false };
331
+ const iterator = charTypeSolutions(context, state);
332
+ iterator.hasPendingAlternatives = () => state.pending;
333
+ return iterator;
334
+ }
335
+
336
+ function* charTypeSolutions({ goal, env }, state) {
337
+ const char = deref(goal.args[0], env);
338
+ const type = deref(goal.args[1], env);
339
+ if (char.type === VAR && type.type === VAR) throw new PrologError('instantiation_error');
340
+ if (char.type !== VAR && (char.type !== ATOM || [...char.name].length !== 1)) {
341
+ throw new PrologError('type_error(character)', char);
342
+ }
343
+ if (type.type !== VAR && !validCharType(type)) throw new PrologError('domain_error(char_type)', type);
344
+
345
+ const firstCode = char.type === VAR ? 0 : char.name.codePointAt(0);
346
+ const lastCode = char.type === VAR ? 0x10ffff : firstCode;
347
+ for (let code = firstCode; code <= lastCode; code++) {
348
+ if (code >= 0xd800 && code <= 0xdfff) continue;
349
+ const ch = String.fromCodePoint(code);
350
+ const candidates = charTypeCandidates(ch).filter((candidate) =>
351
+ type.type === VAR || (candidate.type === type.type && candidate.name === type.name));
352
+ for (let index = 0; index < candidates.length; index++) {
353
+ const candidate = candidates[index];
354
+ const next = env.clone();
355
+ if (unify(goal.args[0], atom(ch), next) && unify(goal.args[1], candidate, next)) {
356
+ state.pending = index + 1 < candidates.length || code < lastCode;
357
+ yield next;
358
+ }
359
+ }
360
+ }
361
+ state.pending = false;
362
+ }
363
+
364
+ const shortMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
365
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
366
+ const longMonths = ['January', 'February', 'March', 'April', 'May', 'June',
367
+ 'July', 'August', 'September', 'October', 'November', 'December'];
368
+ const shortWeekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
369
+ const longWeekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
370
+
371
+
372
+ export const charsioHostBuiltins = {
373
+ register(registry) {
374
+ registry.add('eyeprolog__char_type', 2, charTypeBuiltin, { eyePrologLibrary: true });
375
+ registry.add('eyeprolog__read_from_chars', 2, readFromCharsBuiltin, { deterministic: true, eyePrologLibrary: true });
376
+ registry.add('eyeprolog__read_term_from_chars', 3, readTermFromCharsBuiltin, { deterministic: true, eyePrologLibrary: true });
377
+ registry.add('eyeprolog__write_term_to_chars', 3, writeTermToCharsBuiltin, { deterministic: true, eyePrologLibrary: true });
378
+ registry.add('eyeprolog__chars_base64', 3, charsBase64Builtin, { deterministic: true, eyePrologLibrary: true });
379
+ },
380
+ };