eyeprolog 1.2.6 → 1.2.8
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/conformance-report.md +2 -2
- package/package.json +1 -1
- package/src/iso.js +56 -19
- package/src/parser.js +43 -7
- package/src/repl.js +82 -4
- package/src/write.js +19 -18
- package/test/conformance/ISO-COMPLIANCE.md +1 -1
- package/test/conformance/ISO-MATRIX.md +1 -1
- package/test/conformance/README.md +1 -1
- package/test/conformance/cases/iso/streams_and_term_io.pl +11 -0
- package/test/conformance/cases/iso/wg17_syntax_high_risk.pl +16 -1
- package/test/conformance/cases/strings/trim_tabs_and_spaces.pl +1 -2
- package/test/conformance/cases/syntax/quoted_atom_with_newline_escape.pl +1 -2
- package/test/conformance/cases/syntax/string_with_tab_escape.pl +1 -1
- package/test/conformance/errors/iso/wg17_bad_character_code_escape.pl +1 -0
- package/test/conformance/errors/iso/wg17_literal_newline_in_quote.pl +2 -0
- package/test/conformance/errors/iso/wg17_literal_tab_in_quote.pl +1 -0
- package/test/conformance/errors/iso/wg17_non_iso_escape.pl +1 -0
- package/test/conformance/errors/iso/wg17_non_iso_unicode_escape.pl +1 -0
- package/test/conformance/errors/iso/wg17_unterminated_quoted_token.pl +1 -0
- package/test/conformance/expected/iso/streams_and_term_io.pl +1 -0
- package/test/conformance/expected/iso/wg17_syntax_high_risk.pl +7 -0
- package/test/conformance/expected-errors/iso/wg17_bad_character_code_escape.txt +1 -0
- package/test/conformance/expected-errors/iso/wg17_literal_newline_in_quote.txt +1 -0
- package/test/conformance/expected-errors/iso/wg17_literal_tab_in_quote.txt +1 -0
- package/test/conformance/expected-errors/iso/wg17_non_iso_escape.txt +1 -0
- package/test/conformance/expected-errors/iso/wg17_non_iso_unicode_escape.txt +1 -0
- package/test/conformance/expected-errors/iso/wg17_unterminated_quoted_token.txt +1 -0
- package/test/conformance/expected-errors/syntax/unclosed_quoted_atom.txt +1 -1
- package/test/conformance/expected-errors/syntax/unclosed_quoted_atom_rejected.txt +1 -1
- package/test/conformance/expected-errors/syntax/unclosed_string.txt +1 -1
- package/test/run-regression.mjs +168 -0
- package/the-art-of-eyeprolog.md +38 -22
package/conformance-report.md
CHANGED
|
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
11
11
|
| builtins | 11 | 0 | 0 | 0 | 11 |
|
|
12
12
|
| context | 11 | 0 | 0 | 0 | 11 |
|
|
13
13
|
| control | 15 | 0 | 0 | 0 | 15 |
|
|
14
|
-
| iso | 167 |
|
|
14
|
+
| iso | 167 | 216 | 0 | 0 | 383 |
|
|
15
15
|
| lists | 52 | 3 | 0 | 0 | 55 |
|
|
16
16
|
| modules | 2 | 0 | 0 | 0 | 2 |
|
|
17
17
|
| negation | 8 | 0 | 19 | 0 | 27 |
|
|
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
23
23
|
| terms | 26 | 3 | 0 | 0 | 29 |
|
|
24
24
|
| unification | 18 | 0 | 0 | 0 | 18 |
|
|
25
25
|
| variables | 16 | 9 | 0 | 0 | 25 |
|
|
26
|
-
| **Total** | **482** | **
|
|
26
|
+
| **Total** | **482** | **267** | **19** | **21** | **789** |
|
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1067,6 +1067,11 @@ function quotedEscapeEnd(source, index) {
|
|
|
1067
1067
|
const escaped = source[index + 1] ?? '';
|
|
1068
1068
|
if (!escaped) return index;
|
|
1069
1069
|
|
|
1070
|
+
// A quoted-token continuation escape consumes its newline. Accept CRLF as
|
|
1071
|
+
// the host representation of the same line boundary.
|
|
1072
|
+
if (escaped === '\n') return index + 1;
|
|
1073
|
+
if (escaped === '\r' && source[index + 2] === '\n') return index + 2;
|
|
1074
|
+
|
|
1070
1075
|
// ISO 6.4.2.1 numeric escapes include a terminating backslash. Consume
|
|
1071
1076
|
// that delimiter as part of the quoted character so stream scanning does
|
|
1072
1077
|
// not mistake it for an escape of the quote which follows.
|
|
@@ -1096,6 +1101,13 @@ function* termTextCandidates(stream) {
|
|
|
1096
1101
|
if (blockComment) { if (ch === '*' && next === '/') { blockComment = false; i++; } continue; }
|
|
1097
1102
|
if (quote) {
|
|
1098
1103
|
if (ch === '\\') i = quotedEscapeEnd(source, i);
|
|
1104
|
+
else if (ch !== ' ' && /^[\u0009-\u000d]$/.test(ch)) {
|
|
1105
|
+
// Literal layout characters are not quoted characters (6.4.2.1).
|
|
1106
|
+
// Surface the lexical error immediately even when there is no later
|
|
1107
|
+
// full stop; otherwise read/1 would misreport malformed input as EOF.
|
|
1108
|
+
yield { text: source.slice(stream.position, i + 1), end: i + 1, lexicalError: true };
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1099
1111
|
else if (ch === quote && next === quote) i++;
|
|
1100
1112
|
else if (ch === quote) quote = null;
|
|
1101
1113
|
continue;
|
|
@@ -1135,28 +1147,53 @@ function convertedTermText(text, solver) {
|
|
|
1135
1147
|
return result;
|
|
1136
1148
|
}
|
|
1137
1149
|
function readTermFromStream(stream, solver) {
|
|
1138
|
-
let
|
|
1139
|
-
|
|
1140
|
-
sawCandidate =
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1150
|
+
let requestedInteractiveTerm = false;
|
|
1151
|
+
while (true) {
|
|
1152
|
+
let sawCandidate = false;
|
|
1153
|
+
for (const candidate of termTextCandidates(stream)) {
|
|
1154
|
+
sawCandidate = true;
|
|
1155
|
+
if (candidate.lexicalError) {
|
|
1156
|
+
stream.position = candidate.end;
|
|
1157
|
+
throw new PrologError('syntax_error(read_term)');
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
const operatorState = createParserOperatorState(solver.program.operators.values(), false);
|
|
1161
|
+
const clauses = parseClauses(convertedTermText(candidate.text, solver), {
|
|
1162
|
+
sourceMetadata: false,
|
|
1163
|
+
operatorState,
|
|
1164
|
+
doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
1165
|
+
});
|
|
1166
|
+
if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
|
|
1167
|
+
stream.position = candidate.end;
|
|
1168
|
+
return clauses[0].head;
|
|
1169
|
+
} catch (_) {
|
|
1170
|
+
// A dot inside a graphic operator, such as =.., is only a possible
|
|
1171
|
+
// terminator. Keep scanning until a complete term parses.
|
|
1172
|
+
}
|
|
1154
1173
|
}
|
|
1174
|
+
|
|
1175
|
+
// The interactive top level may attach a synchronous reader to the
|
|
1176
|
+
// standard user_input stream. Ask it for one complete read-term only when
|
|
1177
|
+
// this read operation actually reaches the end of buffered input. This is
|
|
1178
|
+
// deliberately a stream hook, not goal-shape recognition, so conjunctions
|
|
1179
|
+
// and reads reached through user predicates behave the same as read/1.
|
|
1180
|
+
if (!sawCandidate && !requestedInteractiveTerm &&
|
|
1181
|
+
typeof stream.interactiveReadTerm === 'function') {
|
|
1182
|
+
requestedInteractiveTerm = true;
|
|
1183
|
+
const text = stream.interactiveReadTerm();
|
|
1184
|
+
if (text != null) {
|
|
1185
|
+
stream.content += String(text);
|
|
1186
|
+
stream.pastEnd = false;
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
stream.position = String(stream.content).length;
|
|
1192
|
+
if (!sawCandidate) return atom('end_of_file');
|
|
1193
|
+
throw new PrologError('syntax_error(read_term)');
|
|
1155
1194
|
}
|
|
1156
|
-
stream.position = String(stream.content).length;
|
|
1157
|
-
if (!sawCandidate) return atom('end_of_file');
|
|
1158
|
-
throw new PrologError('syntax_error(read_term)');
|
|
1159
1195
|
}
|
|
1196
|
+
|
|
1160
1197
|
function* readBuiltin({ solver, goal, env }) {
|
|
1161
1198
|
const stream = inputStreamFor(solver, goal, env);
|
|
1162
1199
|
if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
|
package/src/parser.js
CHANGED
|
@@ -285,29 +285,58 @@ class Parser {
|
|
|
285
285
|
break;
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
|
-
readEscape(line) {
|
|
288
|
+
readEscape(line, options = {}) {
|
|
289
289
|
const escaped = this.take();
|
|
290
290
|
if (!escaped) throw new Error(`parse line ${line}: unterminated escape sequence`);
|
|
291
|
-
|
|
291
|
+
|
|
292
|
+
// ISO 6.4.2 permits a continuation escape only inside quoted tokens: a
|
|
293
|
+
// backslash immediately followed by a newline. Character-code constants
|
|
294
|
+
// use a single quoted character and therefore cannot use continuation.
|
|
295
|
+
if (escaped === '\n') {
|
|
296
|
+
if (options.allowContinuation !== false) return '';
|
|
297
|
+
throw new Error(`parse line ${line}: bad escape sequence`);
|
|
298
|
+
}
|
|
299
|
+
if (escaped === '\r' && this.peek() === '\n') {
|
|
300
|
+
if (options.allowContinuation !== false) {
|
|
301
|
+
this.take();
|
|
302
|
+
return '';
|
|
303
|
+
}
|
|
304
|
+
throw new Error(`parse line ${line}: bad escape sequence`);
|
|
305
|
+
}
|
|
306
|
+
|
|
292
307
|
const controls = { a: '\x07', b: '\b', r: '\r', f: '\f', t: '\t', n: '\n', v: '\v' };
|
|
293
308
|
if (controls[escaped] != null) return controls[escaped];
|
|
309
|
+
|
|
294
310
|
if (escaped === 'x') {
|
|
295
311
|
let digits = '';
|
|
296
312
|
while (/^[0-9A-Fa-f]$/.test(this.peek())) digits += this.take();
|
|
297
313
|
if (!digits || this.take() !== '\\') throw new Error(`parse line ${line}: bad hexadecimal escape`);
|
|
298
|
-
|
|
314
|
+
const code = Number.parseInt(digits, 16);
|
|
315
|
+
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
|
|
316
|
+
throw new Error(`parse line ${line}: character escape out of range`);
|
|
317
|
+
}
|
|
318
|
+
return String.fromCodePoint(code);
|
|
299
319
|
}
|
|
300
320
|
if (/^[0-7]$/.test(escaped)) {
|
|
301
321
|
let digits = escaped;
|
|
302
322
|
while (/^[0-7]$/.test(this.peek())) digits += this.take();
|
|
303
323
|
if (this.take() !== '\\') throw new Error(`parse line ${line}: bad octal escape`);
|
|
304
|
-
|
|
324
|
+
const code = Number.parseInt(digits, 8);
|
|
325
|
+
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
|
|
326
|
+
throw new Error(`parse line ${line}: character escape out of range`);
|
|
327
|
+
}
|
|
328
|
+
return String.fromCodePoint(code);
|
|
305
329
|
}
|
|
306
330
|
// A backslash followed by a decimal digit is numeric-escape syntax, but
|
|
307
331
|
// ISO octal digits are limited to 0..7. Do not reinterpret \8 or \9 as
|
|
308
332
|
// implementation-specific one-character escapes.
|
|
309
333
|
if (/^[0-9]$/.test(escaped)) throw new Error(`parse line ${line}: bad octal escape`);
|
|
310
|
-
|
|
334
|
+
|
|
335
|
+
// The only remaining ISO meta escapes are the four meta characters from
|
|
336
|
+
// 6.5.5. Forms such as \c, \d, \e, \u or \. are not quoted
|
|
337
|
+
// characters in ISO syntax and must not be silently accepted.
|
|
338
|
+
if (escaped === '\\' || escaped === "'" || escaped === '"' || escaped === '`') return escaped;
|
|
339
|
+
throw new Error(`parse line ${line}: bad escape sequence`);
|
|
311
340
|
}
|
|
312
341
|
nextToken() {
|
|
313
342
|
// The tokenizer keeps just enough state for useful parse-line errors and
|
|
@@ -373,6 +402,11 @@ class Parser {
|
|
|
373
402
|
}
|
|
374
403
|
} else if (value === '\\' && this.peek()) {
|
|
375
404
|
value = this.readEscape(line);
|
|
405
|
+
} else if (value !== ' ' && isWhitespaceCode(value.charCodeAt(0))) {
|
|
406
|
+
// ISO 6.4.2.1 allows an ordinary space in a quoted character, but
|
|
407
|
+
// not literal layout characters such as tab or newline. Newlines
|
|
408
|
+
// are permitted only through the continuation escape handled above.
|
|
409
|
+
throw new Error(`parse line ${line}: layout character in quoted term`);
|
|
376
410
|
}
|
|
377
411
|
text += value;
|
|
378
412
|
}
|
|
@@ -398,8 +432,10 @@ class Parser {
|
|
|
398
432
|
this.take();
|
|
399
433
|
this.take();
|
|
400
434
|
let value = this.take();
|
|
401
|
-
if (!value || value
|
|
402
|
-
|
|
435
|
+
if (!value || (value !== ' ' && isWhitespaceCode(value.charCodeAt(0)))) {
|
|
436
|
+
throw new Error(`parse line ${line}: bad character code constant`);
|
|
437
|
+
}
|
|
438
|
+
if (value === '\\') value = this.readEscape(line, { allowContinuation: false });
|
|
403
439
|
const code = value.codePointAt(0);
|
|
404
440
|
return { type: TOK.NUMBER, text: String(negative ? -code : code), line };
|
|
405
441
|
}
|
package/src/repl.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Interactive top level for the eyeprolog command.
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
+
import { readSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { createInterface } from 'node:readline';
|
|
5
6
|
import { formalErrorTerm } from './iso.js';
|
|
@@ -20,7 +21,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
20
21
|
const errorOutput = options.errorOutput ?? process.stderr;
|
|
21
22
|
const reader = new LineReader(input, output);
|
|
22
23
|
const sources = [];
|
|
23
|
-
let state = makeState(engine, sources, output, options);
|
|
24
|
+
let state = makeState(engine, sources, output, options, null, reader);
|
|
24
25
|
let exitCode = 0;
|
|
25
26
|
|
|
26
27
|
try {
|
|
@@ -34,7 +35,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
34
35
|
const goal = parseGoal(engine, state, text);
|
|
35
36
|
if (!options.isoStrict && isUseModuleGoal(goal)) {
|
|
36
37
|
sources.push({ text: `:- ${text}.\n`, filename: '<repl>' });
|
|
37
|
-
state = makeState(engine, sources, output, options, state);
|
|
38
|
+
state = makeState(engine, sources, output, options, state, reader);
|
|
38
39
|
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
39
40
|
output.write(' true.\n');
|
|
40
41
|
continue;
|
|
@@ -42,7 +43,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
42
43
|
const consultFiles = options.isoStrict ? null : consultDesignations(engine, goal);
|
|
43
44
|
if (consultFiles != null) {
|
|
44
45
|
for (const filename of consultFiles) sources.push(await readSource(filename));
|
|
45
|
-
state = makeState(engine, sources, output, options, state);
|
|
46
|
+
state = makeState(engine, sources, output, options, state, reader);
|
|
46
47
|
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
47
48
|
output.write(' true.\n');
|
|
48
49
|
continue;
|
|
@@ -78,6 +79,8 @@ export async function runRepl(engine, options = {}) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
class LineReader {
|
|
82
|
+
static syncWait = new Int32Array(new SharedArrayBuffer(4));
|
|
83
|
+
|
|
81
84
|
constructor(input, output) {
|
|
82
85
|
this.input = input;
|
|
83
86
|
this.output = output;
|
|
@@ -142,6 +145,57 @@ class LineReader {
|
|
|
142
145
|
return control;
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
canReadTermSynchronously() {
|
|
149
|
+
return this.terminal && Number.isInteger(this.input.fd);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
readInteractiveTermSync() {
|
|
153
|
+
if (!this.canReadTermSynchronously()) return null;
|
|
154
|
+
let source = '';
|
|
155
|
+
let prompt = '|: ';
|
|
156
|
+
while (true) {
|
|
157
|
+
this.output.write(prompt);
|
|
158
|
+
const line = this.readTerminalLineSync();
|
|
159
|
+
if (line == null) return source.trim() ? source : null;
|
|
160
|
+
source += `${line}\n`;
|
|
161
|
+
const end = terminalFullStop(source);
|
|
162
|
+
if (end >= 0) return source.slice(0, end + 1) + '\n';
|
|
163
|
+
prompt = '| ';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
readTerminalLineSync() {
|
|
168
|
+
const byte = Buffer.allocUnsafe(1);
|
|
169
|
+
const bytes = [];
|
|
170
|
+
while (true) {
|
|
171
|
+
let count;
|
|
172
|
+
try {
|
|
173
|
+
count = readSync(this.input.fd, byte, 0, 1, null);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// Node keeps terminal fds non-blocking. Once readline is suspended,
|
|
176
|
+
// a synchronous read can therefore report EAGAIN while waiting for
|
|
177
|
+
// the user. Sleep briefly and retry; terminal signals still retain
|
|
178
|
+
// their native action because no readline signal handler is installed.
|
|
179
|
+
if (error?.code === 'EAGAIN' || error?.code === 'EWOULDBLOCK') {
|
|
180
|
+
Atomics.wait(LineReader.syncWait, 0, 0, 10);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
// In canonical terminal mode Ctrl-D on an empty line makes read(2)
|
|
186
|
+
// return zero bytes. Scope that EOF to the current Prolog read rather
|
|
187
|
+
// than closing the outer readline iterator / top-level loop.
|
|
188
|
+
if (count === 0) {
|
|
189
|
+
return bytes.length === 0 ? null : Buffer.from(bytes).toString('utf8');
|
|
190
|
+
}
|
|
191
|
+
if (byte[0] === 10) {
|
|
192
|
+
if (bytes.at(-1) === 13) bytes.pop();
|
|
193
|
+
return Buffer.from(bytes).toString('utf8');
|
|
194
|
+
}
|
|
195
|
+
bytes.push(byte[0]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
145
199
|
suspendForComputation() {
|
|
146
200
|
if (!this.terminal || !this.readline) return false;
|
|
147
201
|
// Node readline installs terminal signal handling while the interface is
|
|
@@ -175,7 +229,7 @@ function runWithTerminalSignals(reader, operation) {
|
|
|
175
229
|
}
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
function makeState(engine, sources, output, options = {}, previousState = null) {
|
|
232
|
+
function makeState(engine, sources, output, options = {}, previousState = null, reader = null) {
|
|
179
233
|
const strictIso = options.isoStrict === true;
|
|
180
234
|
const program = engine.Program.parseSources(sources, { strictIso, sourceMetadata: strictIso });
|
|
181
235
|
const solver = new engine.Solver(program, {
|
|
@@ -183,6 +237,14 @@ function makeState(engine, sources, output, options = {}, previousState = null)
|
|
|
183
237
|
isoStrict: strictIso,
|
|
184
238
|
ioOptions: { write: (text) => output.write(String(text)) },
|
|
185
239
|
});
|
|
240
|
+
const userInput = solver.io.resolve('user_input');
|
|
241
|
+
if (userInput && reader?.canReadTermSynchronously()) {
|
|
242
|
+
// The solver is synchronous. While pullSolution() has readline suspended,
|
|
243
|
+
// let ISO term input request a complete terminal term exactly when read/1-2
|
|
244
|
+
// or read_term/2-3 actually executes. This also works inside conjunctions
|
|
245
|
+
// and user predicates instead of only when read/* is the whole REPL goal.
|
|
246
|
+
userInput.interactiveReadTerm = () => reader.readInteractiveTermSync();
|
|
247
|
+
}
|
|
186
248
|
const flagOverrides = new Map(previousState?.flagOverrides ?? []);
|
|
187
249
|
for (const [name, value] of flagOverrides) {
|
|
188
250
|
const definition = solver.prologFlags.get(name);
|
|
@@ -218,6 +280,10 @@ async function readQuery(reader) {
|
|
|
218
280
|
}
|
|
219
281
|
|
|
220
282
|
async function prepareInteractiveTermInput(state, goal, reader) {
|
|
283
|
+
// Real terminals are serviced on demand from readTermFromStream() while the
|
|
284
|
+
// synchronous solver is running. Keep the older async preloader only as a
|
|
285
|
+
// fallback for piped/non-TTY REPL tests and scripted input.
|
|
286
|
+
if (reader.canReadTermSynchronously()) return;
|
|
221
287
|
const stream = interactiveTermInputStream(state, goal);
|
|
222
288
|
if (stream == null || terminalFullStop(String(stream.content).slice(stream.position)) >= 0) return;
|
|
223
289
|
|
|
@@ -272,6 +338,12 @@ function quotedEscapeEnd(source, index) {
|
|
|
272
338
|
const escaped = source[index + 1] ?? '';
|
|
273
339
|
if (!escaped) return index;
|
|
274
340
|
|
|
341
|
+
// A backslash-newline pair is a continuation escape (6.4.2), so the
|
|
342
|
+
// newline is not a literal quoted character. Accept CRLF as the host text
|
|
343
|
+
// representation of the same continuation boundary.
|
|
344
|
+
if (escaped === '\n') return index + 1;
|
|
345
|
+
if (escaped === '\r' && source[index + 2] === '\n') return index + 2;
|
|
346
|
+
|
|
275
347
|
// ISO 6.4.2.1 octal and hexadecimal escapes are terminated by a
|
|
276
348
|
// backslash. Consume that terminator as part of the escape so the REPL
|
|
277
349
|
// scanner does not mistake it for an escape of the following quote.
|
|
@@ -317,6 +389,12 @@ function terminalFullStop(source) {
|
|
|
317
389
|
if (quote != null) {
|
|
318
390
|
if (ch === '\\') {
|
|
319
391
|
i = quotedEscapeEnd(source, i);
|
|
392
|
+
} else if (ch === '\n' || ch === '\r') {
|
|
393
|
+
// A literal newline can never be repaired by a later line: ISO
|
|
394
|
+
// 6.4.2.1 excludes it from quoted characters. Return this boundary
|
|
395
|
+
// immediately so the parser reports a syntax error instead of the
|
|
396
|
+
// top level prompting forever for a closing quote.
|
|
397
|
+
return i;
|
|
320
398
|
} else if (ch === quote) {
|
|
321
399
|
if (next === quote) i++;
|
|
322
400
|
else quote = null;
|
package/src/write.js
CHANGED
|
@@ -7,6 +7,23 @@ import {
|
|
|
7
7
|
const graphicAtomCharacters = new Set('!#$&*+-/<=>@^~\\'.split(''));
|
|
8
8
|
const compactInfixOperators = new Set([':', '..']);
|
|
9
9
|
|
|
10
|
+
function quotedControlEscape(ch) {
|
|
11
|
+
if (ch === '\x00') return '\\0\\';
|
|
12
|
+
if (ch === '\x07') return '\\a';
|
|
13
|
+
if (ch === '\b') return '\\b';
|
|
14
|
+
if (ch === '\r') return '\\r';
|
|
15
|
+
if (ch === '\f') return '\\f';
|
|
16
|
+
if (ch === '\t') return '\\t';
|
|
17
|
+
if (ch === '\n') return '\\n';
|
|
18
|
+
if (ch === '\v') return '\\v';
|
|
19
|
+
const code = ch.codePointAt(0);
|
|
20
|
+
// Other C0 controls and DEL have no ISO symbolic-control escape. Emit an
|
|
21
|
+
// octal escape so quoted output remains valid read-back syntax instead of
|
|
22
|
+
// leaking a raw control character into the output stream.
|
|
23
|
+
if (code < 0x20 || code === 0x7f) return `\\${code.toString(8)}\\`;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
function atomNeedsQuotes(name) {
|
|
11
28
|
if (!name) return true;
|
|
12
29
|
if (name === '[]' || name === '{}') return false;
|
|
@@ -22,15 +39,7 @@ function quoteAtom(name) {
|
|
|
22
39
|
for (const ch of name) {
|
|
23
40
|
if (ch === "'") out += "''";
|
|
24
41
|
else if (ch === '\\') out += '\\\\';
|
|
25
|
-
else
|
|
26
|
-
else if (ch === '\x07') out += '\\a';
|
|
27
|
-
else if (ch === '\b') out += '\\b';
|
|
28
|
-
else if (ch === '\r') out += '\\r';
|
|
29
|
-
else if (ch === '\f') out += '\\f';
|
|
30
|
-
else if (ch === '\t') out += '\\t';
|
|
31
|
-
else if (ch === '\n') out += '\\n';
|
|
32
|
-
else if (ch === '\v') out += '\\v';
|
|
33
|
-
else out += ch;
|
|
42
|
+
else out += quotedControlEscape(ch) ?? ch;
|
|
34
43
|
}
|
|
35
44
|
return out + "'";
|
|
36
45
|
}
|
|
@@ -60,15 +69,7 @@ function writeString(value) {
|
|
|
60
69
|
let out = '"';
|
|
61
70
|
for (const ch of value) {
|
|
62
71
|
if (ch === '"' || ch === '\\') out += `\\${ch}`;
|
|
63
|
-
else
|
|
64
|
-
else if (ch === '\x07') out += '\\a';
|
|
65
|
-
else if (ch === '\b') out += '\\b';
|
|
66
|
-
else if (ch === '\r') out += '\\r';
|
|
67
|
-
else if (ch === '\f') out += '\\f';
|
|
68
|
-
else if (ch === '\t') out += '\\t';
|
|
69
|
-
else if (ch === '\n') out += '\\n';
|
|
70
|
-
else if (ch === '\v') out += '\\v';
|
|
71
|
-
else out += ch;
|
|
72
|
+
else out += quotedControlEscape(ch) ?? ch;
|
|
72
73
|
}
|
|
73
74
|
return out + '"';
|
|
74
75
|
}
|
|
@@ -30,7 +30,7 @@ error-ordering alternative to an individual executable assertion.
|
|
|
30
30
|
|
|
31
31
|
| Standard area | Status | Current evidence |
|
|
32
32
|
| --- | --- | --- |
|
|
33
|
-
| Clause 6 — tokens, terms, lists, operators, quoted text | audit | `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, `wg17_syntax_high_risk`, writer/read-back regressions. |
|
|
33
|
+
| Clause 6 — tokens, terms, lists, operators, quoted text | audit | `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, `wg17_syntax_high_risk`, quoted-layout/escape error cases, writer/read-back regressions. |
|
|
34
34
|
| 7.1-7.3 — term types, term order, unification | audit | Standard-order, identity, finite-tree and occurs-check suites, Corrigendum 2 term predicates. |
|
|
35
35
|
| 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Cross-text `multifile/1` and ordering constraints require explicit shall-by-shall audit. |
|
|
36
36
|
| 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access. Public/private and multi-text requirements still need complete mapping. |
|
|
@@ -8,7 +8,7 @@ compliance audit and the remaining work before a full conformance claim.
|
|
|
8
8
|
|
|
9
9
|
| Standard area | Implementation | Representative executable coverage |
|
|
10
10
|
| --- | --- | --- |
|
|
11
|
-
| Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, `wg17_syntax_high_risk`, `wg17_invalid_octal_escape`, syntax error cases |
|
|
11
|
+
| Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, `wg17_syntax_high_risk`, `wg17_invalid_octal_escape`, `wg17_unterminated_quoted_token`, `wg17_literal_newline_in_quote`, `wg17_non_iso_escape`, syntax error cases |
|
|
12
12
|
| Clause 7 term order and unification | finite-tree unification, identity, standard order, errors | `unification_control_information`, `swipl_occurs_check`, `term_modes_and_ordering`, `logtalk_compare_standard_order` |
|
|
13
13
|
| Clause 7 control and exceptions | call, cut, conjunction, disjunction, if-then-else, catch and throw | `cut_control`, `control_and_terms`, `exceptions_and_flags`, `corrigenda_catch_callability`, `throw_copies_ball` |
|
|
14
14
|
| 8.2-8.5 term predicates | unification, Corrigendum 2 tests, comparison, sorting, creation and decomposition | `corrigenda_term_predicates`, `corrigenda_sort_keysort`, `logtalk_arg_unification`, `logtalk_univ`, associated error cases |
|
|
@@ -102,7 +102,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
|
|
|
102
102
|
Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
103
103
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
104
104
|
|
|
105
|
-
The corpus has
|
|
105
|
+
The corpus has 383 cases in `iso/` and 789 file-based conformance cases in
|
|
106
106
|
total. The generated `conformance-report.md` is the authoritative source for
|
|
107
107
|
current category totals. Together with regression, documentation-sync, API,
|
|
108
108
|
example, and book-example checks, `npm test` is the release gate.
|
|
@@ -85,3 +85,14 @@ numeric_escape_term_input(ok) :-
|
|
|
85
85
|
A == '\a',
|
|
86
86
|
B == '\a'.
|
|
87
87
|
|
|
88
|
+
|
|
89
|
+
%% goal: malformed_quoted_term_input(ok)
|
|
90
|
+
|
|
91
|
+
malformed_quoted_term_input(ok) :-
|
|
92
|
+
open('/tmp/eyeprolog-iso-bad-quoted-term.txt', write, Output, []),
|
|
93
|
+
put_code(Output, 39), nl(Output),
|
|
94
|
+
close(Output),
|
|
95
|
+
open('/tmp/eyeprolog-iso-bad-quoted-term.txt', read, Input, []),
|
|
96
|
+
catch(read(Input, _), error(syntax_error(read_term), _), Caught = yes),
|
|
97
|
+
close(Input),
|
|
98
|
+
Caught == yes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
% High-risk ISO syntax/write regressions, independently derived from
|
|
2
2
|
% ISO/IEC 13211-1 clauses 6.3, 6.4 and 7.10 and cross-checked against the
|
|
3
|
-
% public WG17 conformity-testing syntax cases (#1, #14-15, #28-31, #33-34, #301).
|
|
3
|
+
% public WG17 conformity-testing syntax cases (#1, #7-10, #14-15, #18, #28-31, #33-34, #301, #315-316).
|
|
4
4
|
|
|
5
5
|
%% goal: wg17_numeric_escape
|
|
6
6
|
wg17_numeric_escape :-
|
|
@@ -28,3 +28,18 @@ wg17_canonical_list :-
|
|
|
28
28
|
%% goal: wg17_zero_character_escape
|
|
29
29
|
wg17_zero_character_escape :-
|
|
30
30
|
writeq('\0\'), nl.
|
|
31
|
+
|
|
32
|
+
%% goal: wg17_continuation_escapes
|
|
33
|
+
wg17_continuation_escapes :-
|
|
34
|
+
writeq('\
|
|
35
|
+
'), nl,
|
|
36
|
+
writeq('\
|
|
37
|
+
a'), nl,
|
|
38
|
+
writeq('a\
|
|
39
|
+
b'), nl,
|
|
40
|
+
writeq('a\
|
|
41
|
+
b'), nl.
|
|
42
|
+
|
|
43
|
+
%% goal: wg17_non_symbolic_control_write
|
|
44
|
+
wg17_non_symbolic_control_write :-
|
|
45
|
+
writeq('\033\'), nl.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bad :- X = 0'\..
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
writeq(' ').
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
writeq('\e').
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
writeq('\u0021').
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parse line 1: bad escape sequence
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parse line 1: layout character in quoted term
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parse line 1: layout character in quoted term
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parse line 1: bad escape sequence
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parse line 1: bad escape sequence
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parse line 1: layout character in quoted term
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: layout character in quoted term
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: layout character in quoted term
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: layout character in quoted term
|
package/test/run-regression.mjs
CHANGED
|
@@ -696,6 +696,127 @@ c4 ?- call((!;1)).
|
|
|
696
696
|
assertEqual(result.stderr, '', 'stderr');
|
|
697
697
|
},
|
|
698
698
|
},
|
|
699
|
+
{
|
|
700
|
+
name: 'REPL rejects an unterminated quote at the line boundary instead of waiting',
|
|
701
|
+
run: () => {
|
|
702
|
+
const result = runCli([], {
|
|
703
|
+
input: "'\nhalt.\n",
|
|
704
|
+
});
|
|
705
|
+
assertEqual(result.status, 0, 'exit status');
|
|
706
|
+
assertEqual(result.stdout,
|
|
707
|
+
'?- parse line 1: unterminated quoted term.\n' +
|
|
708
|
+
'?- ',
|
|
709
|
+
'stdout');
|
|
710
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: 'REPL rejects a literal newline in a quoted token immediately',
|
|
715
|
+
run: () => {
|
|
716
|
+
const result = runCli([], {
|
|
717
|
+
// The first input line ends while a quote is open. ISO 6.4.2.1
|
|
718
|
+
// makes that newline a lexical error unless it is escaped by the
|
|
719
|
+
// immediately preceding backslash. The following true/0 proves
|
|
720
|
+
// that the top level did not consume another line as continuation.
|
|
721
|
+
input: "writeq('\ntrue.\nhalt.\n",
|
|
722
|
+
});
|
|
723
|
+
assertEqual(result.status, 0, 'exit status');
|
|
724
|
+
assertEqual(result.stdout,
|
|
725
|
+
'?- parse line 1: unterminated quoted term.\n' +
|
|
726
|
+
'?- true.\n' +
|
|
727
|
+
'?- ',
|
|
728
|
+
'stdout');
|
|
729
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: 'parser matches WG17 quoted-character and escape syntax cluster',
|
|
734
|
+
run: () => {
|
|
735
|
+
const invalid = [
|
|
736
|
+
['#2 lone quote', "'\n"],
|
|
737
|
+
['#5 literal horizontal tab', "writeq('\t')"],
|
|
738
|
+
['#6 literal newline', "writeq('\n')"],
|
|
739
|
+
['#11 backslash-space', String.raw`writeq('\ ')`],
|
|
740
|
+
['#12 backslash-horizontal-tab', "writeq('\\\t')"],
|
|
741
|
+
['#16 non-ISO c escape', String.raw`writeq('\ca')`],
|
|
742
|
+
['#241 non-ISO d escape', String.raw`writeq('\d')`],
|
|
743
|
+
['#17 non-ISO e escape', String.raw`writeq('\e')`],
|
|
744
|
+
['#19 non-ISO e in char_code/2', String.raw`char_code('\e', C)`],
|
|
745
|
+
['#21 non-ISO d in char_code/2', String.raw`char_code('\d', C)`],
|
|
746
|
+
['#22 non-ISO u escape', String.raw`writeq('\u1')`],
|
|
747
|
+
['#312 non-ISO Unicode escape', String.raw`writeq('\u0021')`],
|
|
748
|
+
['#314 non-ISO Unicode double-quote escape', String.raw`writeq("\u0021")`],
|
|
749
|
+
['#23 non-ISO character-code escape', String.raw`X = 0'\u1`],
|
|
750
|
+
['#24 unterminated quoted argument', "writeq('\n"],
|
|
751
|
+
['#26 continuation followed by unterminated quote', "'\\\n''"],
|
|
752
|
+
['#210 escaped dot character code', String.raw`X = 0'\.`],
|
|
753
|
+
['#211 escaped dot character code before layout', String.raw`X = 0'\. `],
|
|
754
|
+
];
|
|
755
|
+
for (const [label, source] of invalid) {
|
|
756
|
+
let error = null;
|
|
757
|
+
try {
|
|
758
|
+
parseGoalText(source);
|
|
759
|
+
} catch (caught) {
|
|
760
|
+
error = caught;
|
|
761
|
+
}
|
|
762
|
+
if (error == null) throw new Error(`${label} unexpectedly parsed`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const valid = [
|
|
766
|
+
['#7 empty continuation', "writeq('\\\n')"],
|
|
767
|
+
['#8 leading continuation', "writeq('\\\na')"],
|
|
768
|
+
['#9 embedded continuation', "writeq('a\\\nb')"],
|
|
769
|
+
['#10 continuation before space', "writeq('a\\\n b')"],
|
|
770
|
+
['#13 symbolic tab', String.raw`writeq('\t')`],
|
|
771
|
+
['#14 symbolic alert', String.raw`writeq('\a')`],
|
|
772
|
+
['#15 octal alert', String.raw`writeq('\7\')`],
|
|
773
|
+
['#18 octal escape', String.raw`writeq('\033\')`],
|
|
774
|
+
['#301 NUL escape', String.raw`writeq('\0\')`],
|
|
775
|
+
['#315 hexadecimal escape', String.raw`writeq('\x21\')`],
|
|
776
|
+
['#316 padded hexadecimal escape', String.raw`writeq('\x0021\')`],
|
|
777
|
+
['#38 double-quoted meta escapes', "\"\\'\\`\\\"\" = \"'`\"\"\""],
|
|
778
|
+
['#39 single-quoted meta escapes', "'\\'\\`\\\"' = '''`\"'"],
|
|
779
|
+
['#40 writeq meta escapes', "writeq('\\'\\`\\\"\\\"')"],
|
|
780
|
+
['#41 meta backslash escape', String.raw`('\\') = (\)`],
|
|
781
|
+
];
|
|
782
|
+
for (const [label, source] of valid) {
|
|
783
|
+
try {
|
|
784
|
+
parseGoalText(source);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
throw new Error(`${label} should parse: ${error?.message ?? error}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
name: 'stream term input reports malformed quoted layout as syntax_error',
|
|
793
|
+
run: () => {
|
|
794
|
+
for (const [label, input] of [
|
|
795
|
+
['lone quote', "'\n"],
|
|
796
|
+
['literal newline', "writeq('\n').\n"],
|
|
797
|
+
['literal tab', "writeq('\t').\n"],
|
|
798
|
+
]) {
|
|
799
|
+
let error = null;
|
|
800
|
+
try {
|
|
801
|
+
runEyeProlog('', { goal: 'read(X)', ioOptions: { input } });
|
|
802
|
+
} catch (caught) {
|
|
803
|
+
error = caught;
|
|
804
|
+
}
|
|
805
|
+
assertEqual(error?.message, 'error(syntax_error(read_term))', `${label} read error`);
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
name: 'writeq uses ISO numeric escapes for non-symbolic control characters',
|
|
811
|
+
run: () => {
|
|
812
|
+
const result = runCli([], {
|
|
813
|
+
input: "writeq('\\033\\').\nhalt.\n",
|
|
814
|
+
});
|
|
815
|
+
assertEqual(result.status, 0, 'exit status');
|
|
816
|
+
assertEqual(result.stdout, "?- '\\33\\' true.\n?- ", 'stdout');
|
|
817
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
818
|
+
},
|
|
819
|
+
},
|
|
699
820
|
{
|
|
700
821
|
name: 'REPL read predicates consume following interactive term input',
|
|
701
822
|
run: () => {
|
|
@@ -719,6 +840,53 @@ c4 ?- call((!;1)).
|
|
|
719
840
|
assertEqual(result.stderr, '', 'stderr');
|
|
720
841
|
},
|
|
721
842
|
},
|
|
843
|
+
{
|
|
844
|
+
name: 'REPL term input is on demand in conjunctions and Ctrl-D does not exit the top level',
|
|
845
|
+
run: () => {
|
|
846
|
+
if (process.platform === 'win32') return;
|
|
847
|
+
const available = spawnSync('sh', ['-c',
|
|
848
|
+
'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
|
|
849
|
+
if (available.status !== 0) return;
|
|
850
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
|
|
851
|
+
const scriptCommand =
|
|
852
|
+
`{ printf 'read(X), read(Y).\n'; sleep 0.15; ` +
|
|
853
|
+
`printf 'foo.\n'; sleep 0.15; printf 'bar.\n'; sleep 0.15; ` +
|
|
854
|
+
`printf 'read(Z).\n'; sleep 0.15; printf '\\004'; sleep 0.15; ` +
|
|
855
|
+
`printf 'true.\n'; sleep 0.15; printf 'halt.\n'; } | ` +
|
|
856
|
+
`script -qefc ${shellQuote(command)} /dev/null`;
|
|
857
|
+
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
858
|
+
cwd: packageRoot,
|
|
859
|
+
encoding: 'utf8',
|
|
860
|
+
timeout: 5000,
|
|
861
|
+
});
|
|
862
|
+
assertEqual(result.error?.code, undefined, 'interactive read timeout');
|
|
863
|
+
assertEqual(result.status, 0, 'exit status');
|
|
864
|
+
assertIncludes(result.stdout, 'X = foo, Y = bar.', 'conjunction reads');
|
|
865
|
+
assertIncludes(result.stdout, 'Z = end_of_file.', 'Ctrl-D read result');
|
|
866
|
+
assertIncludes(result.stdout, '?- true.', 'top level resumes after Ctrl-D');
|
|
867
|
+
assertIncludes(result.stdout, ' true.', 'post-EOF query executes');
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
name: 'interactive user_input hook serves reads reached through user predicates',
|
|
872
|
+
run: () => {
|
|
873
|
+
const program = Program.parse('pair(A, B) :- read(A), read(B).\n');
|
|
874
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
875
|
+
const stream = solver.io.resolve('user_input');
|
|
876
|
+
const pending = ['left.\n', 'right.\n'];
|
|
877
|
+
let requests = 0;
|
|
878
|
+
stream.interactiveReadTerm = () => {
|
|
879
|
+
requests++;
|
|
880
|
+
return pending.shift() ?? null;
|
|
881
|
+
};
|
|
882
|
+
const goal = parseGoalText('pair(X, Y)');
|
|
883
|
+
const answers = [...solver.solve([goal], new Env(), 0)];
|
|
884
|
+
assertEqual(answers.length, 1, 'answer count');
|
|
885
|
+
assertEqual(termToString(copyResolved(goal.args[0], answers[0])), 'left', 'first read');
|
|
886
|
+
assertEqual(termToString(copyResolved(goal.args[1], answers[0])), 'right', 'second read');
|
|
887
|
+
assertEqual(requests, 2, 'on-demand read count');
|
|
888
|
+
},
|
|
889
|
+
},
|
|
722
890
|
{
|
|
723
891
|
name: 'REPL releases terminal signals while a query computes',
|
|
724
892
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5276,17 +5276,27 @@ city('München').
|
|
|
5276
5276
|
message("café").
|
|
5277
5277
|
```
|
|
5278
5278
|
|
|
5279
|
-
Inside a quoted atom, a single quote is doubled: `'don''t'`.
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
and
|
|
5283
|
-
|
|
5284
|
-
are
|
|
5285
|
-
|
|
5286
|
-
quoted-character escapes
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5279
|
+
Inside a quoted atom, a single quote is doubled: `'don''t'`. EyeProlog follows
|
|
5280
|
+
the ISO quoted-character grammar rather than accepting arbitrary backslash
|
|
5281
|
+
escapes. The symbolic control escapes are `\a`, `\b`, `\r`, `\f`, `\t`,
|
|
5282
|
+
`\n`, and `\v`; the meta characters backslash, single quote, double quote, and
|
|
5283
|
+
back quote may be escaped after a backslash; and numeric octal or hexadecimal
|
|
5284
|
+
escapes are terminated by a backslash. For example, `'\7\'` and `'\x7\'`
|
|
5285
|
+
both denote the alert character. Forms such as `\c`, `\d`, `\e`, `\u`, `\.`
|
|
5286
|
+
and `\ ` are not ISO quoted-character escapes and are syntax errors.
|
|
5287
|
+
|
|
5288
|
+
A literal layout character other than ordinary space is not a quoted
|
|
5289
|
+
character. In particular, a literal tab or newline inside quotes is a syntax
|
|
5290
|
+
error. A quoted token can cross a line boundary only through a continuation
|
|
5291
|
+
escape: a backslash immediately followed by the newline, which contributes no
|
|
5292
|
+
character to the atom. The NUL character is written readably as `'\0\'`;
|
|
5293
|
+
digits `8` and `9` are not octal digits, so forms such as `'\8\'` are syntax
|
|
5294
|
+
errors. `writeq/1` uses octal escapes for other non-symbolic control characters,
|
|
5295
|
+
for example ESC is written as `'\33\'`. Double-quoted lists use the same
|
|
5296
|
+
quoted-character rules. Whitespace is insignificant between tokens, and a `%`
|
|
5297
|
+
comment continues to the end of its line. Doubling the active delimiter is
|
|
5298
|
+
also accepted inside either quoted form, so `""` inside double-quoted notation
|
|
5299
|
+
denotes one literal double quote character.
|
|
5290
5300
|
|
|
5291
5301
|
Graphic atoms may contain `#$&*+-/<=>@^~\;`. A colon is the Part 2 module
|
|
5292
5302
|
qualification operator in `Module:Goal`; quote an atom whose name itself
|
|
@@ -6228,21 +6238,27 @@ not be a valid right operand of the displayed `=/2`, EyeProlog adds parentheses,
|
|
|
6228
6238
|
for example `T = (a = b).` rather than the invalid `T = a = b.`. Use `[file].`
|
|
6229
6239
|
or `['file.pl'].` to
|
|
6230
6240
|
consult local source, and `halt.` or `halt(Status).` to leave the top level.
|
|
6231
|
-
|
|
6232
|
-
next full-stop-terminated Prolog term
|
|
6233
|
-
treating the
|
|
6241
|
+
When `read/1-2` or `read_term/2-3` actually reaches interactive
|
|
6242
|
+
`user_input`, the top level requests the next full-stop-terminated Prolog term
|
|
6243
|
+
with a `|: ` input prompt instead of treating the terminal stream as already
|
|
6244
|
+
exhausted. The request is made at execution time, so multiple reads in one goal
|
|
6245
|
+
and reads reached through user predicates work independently. For example:
|
|
6234
6246
|
|
|
6235
6247
|
```text
|
|
6236
|
-
?- read(X).
|
|
6248
|
+
?- read(X), read(Y).
|
|
6237
6249
|
|: hello.
|
|
6238
|
-
|
|
6250
|
+
|: world.
|
|
6251
|
+
X = hello, Y = world.
|
|
6239
6252
|
```
|
|
6240
6253
|
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6254
|
+
Typing `Ctrl-D` at an empty `|: ` prompt makes that Prolog read return
|
|
6255
|
+
`end_of_file`; it does not close the surrounding EyeProlog top-level loop, so a
|
|
6256
|
+
new `?- ` query can still be entered afterwards. The top-level prompts and this
|
|
6257
|
+
terminal EOF convention are host-interface behavior rather than part of
|
|
6258
|
+
ISO/IEC 13211-1; terms supplied to the reads are parsed by the same ISO
|
|
6259
|
+
term-input machinery as `read/1-2` and `read_term/2-3` on other text streams.
|
|
6260
|
+
Up and Down recall queries from the current session. Explicit `eyeprolog -h`
|
|
6261
|
+
displays command-line help.
|
|
6246
6262
|
|
|
6247
6263
|
### Selecting goals
|
|
6248
6264
|
|
|
@@ -6953,7 +6969,7 @@ precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
|
|
|
6953
6969
|
maps language families to representative executable cases.
|
|
6954
6970
|
|
|
6955
6971
|
The complete suite must pass before release. The file-based conformance corpus
|
|
6956
|
-
contains
|
|
6972
|
+
contains 789 cases, including 383 focused ISO
|
|
6957
6973
|
cases derived from the success, failure, mode, and error behavior in
|
|
6958
6974
|
ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
|
|
6959
6975
|
Separate exact-output suites check 189 normal
|