eyeprolog 1.5.39 → 1.5.40

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,8 +61,8 @@ Normal mode executes top-level `Conclusion :+ Premise` rules through the bundled
61
61
  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.
62
62
  All 34 bundled modules that overlap Scryer's current library surface cover Scryer's exported predicates, and all 28 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)`, Scryer-compatible TCP `library(sockets)`, the shared Scryer/Trealla `library(json)` DCG, and a merged Scryer/Trealla `library(http)` client/server facade. Filesystem, OS, TCP socket, and HTTP/HTTPS client 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.
63
63
  ## HTTP and JSON
64
- `library(http)` merges Scryer-style `http_open/3` options with Trealla's request helpers (`http_get/3`, `http_post/4`, `http_patch/4`, `http_put/4`, `http_delete/3`, `http_server/2`, `http_request/5`). HTTP/HTTPS clients use the Node-owned `src/http-host.js` bridge, follow up to five redirects, accept Trealla `header(Name,Value)` and `host/path` address-list forms, and expose response bodies as UTF-8 character lists or streams.
65
- `library(json)` provides the shared bidirectional `json_chars//1` DCG with `pairs/1`, `list/1`, `string/1`, `number/1`, `boolean/1`, and `null`. See `examples/json.pl`, `examples/http-client.pl`, `test/run-http-json.mjs`, and the full contracts in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
64
+ `library(http)` merges Scryer-style `http_open/3` options with Trealla's request helpers (`http_get/3`, `http_post/4`, `http_patch/4`, `http_put/4`, `http_delete/3`, `http_server/2`, `http_request/5`). HTTP/HTTPS clients use the Node-owned `src/http-host.js` bridge, follow up to five redirects, preserve repeated request-header values, accept Trealla `header(Name,Value)` and `host/path` address-list forms, and expose response bodies as UTF-8 character lists or streams. `http_open/3` reads response bodies lazily in bounded chunks rather than buffering the whole response through the host RPC bridge; request metadata and `data/1` still pass through that bounded bridge.
65
+ `library(json)` provides the shared bidirectional `json_chars//1` DCG with `pairs/1`, `list/1`, `string/1`, `number/1`, `boolean/1`, and `null`. JSON `\u` escapes combine and generate UTF-16 surrogate pairs for supplementary Unicode scalar values, while unpaired surrogates are rejected. See `examples/json.pl`, `examples/http-client.pl`, `test/run-http-json.mjs`, and the full contracts in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
66
66
  ## Links
67
67
 
68
68
  - [The Art of EyeProlog](https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog) — complete reference
package/examples/json.pl CHANGED
@@ -2,6 +2,7 @@
2
2
  %
3
3
  % Objects are pairs([...]), arrays are list([...]), strings are string(Chars),
4
4
  % numbers are number(N), booleans are boolean(true/false), and null is null.
5
+ % Supplementary Unicode JSON escapes are combined from UTF-16 surrogate pairs.
5
6
 
6
7
  :- use_module(library(json)).
7
8
  :- use_module(library(dcgs)).
@@ -9,7 +10,7 @@
9
10
  %% goal: json_example(Mode, Value)
10
11
 
11
12
  json_example(parsed, JSON) :-
12
- phrase(json_chars(JSON), "{\"name\":\"Ada\",\"active\":true,\"scores\":[3,5,8]}").
13
+ phrase(json_chars(JSON), "{\"name\":\"Ada\",\"active\":true,\"scores\":[3,5,8],\"emoji\":\"\\uD83D\\uDE00\"}").
13
14
 
14
15
  json_example(generated, Chars) :-
