eyeprolog 1.5.97 → 1.5.98

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.97",
6
+ "version": "1.5.98",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/quads.js CHANGED
@@ -322,6 +322,7 @@ function describeLeaf(term) {
322
322
  const leaf = {
323
323
  bindings: [],
324
324
  approximations: [],
325
+ literalApproximations: [],
325
326
  unexpected: false,
326
327
  more: false,
327
328
  sto: false,
@@ -360,8 +361,21 @@ function describeLeaf(term) {
360
361
  continue;
361
362
  }
362
363
  if (item.type === COMPOUND && item.name === '~~' && item.arity === 2) {
363
- if (item.args[0].type !== VAR || approximateDecimalInterval(item.args[1]) == null) leaf.malformed ??= item;
364
- else leaf.approximations.push(item);
364
+ // The left-hand side names either one of the query's own variables
365
+ // (the usual case: compare what the query actually bound it to) or,
366
+ // just as legitimately, a literal number written directly in the
367
+ // answer description -- a free-standing "this value approximately
368
+ // denotes this decimal interval" claim with no query variable
369
+ // involved at all (issue #113: `0.04 ~~ '0.0'` was rejected as
370
+ // malformed purely for lacking a variable on the left, with no
371
+ // deeper reason for that restriction). Both are validated the same
372
+ // way one level up, in matchLeaf/substitutionMatches: syntactic
373
+ // acceptance here does not yet mean the value actually falls in the
374
+ // stated interval.
375
+ const lhsIsVarOrNumber = item.args[0].type === VAR || item.args[0].type === NUMBER;
376
+ if (!lhsIsVarOrNumber || approximateDecimalInterval(item.args[1]) == null) leaf.malformed ??= item;
377
+ else if (item.args[0].type === VAR) leaf.approximations.push(item);
378
+ else leaf.literalApproximations.push(item);
365
379
  continue;
366
380
  }
367
381
  if (item.type === COMPOUND && item.name === 'inputs' && item.arity === 1) {
@@ -385,6 +399,7 @@ function describeLeaf(term) {
385
399
  else leaf.malformed ??= item;
386
400
  }
387
401
  leaf.hasExpectation = leaf.bindings.length > 0 || leaf.approximations.length > 0 ||
402
+ leaf.literalApproximations.length > 0 ||
388
403
  leaf.truth || leaf.false || leaf.maybe || leaf.loops || leaf.waits ||
389
404
  leaf.error != null || leaf.output != null;
390
405
  if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
@@ -528,6 +543,10 @@ function matchLeaf(program, query, leaf, actual, position) {
528
543
  // its absence requires an unconstrained answer. In either case the stated
529
544
  // substitutions remain exact and are checked below.
530
545
  if (leaf.maybe !== hasPendingConstraints(solution.env)) return false;
546
+ // Literal `~~` claims (issue #113) name no query variable at all, so
547
+ // they carry no per-position runtime state to look up; they either hold
548
+ // or they don't, the same at every position.
549
+ if (leaf.literalApproximations.some((item) => !approximatelyMatches(item.args[0], item.args[1]))) return false;
531
550
  return substitutionMatches(query, leaf.bindings, leaf.approximations, solution.env);
532
551
  }
533
552
 
@@ -1195,6 +1195,26 @@ why(
1195
1195
  assertEqual(malformedAtom.failed, 1, 'non-decimal atom is rejected');
1196
1196
  assertIncludes(malformedAtom.stdout, 'MALFORMED', 'non-decimal atom diagnostic');
1197
1197
 
1198
+ // Issue #113: a literal number on the left of `~~` names no query
1199
+ // variable at all -- it is a free-standing "this value approximately
1200
+ // denotes this decimal interval" claim, and there is no deeper reason
1201
+ // to require a variable there. It must be accepted (and actually
1202
+ // checked against the interval, not accepted unconditionally), the
1203
+ // same as the query-variable form already was.
1204
+ const literalOk = publicApi.runQuads('?- true.\n 0.04 ~~ \'0.0\'.\n');
1205
+ assertEqual(literalOk.total, 1, 'literal approximation total');
1206
+ assertEqual(literalOk.passed, 1, 'literal approximation passed');
1207
+ assertEqual(literalOk.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'literal approximation report');
1208
+
1209
+ const literalOutOfRange = publicApi.runQuads('?- true.\n 0.04 ~~ \'5.0\'.\n');
1210
+ assertEqual(literalOutOfRange.failed, 1, 'literal outside the claimed interval fails, not malformed');
1211
+ assertIncludes(literalOutOfRange.stdout, 'quads: FAILED', 'literal out-of-range diagnostic');
1212
+ assertNotIncludes(literalOutOfRange.stdout, 'MALFORMED', 'literal out-of-range is not malformed');
1213
+
1214
+ const literalNonNumberLhs = publicApi.runQuads('?- true.\n foo ~~ \'0.0\'.\n');
1215
+ assertEqual(literalNonNumberLhs.failed, 1, 'a non-variable, non-number left side remains malformed');
1216
+ assertIncludes(literalNonNumberLhs.stdout, 'MALFORMED', 'non-number left side diagnostic');
1217
+
1198
1218
  const duplicate = publicApi.runQuads(`?- V is 14.2.
1199
1219
  V = 14.2, V ~~ '14.2000'.
1200
1220
  `);