eyeprolog 1.5.19 → 1.5.20

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/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
- All 32 bundled modules that overlap Scryer's current `src/lib/` surface cover Scryer's exported predicates, and all 26 bundled modules with public-module counterparts in Trealla's current `library/` surface cover those Trealla exports as well. 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.
64
+ All 33 bundled modules that overlap Scryer's current `src/lib/` surface cover Scryer's exported predicates, and all 26 bundled modules with public-module counterparts in Trealla's current `library/` surface cover those Trealla exports as well. 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, the full Scryer `library(crypto)`, and Scryer-compatible TCP `library(sockets)`. Filesystem, OS, and TCP socket 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.19",
6
+ "version": "1.5.20",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/io.js CHANGED
@@ -112,7 +112,8 @@ export class StreamManager {
112
112
  });
113
113
  }
114
114
  flush(stream) {
115
- if (!stream || stream.standard || stream.mode === 'read') return;
115
+ if (!stream || stream.standard || (stream.mode === 'read' && stream.writable !== true)) return;
116
+ if (typeof stream.flush === 'function') { stream.flush(); return; }
116
117
  fs.writeFileSync(stream.path, stream.type === 'binary' ? BufferCtor.from(stream.content) : stream.content);
117
118
  }
118
119
  discard(stream) {
@@ -120,8 +121,33 @@ export class StreamManager {
120
121
  this.streams.delete(stream.id);
121
122
  }
122
123
  close(stream) {
123
- this.flush(stream);
124
+ if (typeof stream.closeTransport !== 'function') {
125
+ this.flush(stream);
126
+ this.discard(stream);
127
+ return;
128
+ }
129
+ let error = null;
130
+ try { this.flush(stream); } catch (caught) { error = caught; }
131
+ try { stream.closeTransport(); } catch (caught) { error ??= caught; }
124
132
  this.discard(stream);
133
+ if (error != null) throw error;
134
+ }
135
+ refill(stream) {
136
+ if (typeof stream?.interactiveReadUnit !== 'function') return false;
137
+ if (stream.continuousRefill === true && stream.position >= stream.content.length && stream.position > 0) {
138
+ stream.content = stream.type === 'binary' ? [] : '';
139
+ stream.position = 0;
140
+ }
141
+ const value = stream.interactiveReadUnit();
142
+ if (value == null) return false;
143
+ if (stream.type === 'binary') {
144
+ const incoming = Array.isArray(value) ? value : [...value];
145
+ stream.content.push(...incoming);
146
+ } else {
147
+ stream.content += String(value);
148
+ }
149
+ stream.pastEnd = false;
150
+ return true;
125
151
  }
126
152
  readUnit(stream, peek = false) {
127
153
  if (stream.position >= stream.content.length) return null;
@@ -142,6 +168,10 @@ export class StreamManager {
142
168
  return value;
143
169
  }
144
170
  writeUnit(stream, value) {
171
+ if (typeof stream.writeUnit === 'function') {
172
+ stream.writeUnit(value);
173
+ return;
174
+ }
145
175
  if (stream.standard && stream.write) {
146
176
  stream.write(String(value));
147
177
  return;
package/src/iso.js CHANGED
@@ -1154,8 +1154,12 @@ function requireStream(solver, term, env, mode = null) {
1154
1154
  const culprit = deref(term, env);
1155
1155
  const stream = solver.io.resolve(streamReference(term, env));
1156
1156
  if (!stream) throw new PrologError('existence_error(stream)', culprit);
1157
- if (mode && stream.mode !== mode && !(mode === 'write' && stream.mode === 'append')) {
1158
- throw new PrologError(`permission_error(${mode === 'read' ? 'input' : 'output'}, stream)`, culprit);
1157
+ if (mode) {
1158
+ const readable = stream.readable ?? stream.mode === 'read';
1159
+ const writable = stream.writable ?? (stream.mode === 'write' || stream.mode === 'append');
1160
+ if ((mode === 'read' && !readable) || (mode === 'write' && !writable)) {
1161
+ throw new PrologError(`permission_error(${mode === 'read' ? 'input' : 'output'}, stream)`, culprit);
1162
+ }
1159
1163
  }
1160
1164
  return stream;
1161
1165
  }
@@ -1358,18 +1362,27 @@ function* flushOutputBuiltin({ solver, goal, env }) {
1358
1362
  yield env;
1359
1363
  }
1360
1364
 
1365
+ function streamEndState(stream) {
1366
+ if (stream.pastEnd) return 'past';
1367
+ if (stream.position < stream.content.length) return 'not';
1368
+ if (stream.remoteEnded === true) return 'at';
1369
+ if (typeof stream.interactiveReadUnit === 'function') return 'not';
1370
+ return 'at';
1371
+ }
1372
+
1361
1373
  function streamProperties(stream) {
1362
1374
  const properties = [
1363
1375
  compound('mode', [atom(stream.mode)]),
1364
1376
  compound('type', [atom(stream.type)]),
1365
1377
  compound('reposition', [atom(stream.reposition ? 'true' : 'false')]),
1366
1378
  compound('eof_action', [atom(stream.eofAction)]),
1367
- compound('position', [numberTerm(stream.position)]),
1379
+ compound('position', [numberTerm(stream.reportedPosition ?? stream.position)]),
1368
1380
  ];
1369
- properties.push(atom(stream.mode === 'read' ? 'input' : 'output'));
1370
- properties.push(compound('end_of_stream', [
1371
- atom(stream.pastEnd ? 'past' : stream.position >= stream.content.length ? 'at' : 'not'),
1372
- ]));
1381
+ const readable = stream.readable ?? stream.mode === 'read';
1382
+ const writable = stream.writable ?? (stream.mode === 'write' || stream.mode === 'append');
1383
+ if (readable) properties.push(atom('input'));
1384
+ if (writable) properties.push(atom('output'));
1385
+ properties.push(compound('end_of_stream', [atom(streamEndState(stream))]));
1373
1386
  if (stream.alias) properties.push(compound('alias', [atom(stream.alias)]));
1374
1387
  if (stream.path) properties.push(compound('file_name', [atom(stream.path)]));
1375
1388
  return properties;
@@ -1461,7 +1474,7 @@ function* atEndBuiltin({ solver, goal, env }) {
1461
1474
  // stream property and therefore accepts any open stream-or-alias; it is not
1462
1475
  // an input-only operation.
1463
1476
  const stream = goal.arity === 0 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env);
1464
- if (stream.pastEnd || stream.position >= stream.content.length) yield env;
1477
+ if (streamEndState(stream) !== 'not') yield env;
1465
1478
  }
1466
1479
  function validStrictInputCharacterCode(value, solver) {
1467
1480
  if (value.type !== NUMBER || !isDecimalInteger(value.name)) return false;
@@ -1521,11 +1534,7 @@ function inputUnitBuiltin(name) {
1521
1534
  }
1522
1535
  const peek = name.startsWith('peek');
1523
1536
  if (stream.position >= stream.content.length && typeof stream.interactiveReadUnit === 'function') {
1524
- const text = stream.interactiveReadUnit();
1525
- if (text != null) {
1526
- stream.content += String(text);
1527
- stream.pastEnd = false;
1528
- }
1537
+ solver.io.refill(stream);
1529
1538
  }
1530
1539
  let unit;
1531
1540
  try {
@@ -1829,6 +1838,13 @@ function readTermFromStream(stream, solver) {
1829
1838
  throw error;
1830
1839
  }
1831
1840
 
1841
+ // Streams backed by a synchronous refill hook (for example TCP sockets)
1842
+ // may need more input before the current term becomes complete. Refill and
1843
+ // rescan until a complete term is available or the underlying stream ends.
1844
+ if (stream.continuousRefill === true && typeof stream.interactiveReadUnit === 'function' && stream.remoteEnded !== true) {
1845
+ if (solver.io.refill(stream)) continue;
1846
+ }
1847
+
1832
1848
  // The interactive top level may attach a synchronous reader to the
1833
1849
  // standard user_input stream. Ask it for one complete read-term only when
1834
1850
  // this read operation actually reaches the end of buffered input. This is
@@ -0,0 +1,48 @@
1
+ /** TCP sockets represented as ordinary Prolog streams. */
2
+
3
+ :- module(sockets, [
4
+ socket_client_open/3,
5
+ socket_server_open/2,
6
+ socket_server_accept/4,
7
+ socket_server_close/1,
8
+ current_hostname/1
9
+ ]).
10
+
11
+ :- use_module(library(error)).
12
+
13
+ socket_client_open(Addr, Stream, Options) :-
14
+ ( var(Addr) ->
15
+ throw(error(instantiation_error, socket_client_open/3))
16
+ ; true
17
+ ),
18
+ must_be(var, Stream),
19
+ must_be(list, Options),
20
+ ( Addr = Address:Port,
21
+ atom(Address),
22
+ ( atom(Port) ; integer(Port) ) ->
23
+ true
24
+ ; throw(error(type_error(socket_address, Addr), socket_client_open/3))
25
+ ),
26
+ eyeprolog__socket_client_open(Address, Port, Stream, Options).
27
+
28
+ socket_server_open(Addr, ServerSocket) :-
29
+ must_be(var, ServerSocket),
30
+ ( ( integer(Addr) ; var(Addr) ) ->
31
+ eyeprolog__socket_server_open([], Addr, ServerSocket)
32
+ ; Addr = Address:Port,
33
+ must_be(atom, Address),
34
+ can_be(integer, Port),
35
+ eyeprolog__socket_server_open(Address, Port, ServerSocket)
36
+ ).
37
+
38
+ socket_server_accept(ServerSocket, Client, Stream, Options) :-
39
+ must_be(var, Client),
40
+ must_be(var, Stream),
41
+ must_be(list, Options),
42
+ eyeprolog__socket_server_accept(ServerSocket, Client, Stream, Options).
43
+
44
+ socket_server_close(ServerSocket) :-
45
+ eyeprolog__socket_server_close(ServerSocket).
46
+
47
+ current_hostname(HostName) :-
48
+ eyeprolog__current_hostname(HostName).
@@ -111,6 +111,7 @@ export const eyePrologLibraryAutoload = Object.freeze({
111
111
  "crypto_password_hash/3": "crypto",
112
112
  "cumulative/1": "clpz",
113
113
  "cumulative/2": "clpz",
114
+ "current_hostname/1": "sockets",
114
115
  "current_time/1": "time",
115
116
  "curve25519_generator/1": "crypto",
116
117
  "curve25519_scalar_mult/3": "crypto",
@@ -339,6 +340,10 @@ export const eyePrologLibraryAutoload = Object.freeze({
339
340
  "sleep/1": "time",
340
341
  "slice/4": "lists",
341
342
  "smallest_divisor_from/3": "primes",
343
+ "socket_client_open/3": "sockets",
344
+ "socket_server_accept/4": "sockets",
345
+ "socket_server_close/1": "sockets",
346
+ "socket_server_open/2": "sockets",
342
347
  "split/3": "strings",
343
348
  "stable/1": "eyelet",
344
349
  "start_tabling/2": "tabling",
@@ -430,6 +435,7 @@ export const eyePrologLibraryAutoloadModules = Object.freeze([
430
435
  "random",
431
436
  "reif",
432
437
  "si",
438
+ "sockets",
433
439
  "strings",
434
440
  "tabling",
435
441
  "terms",
@@ -0,0 +1,298 @@
1
+ // Node TCP services for Scryer-compatible library(sockets).
2
+ // The public Prolog module stays synchronous; a worker owns Node's asynchronous
3
+ // sockets and the host adapter bridges each operation into the existing stream API.
4
+
5
+ import { isNode } from './platform.js';
6
+ import { PrologError } from './errors.js';
7
+ import {
8
+ ATOM, COMPOUND, NUMBER, VAR, atom, compound, copyResolved, deref, numberTerm,
9
+ properListItems, unify,
10
+ } from './term.js';
11
+
12
+ let WorkerCtor = null;
13
+ if (isNode) ({ Worker: WorkerCtor } = await import('node:worker_threads'));
14
+
15
+ const RPC_BYTES = 1024 * 1024;
16
+ const HEADER_WORDS = 4;
17
+ const WRITE_CHUNK_BYTES = 256 * 1024;
18
+ let bridge = null;
19
+
20
+ class SocketBridge {
21
+ constructor() {
22
+ this.shared = new SharedArrayBuffer(HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT + RPC_BYTES);
23
+ this.header = new Int32Array(this.shared, 0, HEADER_WORDS);
24
+ this.bytes = new Uint8Array(this.shared, HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT);
25
+ this.encoder = new TextEncoder();
26
+ this.decoder = new TextDecoder();
27
+ this.worker = new WorkerCtor(new URL('./sockets-worker.js', import.meta.url), {
28
+ type: 'module', workerData: { shared: this.shared },
29
+ execArgv: typeof process !== 'undefined' ? process.execArgv.filter((arg) => !arg.startsWith('--input-type')) : [],
30
+ });
31
+ this.worker.unref();
32
+ const ready = Atomics.wait(this.header, 0, 0, 5000);
33
+ if (ready === 'timed-out' || Atomics.load(this.header, 0) !== -1) {
34
+ this.worker.terminate();
35
+ throw new PrologError('resource_error(sockets)');
36
+ }
37
+ Atomics.store(this.header, 0, 0);
38
+ }
39
+
40
+ rpc(request) {
41
+ const encoded = this.encoder.encode(JSON.stringify(request));
42
+ if (encoded.length > this.bytes.length) throw new PrologError('resource_error(socket_message)');
43
+ this.bytes.set(encoded, 0);
44
+ Atomics.store(this.header, 1, encoded.length);
45
+ Atomics.store(this.header, 2, 0);
46
+ Atomics.store(this.header, 0, 1);
47
+ this.worker.postMessage(1);
48
+ Atomics.wait(this.header, 0, 1);
49
+ const responseLength = Atomics.load(this.header, 2);
50
+ const response = JSON.parse(this.decoder.decode(this.bytes.subarray(0, responseLength)));
51
+ Atomics.store(this.header, 0, 0);
52
+ if (!response.ok) {
53
+ const error = new Error(response.error?.message ?? 'socket error');
54
+ error.code = response.error?.code ?? 'EUNKNOWN';
55
+ throw error;
56
+ }
57
+ return response.result;
58
+ }
59
+ }
60
+
61
+ function socketBridge() {
62
+ if (!isNode || WorkerCtor == null || typeof SharedArrayBuffer === 'undefined' || typeof Atomics?.wait !== 'function') {
63
+ throw new PrologError('resource_error(sockets)');
64
+ }
65
+ bridge ??= new SocketBridge();
66
+ return bridge;
67
+ }
68
+
69
+ function socketSystemError(error, operation, culprit = null) {
70
+ if (error instanceof PrologError) return error;
71
+ if (error?.code === 'EACCES' || error?.code === 'EPERM') {
72
+ return new PrologError('permission_error(open, source_sink)', culprit);
73
+ }
74
+ if (error?.code === 'EBADF') return new PrologError('existence_error(socket)', culprit);
75
+ if (['ECONNREFUSED', 'EHOSTUNREACH', 'ENETUNREACH', 'ENOTFOUND', 'EADDRNOTAVAIL'].includes(error?.code)) {
76
+ return new PrologError('existence_error(source_sink)', culprit);
77
+ }
78
+ if (error?.code === 'EADDRINUSE') return new PrologError('permission_error(open, source_sink)', culprit);
79
+ return new PrologError(`resource_error(${operation})`, culprit);
80
+ }
81
+
82
+ function optionsList(term, env) {
83
+ const value = deref(term, env);
84
+ if (value.type === VAR) throw new PrologError('instantiation_error');
85
+ const items = properListItems(value, env);
86
+ if (items == null) throw new PrologError('type_error(list)', copyResolved(value, env));
87
+ return items.map((item) => deref(item, env));
88
+ }
89
+
90
+ function parseSocketOptions(term, env) {
91
+ const result = { alias: null, eofAction: 'error', reposition: false, type: 'text' };
92
+ for (const option of optionsList(term, env)) {
93
+ if (option.type === VAR) throw new PrologError('instantiation_error');
94
+ if (option.type !== COMPOUND || option.arity !== 1) throw new PrologError('domain_error(stream_option)', copyResolved(option, env));
95
+ const value = deref(option.args[0], env);
96
+ if (value.type === VAR) throw new PrologError('instantiation_error');
97
+ if (option.name === 'alias') {
98
+ if (value.type !== ATOM) throw new PrologError('domain_error(stream_option)', copyResolved(option, env));
99
+ result.alias = value.name;
100
+ } else if (option.name === 'eof_action') {
101
+ if (value.type !== ATOM || !['error', 'eof_code', 'reset'].includes(value.name)) {
102
+ throw new PrologError('domain_error(stream_option)', copyResolved(option, env));
103
+ }
104
+ result.eofAction = value.name;
105
+ } else if (option.name === 'reposition') {
106
+ if (value.type !== ATOM || !['true', 'false'].includes(value.name)) {
107
+ throw new PrologError('domain_error(stream_option)', copyResolved(option, env));
108
+ }
109
+ result.reposition = value.name === 'true';
110
+ } else if (option.name === 'type') {
111
+ if (value.type !== ATOM || !['text', 'binary'].includes(value.name)) {
112
+ throw new PrologError('domain_error(stream_option)', copyResolved(option, env));
113
+ }
114
+ result.type = value.name;
115
+ } else {
116
+ throw new PrologError('domain_error(stream_option)', copyResolved(option, env));
117
+ }
118
+ }
119
+ return result;
120
+ }
121
+
122
+ function hostText(term, env, { allowEmptyList = false } = {}) {
123
+ const value = deref(term, env);
124
+ if (allowEmptyList && value.type === ATOM && value.name === '[]') return '127.0.0.1';
125
+ if (value.type !== ATOM) throw new PrologError('type_error(atom)', copyResolved(value, env));
126
+ return value.name;
127
+ }
128
+
129
+ function portValue(term, env, { allowVariable = false } = {}) {
130
+ const value = deref(term, env);
131
+ if (allowVariable && value.type === VAR) return { port: 0, variable: true };
132
+ if (value.type === NUMBER && /^\d+$/.test(value.name)) {
133
+ const port = Number(value.name);
134
+ if (Number.isInteger(port) && port >= 0 && port <= 65535) return { port, variable: false };
135
+ }
136
+ if (!allowVariable && value.type === ATOM && /^\d+$/.test(value.name)) {
137
+ const port = Number(value.name);
138
+ if (port <= 65535) return { port, variable: false };
139
+ }
140
+ throw new PrologError('type_error(integer)', copyResolved(value, env));
141
+ }
142
+
143
+ function serverHandle(id) {
144
+ return compound('$socket_server', [numberTerm(id)]);
145
+ }
146
+
147
+ function serverId(term, env) {
148
+ const value = deref(term, env);
149
+ if (value.type === VAR) throw new PrologError('instantiation_error');
150
+ if (value.type === COMPOUND && value.name === '$socket_server' && value.arity === 1) {
151
+ const id = deref(value.args[0], env);
152
+ if (id.type === NUMBER && /^\d+$/.test(id.name)) return Number(id.name);
153
+ }
154
+ throw new PrologError('type_error(socket)', copyResolved(value, env));
155
+ }
156
+
157
+ function base64(bytes) {
158
+ return Buffer.from(bytes).toString('base64');
159
+ }
160
+
161
+ function bytesFromBase64(text) {
162
+ return Uint8Array.from(Buffer.from(text, 'base64'));
163
+ }
164
+
165
+ function addSocketStream(solver, connectionId, options, fileName = '') {
166
+ if (options.reposition) throw new PrologError('permission_error(reposition, stream)');
167
+ if (options.alias && solver.io.resolve(options.alias)) {
168
+ throw new PrologError('permission_error(open, source_sink)', compound('alias', [atom(options.alias)]));
169
+ }
170
+ const worker = socketBridge();
171
+ const textDecoder = options.type === 'text' ? new TextDecoder('utf-8', { fatal: true }) : null;
172
+ const stream = {
173
+ id: solver.io.nextId++, alias: options.alias, mode: 'read_append', type: options.type,
174
+ content: options.type === 'binary' ? [] : '', position: 0, reportedPosition: 0, path: fileName,
175
+ reposition: false, eofAction: options.eofAction, standard: false, pastEnd: false,
176
+ readable: true, writable: true, remoteEnded: false, continuousRefill: true,
177
+ socketConnectionId: connectionId,
178
+ socketOutput: options.type === 'binary' ? [] : '',
179
+ };
180
+ stream.interactiveReadUnit = () => {
181
+ while (true) {
182
+ let result;
183
+ try { result = worker.rpc({ op: 'read', connectionId, maxBytes: 64 * 1024 }); }
184
+ catch (error) { throw socketSystemError(error, 'socket_read'); }
185
+ if (result.eof) {
186
+ stream.remoteEnded = true;
187
+ if (stream.type === 'text') {
188
+ try {
189
+ const tail = textDecoder.decode();
190
+ if (tail) return tail;
191
+ } catch (_) {
192
+ throw new PrologError('representation_error(character)');
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+ const bytes = bytesFromBase64(result.data);
198
+ if (stream.type === 'binary') return [...bytes];
199
+ let text;
200
+ try { text = textDecoder.decode(bytes, { stream: true }); }
201
+ catch (_) { throw new PrologError('representation_error(character)'); }
202
+ if (text.length > 0) return text;
203
+ }
204
+ };
205
+ stream.writeUnit = (value) => {
206
+ if (stream.type === 'binary') stream.socketOutput.push(value);
207
+ else stream.socketOutput += String(value);
208
+ };
209
+ stream.flush = () => {
210
+ const data = stream.type === 'binary'
211
+ ? Uint8Array.from(stream.socketOutput)
212
+ : new TextEncoder().encode(stream.socketOutput);
213
+ if (data.length === 0) return;
214
+ for (let offset = 0; offset < data.length; offset += WRITE_CHUNK_BYTES) {
215
+ const chunk = data.subarray(offset, Math.min(data.length, offset + WRITE_CHUNK_BYTES));
216
+ try { worker.rpc({ op: 'write', connectionId, data: base64(chunk) }); }
217
+ catch (error) { throw socketSystemError(error, 'socket_write'); }
218
+ }
219
+ stream.socketOutput = stream.type === 'binary' ? [] : '';
220
+ };
221
+ stream.closeTransport = () => {
222
+ try { worker.rpc({ op: 'close', connectionId }); }
223
+ catch (error) { throw socketSystemError(error, 'socket_close'); }
224
+ };
225
+ solver.io.add(stream);
226
+ return stream;
227
+ }
228
+
229
+ function streamHandle(id) {
230
+ return compound('$stream', [numberTerm(id)]);
231
+ }
232
+
233
+ function* socketClientOpenBuiltin({ solver, goal, env }) {
234
+ const host = hostText(goal.args[0], env);
235
+ const { port } = portValue(goal.args[1], env);
236
+ const options = parseSocketOptions(goal.args[3], env);
237
+ if (options.reposition) throw new PrologError('permission_error(reposition, stream)', copyResolved(goal.args[3], env));
238
+ let result;
239
+ try { result = socketBridge().rpc({ op: 'client_open', host, port }); }
240
+ catch (error) { throw socketSystemError(error, 'socket_client_open', copyResolved(goal.args[0], env)); }
241
+ const stream = addSocketStream(solver, result.connectionId, options, `${host}:${port}`);
242
+ const next = env.clone();
243
+ if (unify(goal.args[2], streamHandle(stream.id), next)) yield next;
244
+ else solver.io.close(stream);
245
+ }
246
+
247
+ function* socketServerOpenBuiltin({ goal, env }) {
248
+ const host = hostText(goal.args[0], env, { allowEmptyList: true });
249
+ const { port, variable } = portValue(goal.args[1], env, { allowVariable: true });
250
+ let result;
251
+ try { result = socketBridge().rpc({ op: 'server_open', host, port }); }
252
+ catch (error) { throw socketSystemError(error, 'socket_server_open', copyResolved(goal.args[0], env)); }
253
+ const next = env.clone();
254
+ if (!unify(goal.args[2], serverHandle(result.serverId), next) ||
255
+ (variable && !unify(goal.args[1], numberTerm(result.port), next))) {
256
+ try { socketBridge().rpc({ op: 'server_close', serverId: result.serverId }); } catch (_) { /* best effort */ }
257
+ return;
258
+ }
259
+ yield next;
260
+ }
261
+
262
+ function* socketServerAcceptBuiltin({ solver, goal, env }) {
263
+ const id = serverId(goal.args[0], env);
264
+ const options = parseSocketOptions(goal.args[3], env);
265
+ if (options.reposition) throw new PrologError('permission_error(reposition, stream)', copyResolved(goal.args[3], env));
266
+ let result;
267
+ try { result = socketBridge().rpc({ op: 'server_accept', serverId: id }); }
268
+ catch (error) { throw socketSystemError(error, 'socket_server_accept', copyResolved(goal.args[0], env)); }
269
+ const stream = addSocketStream(solver, result.connectionId, options, result.client);
270
+ const next = env.clone();
271
+ if (unify(goal.args[1], atom(result.client), next) && unify(goal.args[2], streamHandle(stream.id), next)) yield next;
272
+ else solver.io.close(stream);
273
+ }
274
+
275
+ function* socketServerCloseBuiltin({ goal, env }) {
276
+ const id = serverId(goal.args[0], env);
277
+ try { socketBridge().rpc({ op: 'server_close', serverId: id }); }
278
+ catch (error) { throw socketSystemError(error, 'socket_server_close', copyResolved(goal.args[0], env)); }
279
+ yield env;
280
+ }
281
+
282
+ function* currentHostnameBuiltin({ goal, env }) {
283
+ let result;
284
+ try { result = socketBridge().rpc({ op: 'hostname' }); }
285
+ catch (error) { throw socketSystemError(error, 'current_hostname'); }
286
+ const next = env.clone();
287
+ if (unify(goal.args[0], atom(result.hostname), next)) yield next;
288
+ }
289
+
290
+ export const socketsHostBuiltins = {
291
+ register(registry) {
292
+ registry.add('eyeprolog__socket_client_open', 4, socketClientOpenBuiltin, { deterministic: true, eyePrologLibrary: true });
293
+ registry.add('eyeprolog__socket_server_open', 3, socketServerOpenBuiltin, { deterministic: true, eyePrologLibrary: true });
294
+ registry.add('eyeprolog__socket_server_accept', 4, socketServerAcceptBuiltin, { deterministic: true, eyePrologLibrary: true });
295
+ registry.add('eyeprolog__socket_server_close', 1, socketServerCloseBuiltin, { deterministic: true, eyePrologLibrary: true });
296
+ registry.add('eyeprolog__current_hostname', 1, currentHostnameBuiltin, { deterministic: true, eyePrologLibrary: true });
297
+ },
298
+ };