15
16
  once(phrase(json_chars(pairs([
@@ -1,2 +1,2 @@
1
- json_example(parsed, pairs([string("name") - string("Ada"), string("active") - boolean(true), string("scores") - list([number(3), number(5), number(8)])])).
1
+ json_example(parsed, pairs([string("name") - string("Ada"), string("active") - boolean(true), string("scores") - list([number(3), number(5), number(8)]), string("emoji") - string("😀")])).
2
2
  json_example(generated, "{\"project\":\"EyeProlog\",\"ok\":true}").
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.39",
6
+ "version": "1.5.40",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -105,4 +105,4 @@ and are intentionally not versioned.
105
105
 
106
106
  ## HTTP and JSON library ownership
107
107
 
108
- `src/lib/json.pl` is a pure Prolog DCG adapted from the BSD-licensed Scryer/Trealla JSON library. `src/lib/http.pl` owns HTTP option normalization, convenience predicates, request parsing, and the small server facade. Node HTTP/HTTPS client I/O is isolated in `src/http-host.js` and `src/http-worker.js`, following the same module-owned host-adapter convention as sockets, files, crypto, and other runtime-dependent libraries.
108
+ `src/lib/json.pl` is a pure Prolog DCG adapted from the BSD-licensed Scryer/Trealla JSON library, with Unicode-scalar handling for JSON UTF-16 surrogate escapes. `src/lib/http.pl` owns HTTP option normalization, convenience predicates, request parsing, and the small server facade. Node HTTP/HTTPS client I/O is isolated in `src/http-host.js` and `src/http-worker.js`, following the same module-owned host-adapter convention as sockets, files, crypto, and other runtime-dependent libraries. The worker returns response metadata first and retains the response body behind a body id; the host stream pulls bounded chunks through the bridge on demand, so response size is no longer bounded by the RPC buffer and early `close/1` can discard the remaining transport body.
package/src/http-host.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Node HTTP(S) bridge for library(http). The Prolog module owns option parsing,
2
- // response shaping, and server-side HTTP parsing; this host adapter only performs
3
- // the asynchronous client exchange and exposes the body as an EyeProlog stream.
2
+ // response shaping, and server-side HTTP parsing; this host adapter performs the
3
+ // asynchronous client exchange and exposes each response body as a lazy stream.
4
4
 
5
5
  import { isNode } from './platform.js';
6
6
  import { PrologError } from './errors.js';
@@ -14,6 +14,7 @@ let WorkerCtor = null;
14
14
  if (isNode) ({ Worker: WorkerCtor } = await import('node:worker_threads'));
15
15
 
16
16
  const RPC_BYTES = 8 * 1024 * 1024;
17
+ const HTTP_READ_BYTES = 64 * 1024;
17
18
  const HEADER_WORDS = 4;
18
19
  let bridge = null;
19
20
 
@@ -75,6 +76,15 @@ function textValue(term, env, { allowNumber = false } = {}) {
75
76
  return characterListText(term, env);
76
77
  }
77
78
 
79
+ function requestBodyValue(term, env) {
80
+ const value = deref(term, env);
81
+ if (value.type === ATOM && value.name === 'no_data') return null;
82
+ if (value.type === COMPOUND && value.name === 'data' && value.arity === 1) {
83
+ return textValue(value.args[0], env);
84
+ }
85
+ throw new PrologError('domain_error(http_data)', copyResolved(term, env));
86
+ }
87
+
78
88
  function requestHeadersValue(term, env) {
79
89
  const items = properListItems(deref(term, env), env);
80
90
  if (items == null) throw new PrologError('type_error(list)', copyResolved(term, env));
@@ -89,11 +99,35 @@ function requestHeadersValue(term, env) {
89
99
 
90
100
  function streamHandle(id) { return compound('$stream', [numberTerm(id)]); }
91
101
 
92
- function addBodyStream(solver, body, finalUrl) {
102
+ function bytesFromBase64(text) {
103
+ return Uint8Array.from(Buffer.from(text, 'base64'));
104
+ }
105
+
106
+ function addBodyStream(solver, bodyId, finalUrl) {
107
+ const worker = httpBridge();
108
+ // Buffer.toString('utf8'), used by the original eager bridge, replaced malformed
109
+ // input with U+FFFD. Keep that behavior while decoding incrementally.
110
+ const decoder = new TextDecoder('utf-8');
93
111
  const stream = {
94
- id: solver.io.nextId++, alias: null, mode: 'read', type: 'text', content: String(body),
112
+ id: solver.io.nextId++, alias: null, mode: 'read', type: 'text', content: '',
95
113
  position: 0, reportedPosition: 0, path: String(finalUrl ?? ''), reposition: false,
96
114
  eofAction: 'eof_code', standard: false, pastEnd: false, readable: true, writable: false,
115
+ remoteEnded: false, continuousRefill: true, httpBodyId: bodyId,
116
+ };
117
+ stream.interactiveReadUnit = () => {
118
+ while (true) {
119
+ const result = worker.rpc({ op: 'body_read', bodyId, maxBytes: HTTP_READ_BYTES });
120
+ if (result.eof) {
121
+ stream.remoteEnded = true;
122
+ const tail = decoder.decode();
123
+ return tail || null;
124
+ }
125
+ const text = decoder.decode(bytesFromBase64(result.data), { stream: true });
126
+ if (text.length > 0) return text;
127
+ }
128
+ };
129
+ stream.closeTransport = () => {
130
+ worker.rpc({ op: 'body_close', bodyId });
97
131
  };
98
132
  solver.io.add(stream);
99
133
  return stream;
@@ -102,10 +136,10 @@ function addBodyStream(solver, body, finalUrl) {
102
136
  function* httpOpenBuiltin({ solver, goal, env }) {
103
137
  const url = textValue(goal.args[0], env);
104
138
  const method = textValue(goal.args[2], env);
105
- const data = textValue(goal.args[3], env);
139
+ const data = requestBodyValue(goal.args[3], env);
106
140
  const requestHeaders = requestHeadersValue(goal.args[5], env);
107
141
  const result = httpBridge().rpc({ op: 'request', url, method, data, headers: requestHeaders, redirects: 5 });
108
- const stream = addBodyStream(solver, result.body, result.finalUrl);
142
+ const stream = addBodyStream(solver, result.bodyId, result.finalUrl);
109
143
  const headerTerms = result.headers.map(([name, value]) => compound('header', [atom(name), chars(value)]));
110
144
  const next = env.clone();
111
145
  if (unify(goal.args[1], streamHandle(stream.id), next) &&
@@ -7,6 +7,8 @@ const header = new Int32Array(workerData.shared, 0, HEADER_WORDS);
7
7
  const bytes = new Uint8Array(workerData.shared, HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT);
8
8
  const decoder = new TextDecoder();
9
9
  const encoder = new TextEncoder();
10
+ const bodies = new Map();
11
+ let nextBodyId = 1;
10
12
 
11
13
  function errorRecord(error) {
12
14
  return { code: String(error?.code ?? 'EUNKNOWN'), message: String(error?.message ?? error ?? 'http error') };
@@ -15,7 +17,7 @@ function errorRecord(error) {
15
17
  function writeResponse(response) {
16
18
  const encoded = encoder.encode(JSON.stringify(response));
17
19
  if (encoded.length > bytes.length) {
18
- const fallback = encoder.encode(JSON.stringify({ ok: false, error: { code: 'EMSGSIZE', message: 'HTTP response too large for bridge' } }));
20
+ const fallback = encoder.encode(JSON.stringify({ ok: false, error: { code: 'EMSGSIZE', message: 'HTTP bridge message too large' } }));
19
21
  bytes.set(fallback.subarray(0, bytes.length), 0);
20
22
  Atomics.store(header, 2, Math.min(fallback.length, bytes.length));
21
23
  } else {
@@ -26,6 +28,27 @@ function writeResponse(response) {
26
28
  Atomics.notify(header, 0, 1);
27
29
  }
28
30
 
31
+ function headerObject(requestHeaders) {
32
+ const headers = Object.create(null);
33
+ for (const [rawName, rawValue] of requestHeaders) {
34
+ const name = String(rawName).toLowerCase();
35
+ const value = String(rawValue);
36
+ const previous = headers[name];
37
+ if (previous == null) headers[name] = value;
38
+ else if (Array.isArray(previous)) previous.push(value);
39
+ else headers[name] = [previous, value];
40
+ }
41
+ return headers;
42
+ }
43
+
44
+ function responseHeaders(res) {
45
+ const rawHeaders = [];
46
+ for (let i = 0; i < res.rawHeaders.length; i += 2) {
47
+ rawHeaders.push([String(res.rawHeaders[i]).toLowerCase(), String(res.rawHeaders[i + 1] ?? '')]);
48
+ }
49
+ return rawHeaders;
50
+ }
51
+
29
52
  function requestOnce(url, method, data, requestHeaders) {
30
53
  return new Promise((resolve, reject) => {
31
54
  const target = new URL(url);
@@ -35,24 +58,14 @@ function requestOnce(url, method, data, requestHeaders) {
35
58
  return;
36
59
  }
37
60
  const body = data == null ? null : Buffer.from(data, 'utf8');
38
- const headers = Object.fromEntries(requestHeaders);
39
- if (body != null && !Object.keys(headers).some((name) => name.toLowerCase() === 'content-length')) {
40
- headers['content-length'] = String(body.length);
41
- }
61
+ const headers = headerObject(requestHeaders);
62
+ if (body != null && headers['content-length'] == null) headers['content-length'] = String(body.length);
42
63
  const req = transport.request(target, { method: method.toUpperCase(), headers }, (res) => {
43
- const chunks = [];
44
- res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
45
- res.on('end', () => {
46
- const rawHeaders = [];
47
- for (let i = 0; i < res.rawHeaders.length; i += 2) {
48
- rawHeaders.push([String(res.rawHeaders[i]).toLowerCase(), String(res.rawHeaders[i + 1] ?? '')]);
49
- }
50
- resolve({
51
- statusCode: Number(res.statusCode ?? 0),
52
- headers: rawHeaders,
53
- body: Buffer.concat(chunks).toString('utf8'),
54
- location: res.headers.location == null ? null : String(res.headers.location),
55
- });
64
+ resolve({
65
+ response: res,
66
+ statusCode: Number(res.statusCode ?? 0),
67
+ headers: responseHeaders(res),
68
+ location: res.headers.location == null ? null : String(res.headers.location),
56
69
  });
57
70
  });
58
71
  req.on('error', reject);
@@ -61,30 +74,92 @@ function requestOnce(url, method, data, requestHeaders) {
61
74
  });
62
75
  }
63
76
 
77
+ function discardResponse(response) {
78
+ return new Promise((resolve) => {
79
+ response.on('error', resolve);
80
+ response.on('end', resolve);
81
+ response.resume();
82
+ });
83
+ }
84
+
85
+ function withoutContentLength(headers) {
86
+ return headers.filter(([name]) => String(name).toLowerCase() !== 'content-length');
87
+ }
88
+
64
89
  async function requestFollowingRedirects(url, method, data, headers, redirects = 5) {
65
90
  let current = url;
66
91
  let currentMethod = method;
67
92
  let currentData = data;
93
+ let currentHeaders = headers;
68
94
  for (let count = 0; ; count++) {
69
- const response = await requestOnce(current, currentMethod, currentData, headers);
70
- if (![301, 302, 303, 307, 308].includes(response.statusCode) || response.location == null || count >= redirects) {
71
- return { ...response, finalUrl: current };
95
+ const result = await requestOnce(current, currentMethod, currentData, currentHeaders);
96
+ if (![301, 302, 303, 307, 308].includes(result.statusCode) || result.location == null || count >= redirects) {
97
+ const bodyId = nextBodyId++;
98
+ bodies.set(bodyId, { response: result.response, iterator: result.response[Symbol.asyncIterator](), remainder: null });
99
+ return { statusCode: result.statusCode, headers: result.headers, bodyId, finalUrl: current };
72
100
  }
73
- current = new URL(response.location, current).toString();
74
- if (response.statusCode === 303 || ((response.statusCode === 301 || response.statusCode === 302) && currentMethod.toLowerCase() === 'post')) {
101
+ await discardResponse(result.response);
102
+ current = new URL(result.location, current).toString();
103
+ if (result.statusCode === 303 || ((result.statusCode === 301 || result.statusCode === 302) && currentMethod.toLowerCase() === 'post')) {
75
104
  currentMethod = 'get';
76
- currentData = '';
105
+ currentData = null;
106
+ currentHeaders = withoutContentLength(currentHeaders);
77
107
  }
78
108
  }
79
109
  }
80
110
 
111
+ function encodeBodyChunk(state, chunk, maxBytes) {
112
+ const data = Buffer.from(chunk);
113
+ if (data.length <= maxBytes) return { data, remainder: null };
114
+ return { data: data.subarray(0, maxBytes), remainder: data.subarray(maxBytes) };
115
+ }
116
+
117
+ async function readBody(bodyId, maxBytes) {
118
+ const state = bodies.get(bodyId);
119
+ if (state == null) return { eof: true, data: '' };
120
+ const limit = Math.max(1, Math.min(Number(maxBytes) || 65536, 1024 * 1024));
121
+ if (state.remainder?.length) {
122
+ const { data, remainder } = encodeBodyChunk(state, state.remainder, limit);
123
+ state.remainder = remainder;
124
+ return { eof: false, data: data.toString('base64') };
125
+ }
126
+ const next = await state.iterator.next();
127
+ if (next.done) {
128
+ bodies.delete(bodyId);
129
+ return { eof: true, data: '' };
130
+ }
131
+ const { data, remainder } = encodeBodyChunk(state, next.value, limit);
132
+ state.remainder = remainder;
133
+ return { eof: false, data: data.toString('base64') };
134
+ }
135
+
136
+ function closeBody(bodyId) {
137
+ const state = bodies.get(bodyId);
138
+ if (state == null) return { closed: false };
139
+ bodies.delete(bodyId);
140
+ state.response.destroy();
141
+ return { closed: true };
142
+ }
143
+
144
+ async function dispatch(request) {
145
+ switch (request.op) {
146
+ case 'request':
147
+ return requestFollowingRedirects(request.url, request.method, request.data, request.headers, request.redirects ?? 5);
148
+ case 'body_read':
149
+ return readBody(request.bodyId, request.maxBytes);
150
+ case 'body_close':
151
+ return closeBody(request.bodyId);
152
+ default:
153
+ throw Object.assign(new Error(`unknown HTTP operation: ${request.op}`), { code: 'EINVAL' });
154
+ }
155
+ }
156
+
81
157
  parentPort.on('message', async () => {
82
158
  if (Atomics.load(header, 0) !== 1) return;
83
159
  try {
84
160
  const length = Atomics.load(header, 1);
85
161
  const request = JSON.parse(decoder.decode(bytes.subarray(0, length)));
86
- if (request.op !== 'request') throw Object.assign(new Error(`unknown HTTP operation: ${request.op}`), { code: 'EINVAL' });
87
- const result = await requestFollowingRedirects(request.url, request.method, request.data, request.headers, request.redirects ?? 5);
162
+ const result = await dispatch(request);
88
163
  writeResponse({ ok: true, result });
89
164
  } catch (error) {
90
165
  writeResponse({ ok: false, error: errorRecord(error) });
package/src/lib/http.pl CHANGED
@@ -28,9 +28,9 @@ http_open(Address0, Response, Options0) :-
28
28
  must_be(list, Options0),
29
29
  http__normalize_address(Address0, Options0, Address, Options),
30
30
  http__method(Options, Method),
31
- http__data(Options, Data),
31
+ http__data_spec(Options, DataSpec),
32
32
  http__request_headers(Options, RequestHeaders),
33
- eyeprolog__http_open(Address, Response, Method, Data, Code, RequestHeaders, RawHeaders, FinalUrl),
33
+ eyeprolog__http_open(Address, Response, Method, DataSpec, Code, RequestHeaders, RawHeaders, FinalUrl),
34
34
  http__scryer_headers(RawHeaders, Headers),
35
35
  http__bind_option(status_code, Code, Options),
36
36
  http__bind_option(headers, Headers, Options),
@@ -70,26 +70,29 @@ http__ensure_slash(['/'|Rest], ['/'|Rest]) :- !.
70
70
  http__ensure_slash(Path, ['/'|Path]).
71
71
 
72
72
  http_get(Address, Data, Options) :-
73
- http__request_data(Address, get, [], Data, Options).
73
+ http__request_data(Address, get, no_data, Data, Options).
74
74
 
75
75
  http_post(Address, PostData, Reply, Options) :-
76
- http__request_data(Address, post, PostData, Reply, Options).
76
+ http__checked_data(PostData, DataSpec),
77
+ http__request_data(Address, post, DataSpec, Reply, Options).
77
78
 
78
79
  http_patch(Address, PostData, Reply, Options) :-
79
- http__request_data(Address, patch, PostData, Reply, Options).
80
+ http__checked_data(PostData, DataSpec),
81
+ http__request_data(Address, patch, DataSpec, Reply, Options).
80
82
 
81
83
  http_put(Address, PostData, Reply, Options) :-
82
- http__request_data(Address, put, PostData, Reply, Options).
84
+ http__checked_data(PostData, DataSpec),
85
+ http__request_data(Address, put, DataSpec, Reply, Options).
83
86
 
84
87
  http_delete(Address, Data, Options) :-
85
- http__request_data(Address, delete, [], Data, Options).
88
+ http__request_data(Address, delete, no_data, Data, Options).
86
89
 
87
90
  http__request_data(Address, DefaultMethod, Payload, Data, Options) :-
88
91
  must_be(list, Options),
89
92
  ( memberchk(method(Method0), Options) -> http__valid_method(Method0), Method = Method0 ; Method = DefaultMethod ),
90
- ( memberchk(data(Data0), Options) -> Payload0 = Data0 ; Payload0 = Payload ),
93
+ ( memberchk(data(Data0), Options) -> http__checked_data(Data0, DataSpec) ; DataSpec = Payload ),
91
94
  http__request_headers(Options, RequestHeaders),
92
- eyeprolog__http_open(Address, Stream, Method, Payload0, Code, RequestHeaders, RawHeaders, FinalUrl),
95
+ eyeprolog__http_open(Address, Stream, Method, DataSpec, Code, RequestHeaders, RawHeaders, FinalUrl),
93
96
  get_n_chars(Stream, _, Data),
94
97
  close(Stream),
95
98
  http__trealla_headers(RawHeaders, Headers),
@@ -107,12 +110,14 @@ http__valid_method(Method) :-
107
110
  ; throw(error(domain_error(http_option, method(Method)), http_open/3))
108
111
  ).
109
112
 
110
- http__data(Options, Data) :-
111
- ( memberchk(data(Data0), Options) ->
112
- ( var(Data0) -> throw(error(instantiation_error, http_open/3)) ; Data = Data0 )
113
- ; Data = []
113
+ http__data_spec(Options, DataSpec) :-
114
+ ( memberchk(data(Data0), Options) -> http__checked_data(Data0, DataSpec)
115
+ ; DataSpec = no_data
114
116
  ).
115
117
 
118
+ http__checked_data(Data, data(Data)) :-
119
+ ( var(Data) -> throw(error(instantiation_error, http_open/3)) ; true ).
120
+
116
121
  http__request_headers(Options, Headers) :-
117
122
  ( memberchk(request_headers(Input), Options) ->
118
123
  ( var(Input) -> throw(error(instantiation_error, http_open/3)) ; true ),
package/src/lib/json.pl CHANGED
@@ -88,12 +88,45 @@ json_character(PrintChar) -->
88
88
  [PrintChar],
89
89
  { dif(PrintChar, '\\'), dif(PrintChar, '"'), char_code(PrintChar, Code), Code >= 32 }.
90
90
  json_character(EscapeChar) -->
91
- "\\u", json_hex(H1), json_hex(H2), json_hex(H3), json_hex(H4),
91
+ "\\u",
92
+ ( parsing ->
93
+ json_hex4(Code),
94
+ json_unicode_parsed(Code, EscapeChar)
95
+ ; { char_code(EscapeChar, Code) },
96
+ json_unicode_generated(Code)
97
+ ).
98
+
99
+ % JSON's \u escape syntax encodes UTF-16 code units. Combine a valid surrogate
100
+ % pair while parsing and split supplementary Unicode scalar values while
101
+ % generating. Lone surrogate code units are not Unicode scalar values and are
102
+ % therefore rejected instead of being passed to char_code/2.
103
+ json_unicode_parsed(Code, Char) -->
104
+ { Code >= 55296, Code =< 56319 }, !,
105
+ "\\u", json_hex4(Low),
106
+ { Low >= 56320, Low =< 57343,
107
+ Scalar is 65536 + (Code - 55296) * 1024 + (Low - 56320),
108
+ char_code(Char, Scalar) }.
109
+ json_unicode_parsed(Code, _) -->
110
+ { Code >= 56320, Code =< 57343 }, !,
111
+ { fail }.
112
+ json_unicode_parsed(Code, Char) -->
113
+ { char_code(Char, Code) }.
114
+
115
+ json_unicode_generated(Code) -->
116
+ { Code > 65535 }, !,
117
+ { Scalar is Code - 65536,
118
+ High is 55296 + Scalar // 1024,
119
+ Low is 56320 + Scalar mod 1024 },
120
+ json_hex4(High), "\\u", json_hex4(Low).
121
+ json_unicode_generated(Code) -->
122
+ { ( Code < 55296 ; Code > 57343 ) },
123
+ json_hex4(Code).
124
+
125
+ json_hex4(Code) -->
126
+ json_hex(H1), json_hex(H2), json_hex(H3), json_hex(H4),
92
127
  { ( nonvar(H1) ->
93
- Code is H1*4096 + H2*256 + H3*16 + H4,
94
- char_code(EscapeChar, Code)
95
- ; char_code(EscapeChar, Code),
96
- H1 is (Code // 4096) mod 16,
128
+ Code is H1*4096 + H2*256 + H3*16 + H4
129
+ ; H1 is (Code // 4096) mod 16,
97
130
  H2 is (Code // 256) mod 16,
98
131
  H3 is (Code // 16) mod 16,
99
132
  H4 is Code mod 16
@@ -9,11 +9,51 @@ const finiteFloat = (text) => {
9
9
  return Number.isFinite(value) ? value : null;
10
10
  };
11
11
 
12
+ // Compare decimal integer spellings without constructing host BigInts. This is
13
+ // used on term-ordering and unification hot paths, including integers far beyond
14
+ // JavaScript's safe-number range. Leading zeros and -0 remain value-equivalent.
15
+ export function compareIntegerValueText(left, right) {
16
+ let li = left[0] === '-' ? 1 : 0;
17
+ let ri = right[0] === '-' ? 1 : 0;
18
+ while (li < left.length && left.charCodeAt(li) === 48) li++;
19
+ while (ri < right.length && right.charCodeAt(ri) === 48) ri++;
20
+
21
+ const leftZero = li === left.length;
22
+ const rightZero = ri === right.length;
23
+ const leftNegative = !leftZero && left[0] === '-';
24
+ const rightNegative = !rightZero && right[0] === '-';
25
+ if (leftNegative !== rightNegative) return leftNegative ? -1 : 1;
26
+ if (leftZero || rightZero) return leftZero ? (rightZero ? 0 : -1) : 1;
27
+
28
+ const leftDigits = left.length - li;
29
+ const rightDigits = right.length - ri;
30
+ if (leftDigits !== rightDigits) {
31
+ const cmp = leftDigits < rightDigits ? -1 : 1;
32
+ return leftNegative ? -cmp : cmp;
33
+ }
34
+ for (let offset = 0; offset < leftDigits; offset++) {
35
+ const a = left.charCodeAt(li + offset);
36
+ const b = right.charCodeAt(ri + offset);
37
+ if (a === b) continue;
38
+ const cmp = a < b ? -1 : 1;
39
+ return leftNegative ? -cmp : cmp;
40
+ }
41
+ return 0;
42
+ }
43
+
44
+ function canonicalIntegerText(text) {
45
+ let index = text[0] === '-' ? 1 : 0;
46
+ while (index < text.length && text.charCodeAt(index) === 48) index++;
47
+ if (index === text.length) return '0';
48
+ const digits = text.slice(index);
49
+ return text[0] === '-' ? `-${digits}` : digits;
50
+ }
51
+
12
52
  export function sameNumberValue(left, right) {
13
53
  const leftInteger = decimalInteger(left);
14
54
  const rightInteger = decimalInteger(right);
15
55
  if (leftInteger || rightInteger) {
16
- return leftInteger && rightInteger && BigInt(left) === BigInt(right);
56
+ return leftInteger && rightInteger && compareIntegerValueText(left, right) === 0;
17
57
  }
18
58
  const leftValue = finiteFloat(left);
19
59
  const rightValue = finiteFloat(right);
@@ -21,7 +61,7 @@ export function sameNumberValue(left, right) {
21
61
  }
22
62
 
23
63
  export function numberValueKey(text) {
24
- if (decimalInteger(text)) return `integer:${BigInt(text)}`;
64
+ if (decimalInteger(text)) return `integer:${canonicalIntegerText(text)}`;
25
65
  const value = finiteFloat(text);
26
66
  if (value == null) return `invalid:${text}`;
27
67
  return `float:${Object.is(value, -0) ? 0 : value}`;
package/src/term.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Term model, environments, unification, readback, and ordering helpers.
2
2
  // Keep dependencies minimal because nearly every other module imports this file.
3
- import { sameNumberValue } from './number-value.js';
3
+ import { compareIntegerValueText, sameNumberValue } from './number-value.js';
4
4
 
5
5
  export const VAR = 'var';
6
6
  export const ATOM = 'atom';
@@ -1009,13 +1009,22 @@ function occurs(variableName, term, env) {
1009
1009
  // as soon as a binding fans out.
1010
1010
  if (initial.length === 1) {
1011
1011
  let name = initial[0];
1012
- const seen = [];
1012
+ const seenSmall = [];
1013
+ let seenLarge = null;
1013
1014
  while (true) {
1014
1015
  if (name === variableName) return true;
1015
- for (let index = 0; index < seen.length; index++) {
1016
- if (seen[index] === name) return false;
1016
+ let alreadySeen = seenLarge?.has(name) === true;
1017
+ if (seenLarge == null) {
1018
+ for (let index = 0; index < seenSmall.length; index++) {
1019
+ if (seenSmall[index] === name) { alreadySeen = true; break; }
1020
+ }
1021
+ }
1022
+ if (alreadySeen) return false;
1023
+ if (seenLarge != null) seenLarge.add(name);
1024
+ else {
1025
+ seenSmall.push(name);
1026
+ if (seenSmall.length === 8) seenLarge = new Set(seenSmall);
1017
1027
  }
1018
- seen.push(name);
1019
1028
  const binding = env?.get(name);
1020
1029
  if (binding === undefined) return false;
1021
1030
  const names = structuralVariableNames(binding);
@@ -1539,15 +1548,16 @@ export function variantTerms(left, leftEnv, right, rightEnv, pairs = new Map(),
1539
1548
 
1540
1549
 
1541
1550
  function compareCharacterText(left, right) {
1542
- const a = Array.from(left);
1543
- const b = Array.from(right);
1544
- const length = Math.min(a.length, b.length);
1545
- for (let i = 0; i < length; i++) {
1546
- const ac = a[i].codePointAt(0);
1547
- const bc = b[i].codePointAt(0);
1551
+ let li = 0;
1552
+ let ri = 0;
1553
+ while (li < left.length && ri < right.length) {
1554
+ const ac = left.codePointAt(li);
1555
+ const bc = right.codePointAt(ri);
1548
1556
  if (ac !== bc) return ac < bc ? -1 : 1;
1557
+ li += ac > 0xffff ? 2 : 1;
1558
+ ri += bc > 0xffff ? 2 : 1;
1549
1559
  }
1550
- return a.length < b.length ? -1 : a.length > b.length ? 1 : 0;
1560
+ return li < left.length ? 1 : ri < right.length ? -1 : 0;
1551
1561
  }
1552
1562
 
1553
1563
  export function compareTerms(left, right, variableRanks = null) {
@@ -1609,6 +1619,9 @@ export function isDecimalInteger(text) {
1609
1619
  }
1610
1620
 
1611
1621
  export function compareIntegerText(left, right) {
1622
+ if (isDecimalInteger(left) && isDecimalInteger(right)) return compareIntegerValueText(left, right);
1623
+ // Preserve the public helper's historical acceptance/error behavior for host
1624
+ // BigInt spellings outside EyeProlog's decimal integer term syntax.
1612
1625
  const a = BigInt(left);
1613
1626
  const b = BigInt(right);
1614
1627
  return a < b ? -1 : a > b ? 1 : 0;
@@ -129,7 +129,9 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
129
129
  Normal mode provides documented module and DCG compatibility profiles whose
130
130
  features overlap standardized Part 2 and Part 3 facilities. They are extensions
131
131
  relative to the Part 1 strict-core boundary and are tested separately; this
132
- ledger does not assert complete Part 2 or Part 3 conformance.
132
+ ledger does not assert complete Part 2 or Part 3 conformance. The concrete
133
+ compatibility boundary, including the normal-profile `phrase/2-3` terminal-sequence
134
+ error choice, is recorded in `ISO-PART2-PART3-SCOPE.md`.
133
135
 
134
136
  ## Important implementation-dependent behavior (not the 5.4 mandatory table)
135
137
 
@@ -0,0 +1,52 @@
1
+ # ISO Part 2 and Part 3 compatibility scope
2
+
3
+ EyeProlog's release-facing `--iso-strict` claim is deliberately limited to
4
+ ISO/IEC 13211-1:1995 together with Technical Corrigenda 1:2007, 2:2012, and
5
+ 3:2017. This note records the separate status of the normal-profile facilities
6
+ that overlap ISO/IEC 13211-2:2000 (modules) and ISO/IEC TS 13211-3:2025 (definite
7
+ clause grammar rules). It is a scope ledger, not an independent certification.
8
+
9
+ ## Part 2 modules
10
+
11
+ Normal mode implements a procedure-oriented module compatibility layer:
12
+ `module/2`, `use_module/1-2`, `meta_predicate/1`, explicit `Module:Goal`
13
+ qualification, exports/imports, nonterminal indicators, and module-aware meta
14
+ calls are covered by the regression and conformance corpora.
15
+
16
+ This overlap is intentionally described as a compatibility profile rather than
17
+ a complete Part 2 conformance claim. In particular, the Part 1 strict registry
18
+ does not enable module directives, and the project does not infer full Part 2
19
+ coverage from interoperability with Scryer, Trealla, or Logtalk. A future Part 2
20
+ claim would require a clause-by-clause Part 2 processor and module-semantics
21
+ ledger comparable to the existing Part 1 matrices.
22
+
23
+ ## Part 3 definite clause grammars
24
+
25
+ Normal mode expands grammar rules to ordinary predicates and supports terminal
26
+ sequences, sequencing, alternatives, semicontexts, embedded goals, cut,
27
+ `call//1`, `phrase//1`, `phrase/2-3`, module-qualified nonterminals, and the
28
+ implementation-dependent negation/if-then choices documented in the reference.
29
+ `--iso-strict` leaves `-->/2` as ordinary Part 1 operator syntax and excludes
30
+ Part 3 grammar expansion and `phrase/2-3` from the strict registry.
31
+
32
+ One compatibility difference is explicit: EyeProlog currently reports
33
+ `type_error(list)` when its `phrase/2-3` terminal-sequence validation rejects a
34
+ non-list. The Part 3 terminal-sequence specification uses its own terminal
35
+ sequence error category. EyeProlog retains the list-shaped error for its normal
36
+ interoperability profile, so this behavior must not be presented as evidence of
37
+ complete Part 3 conformance.
38
+
39
+ The Part 3 implementation otherwise keeps sequence validation, grammar-body
40
+ callability, variable-body instantiation errors, `phrase/3` steadfastness, and
41
+ grammar expansion under focused executable tests. Changes intended to advance a
42
+ formal Part 3 claim should update this ledger and the corresponding error cases
43
+ rather than silently changing the compatibility profile.
44
+
45
+ ## Release boundary
46
+
47
+ - Part 1 + Corrigenda 1-3: release-facing strict-core conformance target.
48
+ - Part 2: normal-mode compatibility profile, tested but not certified complete.
49
+ - Part 3: normal-mode compatibility profile, tested but not certified complete.
50
+
51
+ This separation keeps Part 1 conformance evidence independent of useful module
52
+ and DCG extensions while making known Part 2/Part 3 scope limits visible.
@@ -16,7 +16,8 @@ closes 7.9/Clause 9, and [ISO-PROCESSOR-REQUIREMENTS.md](ISO-PROCESSOR-REQUIREME
16
16
  decomposes the Clause 5 processor obligations.
17
17
  [ISO-CORRIGENDA-MATRIX.md](ISO-CORRIGENDA-MATRIX.md) gives every published
18
18
  Corrigenda amendment cluster an executable, editorial, or superseded
19
- disposition. Built-in rows may group closely related conditions only when the
19
+ disposition. [ISO-PART2-PART3-SCOPE.md](ISO-PART2-PART3-SCOPE.md) records the
20
+ separate normal-profile module/DCG compatibility boundary and known non-claims. Built-in rows may group closely related conditions only when the
20
21
  row names every grouped condition and its executable evidence.
21
22
  The exit checklist is embedded in [ISO-COMPLIANCE.md](ISO-COMPLIANCE.md). [WG17-SYNTAX-STATUS.md](WG17-SYNTAX-STATUS.md) records the
22
23
  complete one-to-one trace for the vendored active upstream WG17 syntax cases.
@@ -1,6 +1,8 @@
1
1
  import http from 'node:http';
2
2
  import { parentPort } from 'node:worker_threads';
3
3
 
4
+ const LARGE_BYTES = 9 * 1024 * 1024;
5
+
4
6
  const server = http.createServer((req, res) => {
5
7
  const chunks = [];
6
8
  req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
@@ -11,8 +13,19 @@ const server = http.createServer((req, res) => {
11
13
  res.end();
12
14
  return;
13
15
  }
16
+ if (req.url === '/large') {
17
+ res.setHeader('content-type', 'text/plain');
18
+ res.setHeader('content-length', String(LARGE_BYTES));
19
+ res.end(Buffer.alloc(LARGE_BYTES, 120));
20
+ return;
21
+ }
14
22
  const body = Buffer.concat(chunks).toString('utf8');
15
- const payload = JSON.stringify({ method: req.method, path: req.url, body, test: req.headers['x-test'] ?? null });
23
+ const payload = JSON.stringify({
24
+ method: req.method, path: req.url, body,
25
+ test: req.headers['x-test'] ?? null,
26
+ repeated: req.headers['x-repeat'] ?? null,
27
+ contentLength: req.headers['content-length'] ?? null,
28
+ });
16
29
  res.setHeader('content-type', 'application/json');
17
30
  res.setHeader('x-eyeprolog-test', 'yes');
18
31
  res.end(payload);
@@ -30,6 +30,29 @@ answer(Cs) :- once(phrase(json_chars(pairs([string("a")-number(2),string("ok")-b
30
30
  'answer("{\\"a\\":2,\\"ok\\":true}").\n', 'JSON generation');
31
31
  });
32
32
 
33
+ reporter.test('library(json) combines supplementary Unicode surrogate escapes', () => {
34
+ const source = String.raw`:- use_module(library(json)).
35
+ :- use_module(library(dcgs)).
36
+ answer(X) :- phrase(json_chars(X), "\"\\uD83D\\uDE00\"").`;
37
+ assertEqual(run(source, { goal: 'answer(X)' }).stdout,
38
+ 'answer(string("😀")).\n', 'JSON surrogate-pair parse');
39
+ });
40
+
41
+ reporter.test('library(json) generates and validates supplementary Unicode escapes relationally', () => {
42
+ const source = String.raw`:- use_module(library(json)).
43
+ :- use_module(library(dcgs)).
44
+ answer :- phrase(json_chars(string("😀")), "\"\\uD83D\\uDE00\"").`;
45
+ assertEqual(run(source, { goal: 'answer' }).stdout, 'answer.\n', 'JSON surrogate-pair generation');
46
+ });
47
+
48
+ reporter.test('library(json) rejects unpaired UTF-16 surrogates', () => {
49
+ const source = String.raw`:- use_module(library(json)).
50
+ :- use_module(library(dcgs)).
51
+ answer :- \+ phrase(json_chars(_), "\"\\uD83D\""),
52
+ \+ phrase(json_chars(_), "\"\\uDE00\"").`;
53
+ assertEqual(run(source, { goal: 'answer' }).stdout, 'answer.\n', 'unpaired JSON surrogate rejection');
54
+ });
55
+
33
56
  reporter.test('http_request/5 parses request lines and lower-cases header names', () => {
34
57
  const source = `:- use_module(library(http)).
35
58
  answer(M,P,V,H) :- http_request(user_input,M,P,V,H).`;
@@ -79,6 +102,39 @@ answer(Data,Code,Size) :-
79
102
  assertIncludes(output, '\\"path\\":\\"/open\\"', 'http_open body');
80
103
  assertIncludes(output, ', 200, ', 'http_open status and size');
81
104
  });
105
+
106
+ reporter.test('GET omits an entity Content-Length unless data/1 is explicit', () => {
107
+ const source = `:- use_module(library(http)).
108
+ answer(Data) :- http_get("http://127.0.0.1:${port}/headers", Data, []).`;
109
+ const output = run(source, { goal: 'answer(Data)' }).stdout;
110
+ assertIncludes(output, '\\"contentLength\\":null', 'GET content-length omission');
111
+ });
112
+
113
+ reporter.test('explicit empty POST data sends Content-Length zero', () => {
114
+ const source = `:- use_module(library(http)).
115
+ answer(Data) :- http_post("http://127.0.0.1:${port}/post-empty", "", Data, []).`;
116
+ const output = run(source, { goal: 'answer(Data)' }).stdout;
117
+ assertIncludes(output, '\\"contentLength\\":\\"0\\"', 'empty POST content-length');
118
+ });
119
+
120
+ reporter.test('request_headers/1 preserves repeated header values', () => {
121
+ const source = `:- use_module(library(http)).
122
+ answer(Data) :-
123
+ http_get("http://127.0.0.1:${port}/headers", Data,
124
+ [request_headers(['x-repeat'("one"),'x-repeat'("two")])]).`;
125
+ const output = run(source, { goal: 'answer(Data)' }).stdout;
126
+ assertIncludes(output, '\\"repeated\\":\\"one, two\\"', 'repeated request headers');
127
+ });
128
+
129
+ reporter.test('http_open/3 streams bodies larger than the host RPC buffer', () => {
130
+ const source = `:- use_module(library(http)).
131
+ :- use_module(library(charsio)).
132
+ answer(Prefix) :-
133
+ http_open("http://127.0.0.1:${port}/large", S, []),
134
+ get_n_chars(S,16,Prefix), close(S).`;
135
+ assertEqual(run(source, { goal: 'answer(Prefix)' }).stdout,
136
+ 'answer("xxxxxxxxxxxxxxxx").\n', 'large HTTP response streaming');
137
+ });
82
138
  } finally {
83
139
  worker.postMessage('close');
84
140
  await worker.terminate();
@@ -7217,6 +7217,30 @@ function whiteBoxCases() {
7217
7217
  assertEqual(String(compareTerms(right, left, ranks)), '1', 'shared order reverse direction');
7218
7218
  },
7219
7219
  },
7220
+ {
7221
+ name: 'numeric identity and term order handle noncanonical unbounded integers without host BigInts',
7222
+ run: () => {
7223
+ const env = new Env();
7224
+ assertEqual(unify(numberTerm('000123'), numberTerm('123'), env), true, 'leading-zero integer identity');
7225
+ assertEqual(unify(numberTerm('-000'), numberTerm('0'), env), true, 'negative-zero integer identity');
7226
+ assertEqual(compareTerms(numberTerm('99999999999999999999999999999999999999'),
7227
+ numberTerm('100000000000000000000000000000000000000')), -1, 'large positive integer order');
7228
+ assertEqual(compareTerms(numberTerm('-100000000000000000000000000000000000000'),
7229
+ numberTerm('-99999999999999999999999999999999999999')), -1, 'large negative integer order');
7230
+ assertEqual(publicApi.compareIntegerText('+1', '1'), 0, 'public helper retains host BigInt spelling compatibility');
7231
+ let invalidIntegerError = null;
7232
+ try { publicApi.compareIntegerText('not-an-integer', '1'); } catch (error) { invalidIntegerError = error; }
7233
+ assertEqual(invalidIntegerError?.name, 'SyntaxError', 'public helper retains invalid-spelling error');
7234
+ },
7235
+ },
7236
+ {
7237
+ name: 'character term order compares Unicode scalar values without materializing code-point arrays',
7238
+ run: () => {
7239
+ assertEqual(compareTerms(atom('a😀'), atom('a😁')), -1, 'supplementary scalar order');
7240
+ assertEqual(compareTerms(atom('a😀'), atom('a😀x')), -1, 'supplementary prefix order');
7241
+ assertEqual(compareTerms(atom('a😀x'), atom('a😀')), 1, 'supplementary reverse prefix order');
7242
+ },
7243
+ },
7220
7244
  {
7221
7245
  name: 'unification rejects direct and indirect cyclic bindings',
7222
7246
  run: () => {
@@ -5861,8 +5861,11 @@ look_ahead(X), [X] --> [X].
5861
5861
  `phrase(+Body,?Sequence)` accepts or generates a complete sequence.
5862
5862
  `phrase(+Body,?Sequence,?Rest)` leaves `Rest` unconsumed and is steadfast in
5863
5863
  that argument. A variable body raises `instantiation_error`; a non-callable
5864
- body raises `type_error(callable)`. EyeProlog performs terminal-sequence checks
5865
- and reports the portable ISO `type_error(list)` error term.
5864
+ body raises `type_error(callable)`. EyeProlog performs terminal-sequence checks and currently reports
5865
+ `type_error(list)` for a rejected non-list. This is a normal-profile
5866
+ interoperability choice, not a Part 3 conformance claim; Part 3 defines a
5867
+ dedicated terminal-sequence error category. See
5868
+ [`ISO-PART2-PART3-SCOPE.md`](test/conformance/ISO-PART2-PART3-SCOPE.md).
5866
5869
 
5867
5870
  #### A bidirectional expression grammar
5868
5871
 
@@ -6811,9 +6814,9 @@ library. Scryer uses `library(builtins)` as its fundamental system module, while
6811
6814
  Trealla's file is implementation support; EyeProlog keeps those procedures in
6812
6815
  the core registry instead of creating a second authority for them.
6813
6816
 
6814
- `library(http)` combines the Scryer `http_open/3` option surface with Trealla's `http_get/3`, `http_post/4`, `http_patch/4`, `http_put/4`, `http_delete/3`, `http_server/2`, and `http_request/5`. HTTP and HTTPS client requests are performed by the module-owned Node adapter in `src/http-host.js`; response bodies are exposed as ordinary text streams for `http_open/3` and as complete character lists for the convenience predicates. The client follows redirects, supports Scryer request/response metadata options, Trealla `header(Name,Value)` request options, and Trealla's `host/path` address-list form. `http_request/5` parses a request line and headers from a stream. The compact `http_server/2` facade accepts one connection per call because EyeProlog does not provide Trealla's `fork` task primitive.
6817
+ `library(http)` combines the Scryer `http_open/3` option surface with Trealla's `http_get/3`, `http_post/4`, `http_patch/4`, `http_put/4`, `http_delete/3`, `http_server/2`, and `http_request/5`. HTTP and HTTPS client requests are performed by the module-owned Node adapter in `src/http-host.js`; response bodies are exposed as ordinary text streams for `http_open/3` and as complete character lists for the convenience predicates. The host stream pulls response bytes lazily in bounded chunks, so opening a large response no longer buffers the entire body through the fixed-size RPC message. An ordinary GET or HEAD has no request entity unless `data/1` is explicit, repeated request-header values are preserved, and an explicitly empty entity gets `Content-Length: 0`. The client follows redirects, supports Scryer request/response metadata options, Trealla `header(Name,Value)` request options, and Trealla's `host/path` address-list form. `http_request/5` parses a request line and headers from a stream. The compact `http_server/2` facade accepts one connection per call because EyeProlog does not provide Trealla's `fork` task primitive.
6815
6818
 
6816
- `library(json)` is the BSD-licensed Scryer JSON DCG also distributed by Trealla. `json_chars//1` is bidirectional and represents JSON objects as `pairs/1`, arrays as `list/1`, strings as `string/1`, numbers as `number/1`, booleans as `boolean/1`, and JSON null as `null`. See `examples/json.pl` and `examples/http-client.pl`.
6819
+ `library(json)` is the BSD-licensed Scryer JSON DCG also distributed by Trealla. `json_chars//1` is bidirectional and represents JSON objects as `pairs/1`, arrays as `list/1`, strings as `string/1`, numbers as `number/1`, booleans as `boolean/1`, and JSON null as `null`. JSON `\u` escapes are UTF-16 code units: valid surrogate pairs are combined into one supplementary Unicode scalar while parsing and emitted as a pair when that escaped representation is requested during generation; unpaired surrogates are rejected. See `examples/json.pl` and `examples/http-client.pl`.
6817
6820
 
6818
6821
  `library(sockets)` follows Scryer's TCP stream interface. `socket_client_open/3`
6819
6822
  connects to `Host:Port`; `socket_server_open/2` accepts either a port or