eyeprolog 1.5.100 → 1.5.101

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.5.100",
6
+ "version": "1.5.101",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/parser.js CHANGED
@@ -1428,6 +1428,28 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
1428
1428
  return value;
1429
1429
  };
1430
1430
 
1431
+ // SIMPLE_NUMBER-matched text is already lexically valid Prolog number
1432
+ // syntax, and this fast path deliberately leaves a numeric literal's
1433
+ // spelling otherwise untouched (test/conformance/cases/arithmetic/
1434
+ // 024_numeric_literal_readback.pl: decimal and exponent literals retain
1435
+ // their lexical form here, so e.g. 6.02e23 must not become 6.02e+23).
1436
+ // Two things still cannot be left as raw matched text, though: an integer
1437
+ // is never left non-canonical anywhere else in the language (a leading
1438
+ // zero, or the "-0" from issue #114, must collapse the same way the
1439
+ // scanner's own adjacent-minus BigInt construction already does), and a
1440
+ // float whose value is exactly zero must never print with a stray minus --
1441
+ // this implementation deliberately never produces -0.0 (term.js
1442
+ // numberTextFromDouble) -- without reformatting any non-zero float.
1443
+ const canonicalFastNumberText = (text) => {
1444
+ if (text.includes('.') || text.includes('e') || text.includes('E')) {
1445
+ return (text.charCodeAt(0) === 45 && Number(text) === 0) ? text.slice(1) : text;
1446
+ }
1447
+ const first = text.charCodeAt(0);
1448
+ if (first !== 45 && (text.length === 1 || first !== 48)) return text;
1449
+ return BigInt(text).toString();
1450
+ };
1451
+ const fastNumberTerm = (text) => numberTerm(canonicalFastNumberText(text));
1452
+
1431
1453
  const isFastScalarToken = (text) => SIMPLE_VARIABLE.test(text) || SIMPLE_ATOM.test(text) || GRAPHIC_ATOM.test(text) || SIMPLE_NUMBER.test(text);
1432
1454
  const scalarOrVariableFast = (text) => {
1433
1455
  if (!text || !isFastScalarToken(text)) throw new Error('bad simple term');
@@ -1440,7 +1462,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
1440
1462
  variableCache.set(text, value);
1441
1463
  return value;
1442
1464
  }
1443
- if ((first === 45 || isDigitCode(first)) && SIMPLE_NUMBER.test(text)) return cached(numberCache, text, numberTerm);
1465
+ if ((first === 45 || isDigitCode(first)) && SIMPLE_NUMBER.test(text)) return cached(numberCache, text, fastNumberTerm);
1444
1466
  return atom(text);
1445
1467
  };
1446
1468
 
@@ -1506,7 +1528,10 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
1506
1528
  return term;
1507
1529
  }
1508
1530
  if (kind === 'atom') return atom(value);
1509
- if (simpleNumberInRange(text, start, end)) return cached(numberCache, value, numberTerm);
1531
+ // See canonicalFastNumberText above: "-0" and "007" must still come out
1532
+ // the same as everywhere else in the language (issue #114), without
1533
+ // reformatting a non-zero float's retained lexical spelling.
1534
+ if (simpleNumberInRange(text, start, end)) return cached(numberCache, value, fastNumberTerm);
1510
1535
  return null;
1511
1536
  };
1512
1537
 
@@ -1522,6 +1547,12 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
1522
1547
  if (type == null) return false;
1523
1548
  let name = text.slice(start, end);
1524
1549
  if (type === 'var' && name === '_') name = `__anon${anonymous++}`;
1550
+ // This range becomes a CompactBinaryClause argument, materialized lazily
1551
+ // as numberTerm(name) (program-indexing.js) and used as-is for indexing.
1552
+ // Canonicalize the stored text up front for the same reason as the two
1553
+ // callers above -- otherwise a fact such as bar(-0, 1) or bar(007, 1)
1554
+ // keeps its raw, non-canonical spelling forever (issue #114).
1555
+ if (type === 'number') name = canonicalFastNumberText(name);
1525
1556
  if (slot === 0) {
1526
1557
  out.arg0Type = type;
1527
1558
  out.arg0Name = name;
@@ -5892,5 +5892,60 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
5892
5892
  }
