eyeprolog 1.2.22 → 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.22",
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/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: () => {
@@ -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