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 +1 -1
- package/package.json +1 -1
- package/src/io.js +32 -2
- package/src/iso.js +29 -13
- package/src/lib/sockets.pl +48 -0
- package/src/library-autoload-index.js +6 -0
- package/src/sockets-host.js +298 -0
- package/src/sockets-worker.js +235 -0
- package/src/standard-library.js +6 -1
- package/test/run-regression.mjs +57 -11
- package/test/scryer-library-exports.json +7 -0
- package/the-art-of-eyeprolog.md +425 -391
- package/tools/generate-predicate-reference.mjs +1 -1
- package/tools/predicate-reference.json +46 -1
- package/why-eyeprolog.md +2 -3
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
|
|
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
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
|
-
|
|
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
|
|
1158
|
-
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|