eyeprolog 1.2.21 → 1.2.23
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/package.json +1 -1
- package/src/iso.js +20 -18
- package/src/number-value.js +28 -0
- package/src/parser.js +28 -0
- package/src/program.js +9 -4
- package/src/repl.js +20 -5
- package/src/solver.js +5 -2
- package/src/term.js +6 -3
- package/test/run-regression.mjs +74 -11
- package/the-art-of-eyeprolog.md +14 -5
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
isDecimalInteger, listFromItems, numberTerm, numberTextFromDouble,
|
|
6
6
|
properListItems, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
8
|
-
import {
|
|
8
|
+
import { sameNumberValue } from './number-value.js';
|
|
9
|
+
import { createParserOperatorState, parseClauses, parseGoalText, parseNumberTokenText } from './parser.js';
|
|
9
10
|
import { formatTermForWrite } from './write.js';
|
|
10
11
|
import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
|
|
11
12
|
|
|
@@ -220,7 +221,8 @@ function subsumesTerm(general, specific, env) {
|
|
|
220
221
|
}
|
|
221
222
|
continue;
|
|
222
223
|
}
|
|
223
|
-
if (left.type !== right.type || left.
|
|
224
|
+
if (left.type !== right.type || left.arity !== right.arity) return false;
|
|
225
|
+
if (left.type === NUMBER ? !sameNumberValue(left.name, right.name) : left.name !== right.name) return false;
|
|
224
226
|
for (let i = left.arity - 1; i >= 0; i--) pending.push([left.args[i], right.args[i]]);
|
|
225
227
|
}
|
|
226
228
|
return true;
|
|
@@ -239,7 +241,8 @@ function* nonIdentity({ goal, env }) {
|
|
|
239
241
|
function identical(left, right, env) {
|
|
240
242
|
left = deref(left, env);
|
|
241
243
|
right = deref(right, env);
|
|
242
|
-
if (left.type !== right.type || left.
|
|
244
|
+
if (left.type !== right.type || left.arity !== right.arity) return false;
|
|
245
|
+
if (left.type === NUMBER ? !sameNumberValue(left.name, right.name) : left.name !== right.name) return false;
|
|
243
246
|
if (left.type === VAR) return left.name === right.name;
|
|
244
247
|
for (let i = 0; i < left.arity; i++) if (!identical(left.args[i], right.args[i], env)) return false;
|
|
245
248
|
return true;
|
|
@@ -1549,9 +1552,7 @@ function quotedNumberSign(text, start) {
|
|
|
1549
1552
|
}
|
|
1550
1553
|
|
|
1551
1554
|
function parseIsoNumber(text) {
|
|
1552
|
-
|
|
1553
|
-
// with the numeric token itself rather than trailing layout text.
|
|
1554
|
-
if (text.length === 0 || /[\u0009-\u000d\u0020]$/.test(text)) return null;
|
|
1555
|
+
if (text.length === 0) return null;
|
|
1555
1556
|
let position = skipNumberLayout(text, 0);
|
|
1556
1557
|
let sign = '';
|
|
1557
1558
|
|
|
@@ -1587,10 +1588,7 @@ function parseIsoNumber(text) {
|
|
|
1587
1588
|
// ISO floating-point syntax requires a decimal fraction before an exponent.
|
|
1588
1589
|
if (/^-?\d+[eE][+-]?\d+$/.test(numericText)) return null;
|
|
1589
1590
|
try {
|
|
1590
|
-
const
|
|
1591
|
-
if (parsed.type !== COMPOUND || parsed.name !== 'number_chars_value' ||
|
|
1592
|
-
parsed.arity !== 1 || parsed.args[0].type !== NUMBER) return null;
|
|
1593
|
-
const value = parsed.args[0];
|
|
1591
|
+
const value = parseNumberTokenText(numericText);
|
|
1594
1592
|
if (isDecimalInteger(value.name)) return numberTerm(BigInt(value.name).toString());
|
|
1595
1593
|
const finite = Number(value.name);
|
|
1596
1594
|
if (!Number.isFinite(finite)) return null;
|
|
@@ -1601,14 +1599,18 @@ function parseIsoNumber(text) {
|
|
|
1601
1599
|
}
|
|
1602
1600
|
|
|
1603
1601
|
function sameNumber(left, right) {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1602
|
+
return sameNumberValue(left.name, right.name);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function canonicalNumberText(value) {
|
|
1606
|
+
if (isDecimalInteger(value.name)) return BigInt(value.name).toString();
|
|
1607
|
+
const finite = Number(value.name);
|
|
1608
|
+
if (Number.isFinite(finite) && /^-?\d+\.\d+(?:[eE][+-]?\d+)?$/.test(value.name)) {
|
|
1609
|
+
return value.name;
|
|
1608
1610
|
}
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
return
|
|
1611
|
+
const text = numberTextFromDouble(finite);
|
|
1612
|
+
if (text == null) throw new PrologError('representation_error(max_float)');
|
|
1613
|
+
return text;
|
|
1612
1614
|
}
|
|
1613
1615
|
|
|
1614
1616
|
function numberListText(list, env, kind, valueIsBound) {
|
|
@@ -1650,7 +1652,7 @@ function numberListBuiltin(kind) {
|
|
|
1650
1652
|
if (sameNumber(value, parsed)) yield next;
|
|
1651
1653
|
return;
|
|
1652
1654
|
}
|
|
1653
|
-
const items = characters(value
|
|
1655
|
+
const items = characters(canonicalNumberText(value)).map((ch) =>
|
|
1654
1656
|
kind === 'chars' ? atom(ch) : numberTerm(ch.codePointAt(0)));
|
|
1655
1657
|
if (unify(goal.args[1], listFromItems(items), next)) yield next;
|
|
1656
1658
|
return;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Numeric value identity shared by term semantics and scalar indexes. Keep
|
|
2
|
+
// integer and float terms distinct while ignoring insignificant spelling
|
|
3
|
+
// differences within either ISO numeric type.
|
|
4
|
+
const decimalInteger = (text) => /^-?\d+$/.test(text ?? '');
|
|
5
|
+
|
|
6
|
+
const finiteFloat = (text) => {
|
|
7
|
+
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text ?? '')) return null;
|
|
8
|
+
const value = Number(text);
|
|
9
|
+
return Number.isFinite(value) ? value : null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function sameNumberValue(left, right) {
|
|
13
|
+
const leftInteger = decimalInteger(left);
|
|
14
|
+
const rightInteger = decimalInteger(right);
|
|
15
|
+
if (leftInteger || rightInteger) {
|
|
16
|
+
return leftInteger && rightInteger && BigInt(left) === BigInt(right);
|
|
17
|
+
}
|
|
18
|
+
const leftValue = finiteFloat(left);
|
|
19
|
+
const rightValue = finiteFloat(right);
|
|
20
|
+
return leftValue != null && rightValue != null && leftValue === rightValue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function numberValueKey(text) {
|
|
24
|
+
if (decimalInteger(text)) return `integer:${BigInt(text)}`;
|
|
25
|
+
const value = finiteFloat(text);
|
|
26
|
+
if (value == null) return `invalid:${text}`;
|
|
27
|
+
return `float:${Object.is(value, -0) ? 0 : value}`;
|
|
28
|
+
}
|
package/src/parser.js
CHANGED
|
@@ -433,6 +433,18 @@ class Parser {
|
|
|
433
433
|
this.take();
|
|
434
434
|
this.take();
|
|
435
435
|
let value = this.take();
|
|
436
|
+
if (value) {
|
|
437
|
+
const firstCode = value.charCodeAt(0);
|
|
438
|
+
if (firstCode >= 0xd800 && firstCode <= 0xdbff) {
|
|
439
|
+
const secondCode = this.peek().charCodeAt(0);
|
|
440
|
+
if (secondCode < 0xdc00 || secondCode > 0xdfff) {
|
|
441
|
+
throw new Error(`parse line ${line}: bad character code constant`);
|
|
442
|
+
}
|
|
443
|
+
value += this.take();
|
|
444
|
+
} else if (firstCode >= 0xdc00 && firstCode <= 0xdfff) {
|
|
445
|
+
throw new Error(`parse line ${line}: bad character code constant`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
436
448
|
if (!value || (value !== ' ' && isWhitespaceCode(value.charCodeAt(0)))) {
|
|
437
449
|
throw new Error(`parse line ${line}: bad character code constant`);
|
|
438
450
|
}
|
|
@@ -1294,6 +1306,22 @@ export function parseProgramText(source, options = {}) {
|
|
|
1294
1306
|
return parseClauses(source, options);
|
|
1295
1307
|
}
|
|
1296
1308
|
|
|
1309
|
+
export function parseNumberTokenText(text) {
|
|
1310
|
+
const source = String(text ?? '');
|
|
1311
|
+
const parser = new Parser(source, {
|
|
1312
|
+
includeDefaultOperators: false,
|
|
1313
|
+
sourceMetadata: false,
|
|
1314
|
+
});
|
|
1315
|
+
// Inspect the tokenizer position before requesting another token: advancing
|
|
1316
|
+
// would skip trailing layout and make `3 ` or `3/**/` look complete. A
|
|
1317
|
+
// literal space consumed by the character-code token `0' ` is already part
|
|
1318
|
+
// of parser.pos and is therefore correctly accepted.
|
|
1319
|
+
if (parser.token.type !== TOK.NUMBER || parser.pos !== source.length) {
|
|
1320
|
+
throw new Error('not exactly one number token');
|
|
1321
|
+
}
|
|
1322
|
+
return numberTerm(parser.token.text);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1297
1325
|
export function parseGoalText(text, options = {}) {
|
|
1298
1326
|
const clauses = parseClauses(`zz_goal((${text})).`, options);
|
|
1299
1327
|
const head = clauses[0]?.head;
|
package/src/program.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Program representation and clause indexing.
|
|
2
2
|
// Indexes are deliberately conservative: they speed up common scalar arguments but never replace unification as the final check.
|
|
3
3
|
import { ATOM, COMPOUND, VAR, Env, atom, compound, deref, flattenConjunction, isScalar, numberTerm, properListItems, termToString, variable } from './term.js';
|
|
4
|
+
import { numberValueKey } from './number-value.js';
|
|
4
5
|
import { formatTermForWrite } from './write.js';
|
|
5
6
|
import {
|
|
6
7
|
ISO_OPERATOR_DEFINITIONS,
|
|
@@ -1238,15 +1239,19 @@ function scalarBuckets(index, term) {
|
|
|
1238
1239
|
}
|
|
1239
1240
|
|
|
1240
1241
|
function argumentBucket(index, term) {
|
|
1241
|
-
return scalarBuckets(index, term).get(term.name) ?? null;
|
|
1242
|
+
return scalarBuckets(index, term).get(scalarBucketKey(term.type, term.name)) ?? null;
|
|
1242
1243
|
}
|
|
1243
1244
|
|
|
1244
1245
|
function addArgumentBucket(index, term, clause) {
|
|
1245
|
-
addClauseBucket(scalarBuckets(index, term), term.name, clause);
|
|
1246
|
+
addClauseBucket(scalarBuckets(index, term), scalarBucketKey(term.type, term.name), clause);
|
|
1246
1247
|
}
|
|
1247
1248
|
|
|
1248
1249
|
function scalarIndexKey(term) {
|
|
1249
|
-
return `${term.type}\u0000${term.name}`;
|
|
1250
|
+
return `${term.type}\u0000${scalarBucketKey(term.type, term.name)}`;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function scalarBucketKey(type, name) {
|
|
1254
|
+
return type === 'number' ? numberValueKey(name) : name;
|
|
1250
1255
|
}
|
|
1251
1256
|
|
|
1252
1257
|
function addClauseBucket(buckets, key, clause) {
|
|
@@ -1276,7 +1281,7 @@ function indexCompactOne(index, type, name, clause, clauses = null, clausePositi
|
|
|
1276
1281
|
index.sawScalar = true;
|
|
1277
1282
|
if (clauses && clausePosition > 0) index.fallback = clauses.slice(0, clausePosition);
|
|
1278
1283
|
}
|
|
1279
|
-
addClauseBucket(compactScalarBuckets(index, type), name, clause);
|
|
1284
|
+
addClauseBucket(compactScalarBuckets(index, type), scalarBucketKey(type, name), clause);
|
|
1280
1285
|
} else if (index.sawScalar) {
|
|
1281
1286
|
index.fallback.push(clause);
|
|
1282
1287
|
}
|
package/src/repl.js
CHANGED
|
@@ -31,13 +31,23 @@ export async function runRepl(engine, options = {}) {
|
|
|
31
31
|
if (text == null) break;
|
|
32
32
|
if (!text.trim()) continue;
|
|
33
33
|
|
|
34
|
+
let resultIndent = ' ';
|
|
34
35
|
try {
|
|
35
36
|
const goal = parseGoal(engine, state, text);
|
|
37
|
+
// A complete, successfully parsed query has left the top-level reader
|
|
38
|
+
// and is about to execute. Show two spaces immediately so a terminal
|
|
39
|
+
// user can distinguish that state from a reader waiting for more text;
|
|
40
|
+
// the final result adds the third indentation space below. A direct
|
|
41
|
+
// halt has no result and exits immediately, so it needs no marker.
|
|
42
|
+
if (!isHaltGoal(goal)) {
|
|
43
|
+
output.write(' ');
|
|
44
|
+
resultIndent = ' ';
|
|
45
|
+
}
|
|
36
46
|
if (!options.isoStrict && isUseModuleGoal(goal)) {
|
|
37
47
|
sources.push({ text: `:- ${text}.\n`, filename: '<repl>' });
|
|
38
48
|
state = makeState(engine, sources, output, options, state, reader);
|
|
39
49
|
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
40
|
-
output.write('
|
|
50
|
+
output.write(' true.\n');
|
|
41
51
|
continue;
|
|
42
52
|
}
|
|
43
53
|
const consultFiles = options.isoStrict ? null : consultDesignations(engine, goal);
|
|
@@ -45,7 +55,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
45
55
|
for (const filename of consultFiles) sources.push(await readSource(filename));
|
|
46
56
|
state = makeState(engine, sources, output, options, state, reader);
|
|
47
57
|
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
48
|
-
output.write('
|
|
58
|
+
output.write(' true.\n');
|
|
49
59
|
continue;
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -65,7 +75,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
65
75
|
exitCode = error.code;
|
|
66
76
|
break;
|
|
67
77
|
}
|
|
68
|
-
output.write(
|
|
78
|
+
output.write(`${resultIndent}${formatError(engine, state, error)}\n`);
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
} catch (error) {
|
|
@@ -442,6 +452,11 @@ function isUseModuleGoal(goal) {
|
|
|
442
452
|
return goal.type === 'compound' && goal.name === 'use_module' && [1, 2].includes(goal.arity);
|
|
443
453
|
}
|
|
444
454
|
|
|
455
|
+
function isHaltGoal(goal) {
|
|
456
|
+
return (goal.type === 'atom' && goal.name === 'halt') ||
|
|
457
|
+
(goal.type === 'compound' && goal.name === 'halt' && goal.arity === 1);
|
|
458
|
+
}
|
|
459
|
+
|
|
445
460
|
function consultDesignations(engine, goal) {
|
|
446
461
|
if (goal.type === 'atom' && goal.name === '[]') return [];
|
|
447
462
|
if (goal.type !== 'compound' || goal.name !== '.' || goal.arity !== 2) return null;
|
|
@@ -481,7 +496,7 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
481
496
|
}
|
|
482
497
|
|
|
483
498
|
if (current.result.done) {
|
|
484
|
-
output.write('
|
|
499
|
+
output.write(' false.\n');
|
|
485
500
|
return null;
|
|
486
501
|
}
|
|
487
502
|
|
|
@@ -498,7 +513,7 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
498
513
|
if (formattingAfterAdvance) output.write(' ');
|
|
499
514
|
formattingAfterAdvance = false;
|
|
500
515
|
output.write(current.output);
|
|
501
|
-
output.write(`${firstAnswer ? '
|
|
516
|
+
output.write(`${firstAnswer ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
502
517
|
answersShown++;
|
|
503
518
|
firstAnswer = false;
|
|
504
519
|
if (!next.error && next.result.done) {
|
package/src/solver.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
COMPOUND, Env, compound, copyResolved, deref, emptyList, flattenConjunction, freshTerm,
|
|
5
5
|
numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variantTerms,
|
|
6
6
|
} from './term.js';
|
|
7
|
+
import { sameNumberValue } from './number-value.js';
|
|
7
8
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
8
9
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
9
10
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
|
|
@@ -1089,11 +1090,13 @@ function isScalarTerm(term) {
|
|
|
1089
1090
|
}
|
|
1090
1091
|
|
|
1091
1092
|
function sameScalarTerm(left, right) {
|
|
1092
|
-
return isScalarTerm(left) && isScalarTerm(right) && left.type === right.type &&
|
|
1093
|
+
return isScalarTerm(left) && isScalarTerm(right) && left.type === right.type &&
|
|
1094
|
+
(left.type === 'number' ? sameNumberValue(left.name, right.name) : left.name === right.name);
|
|
1093
1095
|
}
|
|
1094
1096
|
|
|
1095
1097
|
function sameGroundTerm(left, right) {
|
|
1096
|
-
if (left?.type !== right?.type
|
|
1098
|
+
if (left?.type !== right?.type) return false;
|
|
1099
|
+
if (left?.type === 'number' ? !sameNumberValue(left.name, right.name) : left?.name !== right?.name) return false;
|
|
1097
1100
|
const arity = left.args?.length ?? 0;
|
|
1098
1101
|
if (arity !== (right.args?.length ?? 0)) return false;
|
|
1099
1102
|
for (let i = 0; i < arity; i++) if (!sameGroundTerm(left.args[i], right.args[i])) return false;
|
package/src/term.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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';
|
|
4
|
+
|
|
3
5
|
export const VAR = 'var';
|
|
4
6
|
export const ATOM = 'atom';
|
|
5
7
|
export const STRING = 'string';
|
|
@@ -247,7 +249,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
if (isScalar(a)) {
|
|
250
|
-
if (a.name !== b.name) return false;
|
|
252
|
+
if (a.type === NUMBER ? !sameNumberValue(a.name, b.name) : a.name !== b.name) return false;
|
|
251
253
|
continue;
|
|
252
254
|
}
|
|
253
255
|
|
|
@@ -497,7 +499,8 @@ export function variantTerms(left, leftEnv, right, rightEnv, pairs = new Map(),
|
|
|
497
499
|
reverse.set(right.name, left.name);
|
|
498
500
|
return true;
|
|
499
501
|
}
|
|
500
|
-
if (left.type !== right.type || left.
|
|
502
|
+
if (left.type !== right.type || left.arity !== right.arity) return false;
|
|
503
|
+
if (left.type === NUMBER ? !sameNumberValue(left.name, right.name) : left.name !== right.name) return false;
|
|
501
504
|
for (let i = 0; i < left.arity; i++) {
|
|
502
505
|
if (!variantTerms(left.args[i], leftEnv, right.args[i], rightEnv, pairs, reverse)) return false;
|
|
503
506
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -567,6 +567,63 @@ c4 ?- call((!;1)).
|
|
|
567
567
|
}
|
|
568
568
|
},
|
|
569
569
|
},
|
|
570
|
+
{
|
|
571
|
+
name: 'number conversion accepts ISO space and Unicode character-code constants exactly',
|
|
572
|
+
run: () => {
|
|
573
|
+
const source = String.raw`
|
|
574
|
+
?- N = 0' .
|
|
575
|
+
N = 32.
|
|
576
|
+
?- number_chars(N,"0' ").
|
|
577
|
+
N = 32.
|
|
578
|
+
?- number_codes(N,[48,39,32]).
|
|
579
|
+
N = 32.
|
|
580
|
+
?- N = 0'😀.
|
|
581
|
+
N = 128512.
|
|
582
|
+
?- number_chars(N,"0'😀").
|
|
583
|
+
N = 128512.
|
|
584
|
+
?- number_codes(N,[48,39,128512]).
|
|
585
|
+
N = 128512.
|
|
586
|
+
?- number_codes(N,[48,39,34]).
|
|
587
|
+
N = 34.
|
|
588
|
+
?- number_codes(N,[48,39,96]).
|
|
589
|
+
N = 96.
|
|
590
|
+
?- number_codes(N,[48,39,92,92]).
|
|
591
|
+
N = 92.
|
|
592
|
+
?- number_chars(01,Chars).
|
|
593
|
+
Chars = "1".
|
|
594
|
+
?- number_codes(01,Codes).
|
|
595
|
+
Codes = [49].
|
|
596
|
+
?- 1.2 = 1.20.
|
|
597
|
+
true.
|
|
598
|
+
?- 1.2 == 1.20.
|
|
599
|
+
true.
|
|
600
|
+
?- 1 = 1.0.
|
|
601
|
+
false.
|
|
602
|
+
?- number_chars(1.20,C), number_chars(Y,C), 1.20 == Y.
|
|
603
|
+
C = "1.20", Y = 1.2.
|
|
604
|
+
`;
|
|
605
|
+
const result = publicApi.runQuads(source);
|
|
606
|
+
assertEqual(result.total, 15, 'quad total');
|
|
607
|
+
assertEqual(result.passed, 15, 'quad passed');
|
|
608
|
+
assertEqual(result.stdout, 'quads: 15 run, 15 passed, 0 failed.\n', 'quad report');
|
|
609
|
+
|
|
610
|
+
for (const goal of [
|
|
611
|
+
'number_chars(N,"3/**/")',
|
|
612
|
+
'number_codes(N,[51,47,42,42,47])',
|
|
613
|
+
'number_chars(N,"0\' ")',
|
|
614
|
+
'number_codes(N,[48,39,32,32])',
|
|
615
|
+
]) {
|
|
616
|
+
let caught = null;
|
|
617
|
+
try {
|
|
618
|
+
publicApi.run('', { goal });
|
|
619
|
+
} catch (error) {
|
|
620
|
+
caught = error;
|
|
621
|
+
}
|
|
622
|
+
if (caught == null) throw new Error(`${goal} should reject trailing layout`);
|
|
623
|
+
assertIncludes(String(caught?.message ?? caught), 'syntax_error(number)', goal);
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
},
|
|
570
627
|
{
|
|
571
628
|
name: 'number conversion rejects parenthesized numeric terms',
|
|
572
629
|
run: () => {
|
|
@@ -820,7 +877,7 @@ c4 ?- call((!;1)).
|
|
|
820
877
|
},
|
|
821
878
|
},
|
|
822
879
|
{
|
|
823
|
-
name: 'REPL answer
|
|
880
|
+
name: 'REPL query and answer prompts distinguish waiting from computation',
|
|
824
881
|
run: () => {
|
|
825
882
|
const helper = `
|
|
826
883
|
import { spawn } from 'node:child_process';
|
|
@@ -833,9 +890,11 @@ c4 ?- call((!;1)).
|
|
|
833
890
|
child.stderr.setEncoding('utf8');
|
|
834
891
|
let stdout = '';
|
|
835
892
|
let stderr = '';
|
|
893
|
+
let sawQueryComputingPrompt = false;
|
|
836
894
|
let sawComputingPrompt = false;
|
|
837
895
|
child.stdout.on('data', (text) => {
|
|
838
896
|
stdout += text;
|
|
897
|
+
if (stdout.endsWith('?- ')) sawQueryComputingPrompt = true;
|
|
839
898
|
if (stdout.endsWith('\\n; ')) sawComputingPrompt = true;
|
|
840
899
|
});
|
|
841
900
|
child.stderr.on('data', (text) => { stderr += text; });
|
|
@@ -852,6 +911,10 @@ c4 ?- call((!;1)).
|
|
|
852
911
|
|
|
853
912
|
child.stdin.write('use_module(library(prologue)).\\n');
|
|
854
913
|
await waitFor(() => stdout.includes(' true.\\n?- '), 'module import');
|
|
914
|
+
sawQueryComputingPrompt = false;
|
|
915
|
+
child.stdin.write('between(0,0xff,I),I<0.\\n');
|
|
916
|
+
await waitFor(() => sawQueryComputingPrompt, 'query computing prompt');
|
|
917
|
+
await waitFor(() => stdout.endsWith(' false.\\n?- '), 'query result');
|
|
855
918
|
child.stdin.write('(N = 0; N = 1; (call_nth(repeat, 100000), N = 2)).\\n');
|
|
856
919
|
await waitFor(() => stdout.endsWith(' N = 0\\n;'), 'waiting prompt');
|
|
857
920
|
child.stdin.write(';\\n');
|
|
@@ -862,7 +925,7 @@ c4 ?- call((!;1)).
|
|
|
862
925
|
child.stdin.write('halt.\\n');
|
|
863
926
|
const status = await new Promise((resolve) => child.once('exit', resolve));
|
|
864
927
|
if (status !== 0) throw new Error('child status ' + status + '; stderr=' + stderr);
|
|
865
|
-
process.stdout.write('waiting;computing;formatting');
|
|
928
|
+
process.stdout.write('query-computing;waiting;computing;formatting');
|
|
866
929
|
`;
|
|
867
930
|
const result = spawnSync(process.execPath, [
|
|
868
931
|
'--input-type=module',
|
|
@@ -871,7 +934,7 @@ c4 ?- call((!;1)).
|
|
|
871
934
|
], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
|
|
872
935
|
if (result.error) throw result.error;
|
|
873
936
|
assertEqual(result.status, 0, `prompt helper status; stderr=${result.stderr}`);
|
|
874
|
-
assertEqual(result.stdout, 'waiting;computing;formatting', 'prompt state sequence');
|
|
937
|
+
assertEqual(result.stdout, 'query-computing;waiting;computing;formatting', 'prompt state sequence');
|
|
875
938
|
},
|
|
876
939
|
},
|
|
877
940
|
{
|
|
@@ -941,9 +1004,9 @@ c4 ?- call((!;1)).
|
|
|
941
1004
|
});
|
|
942
1005
|
assertEqual(result.status, 0, 'exit status');
|
|
943
1006
|
assertEqual(result.stdout,
|
|
944
|
-
"?-
|
|
945
|
-
"?-
|
|
946
|
-
"?-
|
|
1007
|
+
"?- '\\a' true.\n" +
|
|
1008
|
+
"?- '\\a' true.\n" +
|
|
1009
|
+
"?- '\\a' true.\n" +
|
|
947
1010
|
'?- ',
|
|
948
1011
|
'stdout');
|
|
949
1012
|
assertEqual(result.stderr, '', 'stderr');
|
|
@@ -956,7 +1019,7 @@ c4 ?- call((!;1)).
|
|
|
956
1019
|
input: "writeq('\\0\\').\nhalt.\n",
|
|
957
1020
|
});
|
|
958
1021
|
assertEqual(result.status, 0, 'exit status');
|
|
959
|
-
assertEqual(result.stdout, "?-
|
|
1022
|
+
assertEqual(result.stdout, "?- '\\0\\' true.\n?- ", 'stdout');
|
|
960
1023
|
assertEqual(result.stderr, '', 'stderr');
|
|
961
1024
|
},
|
|
962
1025
|
},
|
|
@@ -1091,7 +1154,7 @@ c4 ?- call((!;1)).
|
|
|
1091
1154
|
input: "writeq('\\033\\').\nhalt.\n",
|
|
1092
1155
|
});
|
|
1093
1156
|
assertEqual(result.status, 0, 'exit status');
|
|
1094
|
-
assertEqual(result.stdout, "?-
|
|
1157
|
+
assertEqual(result.stdout, "?- '\\33\\' true.\n?- ", 'stdout');
|
|
1095
1158
|
assertEqual(result.stderr, '', 'stderr');
|
|
1096
1159
|
},
|
|
1097
1160
|
},
|
|
@@ -1110,9 +1173,9 @@ c4 ?- call((!;1)).
|
|
|
1110
1173
|
});
|
|
1111
1174
|
assertEqual(result.status, 0, 'exit status');
|
|
1112
1175
|
assertEqual(result.stdout,
|
|
1113
|
-
'?-
|
|
1114
|
-
"?-
|
|
1115
|
-
'?-
|
|
1176
|
+
'?- |: X = foo.\n' +
|
|
1177
|
+
"?- |: Y = '\\a'.\n" +
|
|
1178
|
+
'?- |: Z = bar.\n' +
|
|
1116
1179
|
'?- ',
|
|
1117
1180
|
'stdout');
|
|
1118
1181
|
assertEqual(result.stderr, '', 'stderr');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5777,7 +5777,13 @@ therefore follow `-` directly because `%` cannot continue a graphic token; an
|
|
|
5777
5777
|
adjacent bracketed comment in `-/**/1` remains a syntax error under the eager
|
|
5778
5778
|
token-consumer rule. Decimal fractions and decimal exponents are supported;
|
|
5779
5779
|
the apostrophe character code is written with a doubled apostrophe as `0'''`
|
|
5780
|
-
and has value 39
|
|
5780
|
+
and has value 39, while a literal space character code is `0' ` and has value
|
|
5781
|
+
32. Character-code constants consume one Unicode scalar, including a
|
|
5782
|
+
non-BMP character. Trailing layout, comments, and other material are rejected;
|
|
5783
|
+
bound integers are converted to their canonical decimal spelling, and
|
|
5784
|
+
non-finite values are rejected. Equivalent spellings of the same numeric type
|
|
5785
|
+
compare by value, preserving the standard conversion round trip, while integer
|
|
5786
|
+
and floating-point terms remain distinct. The
|
|
5781
5787
|
regression gate vendors all 74 numbered cases from Ulrich Neumerkel's contemporary
|
|
5782
5788
|
`number_chars/2` comparison, including the Cor.2 error-precedence cases;
|
|
5783
5789
|
`number_codes/2` shares the same numeric parser and has mirrored coverage for
|
|
@@ -6285,10 +6291,13 @@ When another answer exists in an interactive terminal, press `;`, Space, or
|
|
|
6285
6291
|
enumeration, `a` enumerates all remaining answers, and `f` advances to the
|
|
6286
6292
|
next five-answer boundary (5, 10, 15, ... displayed leaf answers), regardless
|
|
6287
6293
|
of how many answers were stepped through individually beforehand. `h` displays
|
|
6288
|
-
the answer-control help.
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6294
|
+
the answer-control help. Once the top-level reader has accepted a complete
|
|
6295
|
+
query, the following line begins with two spaces to mark active execution; a
|
|
6296
|
+
third space appears when its result is ready for formatting. The answer prompt
|
|
6297
|
+
is `;` with no trailing space while it waits for input; after an advance
|
|
6298
|
+
command, one space marks active search and a second marks an answer ready for
|
|
6299
|
+
formatting. While a query is actively computing, EyeProlog releases readline's
|
|
6300
|
+
terminal signal handling: `Ctrl-C`
|
|
6292
6301
|
therefore terminates the current EyeProlog process immediately, and on POSIX
|
|
6293
6302
|
terminals `Ctrl-Z` suspends it in the usual shell-managed way. This remains a
|
|
6294
6303
|
host top-level convention rather than an ISO/IEC 13211-1 language feature. A
|