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 +2 -2
- package/examples/json.pl +2 -1
- package/examples/output/json.pl +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +1 -1
- package/src/http-host.js +40 -6
- package/src/http-worker.js +101 -26
- package/src/lib/http.pl +18 -13
- package/src/lib/json.pl +38 -5
- package/src/number-value.js +42 -2
- package/src/term.js +25 -12
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +3 -1
- package/test/conformance/ISO-PART2-PART3-SCOPE.md +52 -0
- package/test/conformance/README.md +2 -1
- package/test/fixtures/http-test-server.mjs +14 -1
- package/test/run-http-json.mjs +56 -0
- package/test/run-regression.mjs +24 -0
- package/the-art-of-eyeprolog.md +7 -4
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([
|
package/examples/output/json.pl
CHANGED
|
@@ -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
package/src/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
3
|
-
//
|
|
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
|
|
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:
|
|
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 =
|
|
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.
|
|
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) &&
|
package/src/http-worker.js
CHANGED
|
@@ -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
|
|
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 =
|
|
39
|
-
if (body != null &&
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
70
|
-
if (![301, 302, 303, 307, 308].includes(
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
http__data_spec(Options, DataSpec),
|
|
32
32
|
http__request_headers(Options, RequestHeaders),
|
|
33
|
-
eyeprolog__http_open(Address, Response, Method,
|
|
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,
|
|
73
|
+
http__request_data(Address, get, no_data, Data, Options).
|
|
74
74
|
|
|
75
75
|
http_post(Address, PostData, Reply, Options) :-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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) ->
|
|
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,
|
|
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
|
-
|
|
111
|
-
( memberchk(data(Data0), Options) ->
|
|
112
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
package/src/number-value.js
CHANGED
|
@@ -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 &&
|
|
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:${
|
|
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
|
|
1012
|
+
const seenSmall = [];
|
|
1013
|
+
let seenLarge = null;
|
|
1013
1014
|
while (true) {
|
|
1014
1015
|
if (name === variableName) return true;
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const
|
|
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
|
|
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.
|
|
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({
|
|
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);
|
package/test/run-http-json.mjs
CHANGED
|
@@ -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();
|
package/test/run-regression.mjs
CHANGED
|
@@ -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: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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
|
-
|
|
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
|