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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.21",
6
+ "version": "1.2.23",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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 { createParserOperatorState, parseClauses, parseGoalText } from './parser.js';
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.name !== right.name || left.arity !== right.arity) return false;
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.name !== right.name || left.arity !== right.arity) return false;
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
- // number_chars/2 accepts leading layout, but its character list must end
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 parsed = parseGoalText(`number_chars_value(${numericText})`);
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
- const leftInteger = isDecimalInteger(left.name);
1605
- const rightInteger = isDecimalInteger(right.name);
1606
- if (leftInteger || rightInteger) {
1607
- return leftInteger && rightInteger && BigInt(left.name) === BigInt(right.name);
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 leftValue = Number(left.name);
1610
- const rightValue = Number(right.name);
1611
- return Number.isFinite(leftValue) && Number.isFinite(rightValue) && leftValue === rightValue;
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.name).map((ch) =>
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(' true.\n');
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(' true.\n');
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(` ${formatError(engine, state, error)}\n`);
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(' false.\n');
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 ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
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 && left.name === right.name;
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 || left?.name !== right?.name) return false;
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
- // This file is intentionally dependency-free because nearly every other module imports it.
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.name !== right.name || left.arity !== right.arity) return false;
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
  }
@@ -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 prompt distinguishes waiting from computation',
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
- "?- '\\a' true.\n" +
945
- "?- '\\a' true.\n" +
946
- "?- '\\a' true.\n" +
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, "?- '\\0\\' true.\n?- ", '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, "?- '\\33\\' true.\n?- ", '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
- '?- |: X = foo.\n' +
1114
- "?- |: Y = '\\a'.\n" +
1115
- '?- |: Z = bar.\n' +
1176
+ '?- |: X = foo.\n' +
1177
+ "?- |: Y = '\\a'.\n" +
1178
+ '?- |: Z = bar.\n' +
1116
1179
  '?- ',
1117
1180
  'stdout');
1118
1181
  assertEqual(result.stderr, '', 'stderr');
@@ -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. Trailing material and non-finite values are rejected. The
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. The answer prompt is `;` with no trailing space while
6289
- it waits for input; after an advance command, one space marks active search and
6290
- a second marks an answer ready for formatting. While a query is actively
6291
- computing, EyeProlog releases readline's terminal signal handling: `Ctrl-C`
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