eyeprolog 1.5.39 → 1.5.41

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.41",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -75,6 +75,7 @@
75
75
  "conformance:update:neumerkel": "node test/run-neumerkel.mjs --update-report",
76
76
  "conformance:check:neumerkel": "node test/run-neumerkel.mjs --cached --verify-report",
77
77
  "conformance:sync:neumerkel": "node test/run-neumerkel.mjs --cached --update-report",
78
- "test:http-json": "node test/run-http-json.mjs"
78
+ "test:http-json": "node test/run-http-json.mjs",
79
+ "test:iso-part2-amendment": "node test/run-iso-part2-amendment.mjs"
79
80
  }
80
81
  }
@@ -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/parser.js CHANGED
@@ -112,7 +112,6 @@ const INFIX_OPERATORS = new Map([
112
112
  ['=<', { precedence: 501, associativity: 'none' }],
113
113
  ['>', { precedence: 501, associativity: 'none' }],
114
114
  ['>=', { precedence: 501, associativity: 'none' }],
115
- [':', { precedence: 601, associativity: 'right' }],
116
115
  ['+', { precedence: 701, associativity: 'left' }],
117
116
  ['-', { precedence: 701, associativity: 'left' }],
118
117
  ['/\\', { precedence: 701, associativity: 'left' }],
@@ -142,13 +141,21 @@ export const ISO_OPERATOR_DEFINITIONS = [
142
141
  [900, 'fy', '\\+'],
143
142
  ...['=', '=..', '\\=', '==', '\\==', '@<', '@=<', '@>', '@>=', 'is',
144
143
  '=:=', '=\\=', '<', '=<', '>', '>='].map((name) => [700, 'xfx', name]),
145
- [600, 'xfy', ':'],
146
144
  ...['+', '-', '/\\', '\\/'].map((name) => [500, 'yfx', name]),
147
145
  ...['*', '/', '//', 'div', 'mod', 'rem', '<<', '>>'].map((name) => [400, 'yfx', name]),
148
146
  [200, 'xfx', '**'], [200, 'xfy', '^'],
149
147
  [200, 'fy', '+'], [200, 'fy', '-'], [200, 'fy', '\\'],
150
148
  ];
151
149
 
150
+ // ISO/IEC 13211-2 adds module qualification to the Part 1 operator table.
151
+ // The 2013 amendment also writes meta_predicate/1 in directive operator form,
152
+ // so normal module mode accepts that standard spelling while strict Part 1
153
+ // keeps both additions out of its initial operator table.
154
+ export const PART2_OPERATOR_DEFINITIONS = [
155
+ [600, 'xfy', ':'],
156
+ [1150, 'fx', 'meta_predicate'],
157
+ ];
158
+
152
159
  // The alternative operator belongs to the Part 3 grammar-rule profile. Part 1
153
160
  // reserves `|` as list punctuation but permits a program to declare it as an
154
161
  // infix operator at priority 1001 or greater (Corrigendum 2).
@@ -226,7 +233,7 @@ export function createParserOperatorState(definitions = [], includeDefaults = tr
226
233
  postfixOperators: new Map(),
227
234
  };
228
235
  if (includeDefaults && options.isoStrict !== true) {
229
- for (const [priority, specifier, name] of [...PART3_OPERATOR_DEFINITIONS, ...QUAD_OPERATOR_DEFINITIONS]) {
236
+ for (const [priority, specifier, name] of [...PART2_OPERATOR_DEFINITIONS, ...PART3_OPERATOR_DEFINITIONS, ...QUAD_OPERATOR_DEFINITIONS]) {
230
237
  defineParserOperator(state, priority, specifier, name);
231
238
  }
232
239
  }