5893
5893
  },
5894
5894
  },
5895
+ {
5896
+ // https://github.com/eyereasoner/eyeprolog/issues/114#issuecomment-5663598188
5897
+ name: 'the fast compact-clause loader canonicalizes numeric literals the same way the general parser does (issue #114 follow-up)',
5898
+ run: () => {
5899
+ // parseClausesFastNoSource (src/parser.js) has its own, separate
5900
+ // number-token handling for facts and rules that match its compact
5901
+ // two-argument shape -- exactly the kind of second code path that
5902
+ // reintroduces the #114 bug class. It used to build every numeric
5903
+ // argument straight from the raw regex-matched text, so a fact like
5904
+ // bar(-0, 1) or bar(007, 1) kept that raw spelling forever instead of
5905
+ // canonicalizing like every other numeric literal in the language.
5906
+ for (const [program, goal, expected] of [
5907
+ ["bar(-0, 1).\n", 'bar(A, B), write_canonical(A), nl', '0'],
5908
+ ["bar(-0.0, 1).\n", 'bar(A, B), write_canonical(A), nl', '0.0'],
5909
+ ["bar(007, 1).\n", 'bar(A, B), write_canonical(A), nl', '7'],
5910
+ ['point(1, -0).\n', 'point(A, B), write_canonical(B), nl', '0'],
5911
+ ]) {
5912
+ const stdout = run(program, { goal }).stdout;
5913
+ assertEqual(stdout.split('\n')[0], expected, `${program.trim()}: ${goal}`);
5914
+ }
5915
+ // The fast loader also deliberately retains a numeric literal's exact
5916
+ // lexical spelling otherwise (test/conformance/cases/arithmetic/
5917
+ // 024_numeric_literal_readback.pl): the zero-canonicalization above
5918
+ // must not turn into a general reformatting pass that rewrites a
5919
+ // non-zero float's notation.
5920
+ assertEqual(
5921
+ run('raw(a, 6.02e23).\n', { goal: 'raw(_, B), write_canonical(B), nl' }).stdout.split('\n')[0],
5922
+ '6.02e23',
5923
+ 'non-zero exponent notation is preserved, not reformatted to 6.02e+23',
5924
+ );
5925
+ assertEqual(
5926
+ run('raw(a, -1.0e-3).\n', { goal: 'raw(_, B), write_canonical(B), nl' }).stdout.split('\n')[0],
5927
+ '-1.0e-3',
5928
+ 'non-zero negative exponent notation is preserved, not reformatted to -0.001',
5929
+ );
5930
+ },
5931
+ },
5932
+ {
5933
+ // https://github.com/eyereasoner/eyeprolog/issues/114#issuecomment-5663598188
5934
+ name: 'a genuine IEEE-754 negative zero produced by arithmetic (not just parsed from text) still prints as 0.0',
5935
+ run: () => {
5936
+ // Every fix above canonicalizes -0.0 where it is spelled out in
5937
+ // source text. EyeProlog's floats are ordinary JS doubles, though,
5938
+ // and JS arithmetic itself produces a real, distinct -0.0 bit
5939
+ // pattern for operations such as a negative number times zero --
5940
+ // this is genuine IEEE-754 negative zero, not a text-parsing
5941
+ // artifact, and 13211-1 has nothing to say about it either way. It
5942
+ // must still come out normalized to plain 0.0 on the way to a term,
5943
+ // the same as every syntactic spelling of negative zero does.
5944
+ for (const goal of ['X is -1.0 * 0.0', 'X is 0.0 * -1.0', 'X is -(1.0 - 1.0)']) {
5945
+ const stdout = run('', { goal: `${goal}, write_canonical(X), nl` }).stdout;
5946
+ assertEqual(stdout.split('\n')[0], '0.0', `${goal}: internal -0.0 normalizes to 0.0`);
5947
+ }
5948
+ },
5949
+ },
5895
5950
  ];
5896
5951
  }