eyeprolog 1.3.32 → 1.3.34

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.
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
11
11
  | builtins | 11 | 0 | 0 | 0 | 11 |
12
12
  | context | 11 | 0 | 0 | 0 | 11 |
13
13
  | control | 15 | 0 | 0 | 0 | 15 |
14
- | iso | 168 | 218 | 0 | 0 | 386 |
14
+ | iso | 169 | 217 | 0 | 0 | 386 |
15
15
  | lists | 52 | 3 | 0 | 0 | 55 |
16
16
  | modules | 2 | 0 | 0 | 0 | 2 |
17
17
  | negation | 8 | 0 | 19 | 0 | 27 |
@@ -24,4 +24,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
24
24
  | terms | 26 | 3 | 0 | 0 | 29 |
25
25
  | unification | 18 | 0 | 0 | 0 | 18 |
26
26
  | variables | 16 | 7 | 0 | 0 | 23 |
27
- | **Total** | **489** | **271** | **19** | **21** | **800** |
27
+ | **Total** | **490** | **270** | **19** | **21** | **800** |
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.32",
6
+ "version": "1.3.34",
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
@@ -1939,7 +1939,13 @@ function validateControlCallable(term, culprit, env) {
1939
1939
  if (current.type !== COMPOUND || ![',', ';', '->'].includes(current.name) || current.arity !== 2) continue;
1940
1940
  for (let index = current.arity - 1; index >= 0; index--) {
1941
1941
  const argument = deref(current.args[index], env);
1942
- if (argument.type === VAR) throw new PrologError('instantiation_error');
1942
+ // A variable nested in a control construct is not an error at call/1
1943
+ // entry: an earlier goal may instantiate it before execution reaches
1944
+ // that position. If it is still unbound when selected, the solver then
1945
+ // raises instantiation_error at that point, after any preceding effects.
1946
+ // Non-variable non-callables are different: ISO call/1 validates those
1947
+ // eagerly and reports the whole control term as the culprit.
1948
+ if (argument.type === VAR) continue;
1943
1949
  if (argument.type !== ATOM && argument.type !== COMPOUND) {
1944
1950
  throw new PrologError('type_error(callable)', culprit);
1945
1951
  }
@@ -2360,7 +2366,6 @@ function evaluateOperation(term, args) {
2360
2366
  const value = fn(a);
2361
2367
  if (Number.isNaN(value) || (name === 'log' && a === 0)) throw new PrologError('evaluation_error(undefined)');
2362
2368
  if (!Number.isFinite(value)) throw new PrologError('evaluation_error(float_overflow)');
2363
- if (value === 0 && a !== 0 && name === 'exp') throw new PrologError('evaluation_error(underflow)');
2364
2369
  return { integer: false, value };
2365
2370
  }
2366
2371
  if (arity !== 2) throw new PrologError('type_error(evaluable)', compound('/', [atom(name), numberTerm(arity)]));
@@ -2420,12 +2425,6 @@ function evaluateOperation(term, args) {
2420
2425
  else throw new PrologError('type_error(evaluable)', compound('/', [atom(name), numberTerm(arity)]));
2421
2426
  if (Number.isNaN(value)) throw new PrologError('evaluation_error(undefined)');
2422
2427
  if (!Number.isFinite(value)) throw new PrologError('evaluation_error(float_overflow)');
2423
- const underflow = value === 0 && (
2424
- (name === '*' && x !== 0 && y !== 0) ||
2425
- (name === '/' && x !== 0) ||
2426
- ((name === '**' || name === '^') && x !== 0)
2427
- );
2428
- if (underflow) throw new PrologError('evaluation_error(underflow)');
2429
2428
  return { integer: false, value };
2430
2429
  }
2431
2430
  export function arithmeticValueTerm(value) {
package/src/quads.js CHANGED
@@ -247,7 +247,10 @@ function executeQuery(program, query, input, maxSolutions, options) {
247
247
  error,
248
248
  tailOutput,
249
249
  inputPosition,
250
- loops: solver.depthLimitExceeded || solver.inferenceLimitExceeded,
250
+ // Accept direct structural evidence from EyeProlog's recursion guard as
251
+ // well as bounded-search evidence. A detected active-variant cycle is
252
+ // stronger evidence than merely reaching a timeout/inference ceiling.
253
+ loops: solver.recursionCycleDetected || solver.depthLimitExceeded || solver.inferenceLimitExceeded,
251
254
  };
252
255
  }
253
256
 
package/src/solver.js CHANGED
@@ -63,6 +63,10 @@ export class Solver {
63
63
  // limit accounting. time/1 snapshots this counter around the measured goal.
64
64
  this.inferenceObservation = options.inferenceObservation ?? { value: 0 };
65
65
  this.inferenceLimitExceeded = false;
66
+ // Set when the normal-profile recursion guard detects re-entry of an
67
+ // already active variant on the current search path. Quad `loops` checks
68
+ // use this structural evidence in addition to bounded resource probes.
69
+ this.recursionCycleDetected = false;
66
70
  this.maxMemoryBytes = options.maxMemoryBytes ?? softHeapLimit();
67
71
  this.memoryRecovery = options.memoryRecovery ?? {
68
72
  active: false,
@@ -261,6 +265,7 @@ export class Solver {
261
265
  if (!child || child === this || !child.stats) return;
262
266
  this.depthLimitExceeded ||= child.depthLimitExceeded;
263
267
  this.inferenceLimitExceeded ||= child.inferenceLimitExceeded;
268
+ this.recursionCycleDetected ||= child.recursionCycleDetected;
264
269
  for (const [key, value] of Object.entries(child.stats)) {
265
270
  if (key === 'max_depth' || key === 'max_goal_count') {
266
271
  this.stats[key] = Math.max(this.stats[key] ?? 0, value ?? 0);
@@ -845,7 +850,10 @@ export class Solver {
845
850
  }
846
851
 
847
852
  *solveUserGoalUncached(group, goal, rest, env, depth) {
848
- if (group.recursive && !group.cutRecursive && !group.linearNumeric && this.activeVariant(goal, env)) return;
853
+ if (group.recursive && !group.cutRecursive && !group.linearNumeric && this.activeVariant(goal, env)) {
854
+ this.recursionCycleDetected = true;
855
+ return;
856
+ }
849
857
  // Program indexes provide candidate clauses, but every candidate is still
850
858
  // freshened and unified below. The index is a performance hint, not a
851
859
  // semantic shortcut.
@@ -1047,7 +1055,10 @@ function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solv
1047
1055
  }
1048
1056
 
1049
1057
  function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
1050
- if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) return;
1058
+ if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) {
1059
+ solver.recursionCycleDetected = true;
1060
+ return;
1061
+ }
1051
1062
  if (group.fastPi && pushFastPiFrames(stack, goal, rest, env, depth, active)) return;
1052
1063
  if (tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, depth, active)) return;
1053
1064
  if (tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active)) return;
@@ -1981,7 +1992,10 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
1981
1992
  }
1982
1993
 
1983
1994
  const goal = runGoals[state.index];
1984
- if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) continue;
1995
+ if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
1996
+ solver.recursionCycleDetected = true;
1997
+ continue;
1998
+ }
1985
1999
  solver.stats.solve_one_goal_calls++;
1986
2000
  const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
1987
2001
  const nextStates = [];
@@ -2035,7 +2049,10 @@ function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
2035
2049
  }
2036
2050
 
2037
2051
  const goal = goals[state.index];
2038
- if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) continue;
2052
+ if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
2053
+ solver.recursionCycleDetected = true;
2054
+ continue;
2055
+ }
2039
2056
  solver.stats.solve_one_goal_calls++;
2040
2057
  const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
2041
2058
  const nextStates = [];
@@ -2205,7 +2222,11 @@ function tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env,
2205
2222
  if (solver.solutionsSeen >= solver.solutionLimit) return true;
2206
2223
  solver.stats.max_depth = Math.max(solver.stats.max_depth, currentDepth);
2207
2224
  const seenSet = seen[secondType];
2208
- if (!seenSet || seenSet.has(secondName)) return true;
2225
+ if (!seenSet) return true;
2226
+ if (seenSet.has(secondName)) {
2227
+ solver.recursionCycleDetected = true;
2228
+ return true;
2229
+ }
2209
2230
  if (cache[secondType].has(secondName)) {
2210
2231
  rememberCompactChainSuccess(cache, seen);
2211
2232
  stack.push({ kind: 'goals', goals: rest, env, depth: depth + 1, active });
@@ -2326,8 +2347,14 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
2326
2347
  if (solver.solutionsSeen >= solver.solutionLimit) return true;
2327
2348
  solver.stats.max_depth = Math.max(solver.stats.max_depth, currentDepth);
2328
2349
  const key = groundChainKey(currentGoal);
2329
- if (seen.has(key)) return true;
2330
- if (activeVariantIn(currentGoal, currentEnv, active)) return true;
2350
+ if (seen.has(key)) {
2351
+ solver.recursionCycleDetected = true;
2352
+ return true;
2353
+ }
2354
+ if (activeVariantIn(currentGoal, currentEnv, active)) {
2355
+ solver.recursionCycleDetected = true;
2356
+ return true;
2357
+ }
2331
2358
  if (solver.groundChainSuccess.has(key)) {
2332
2359
  rememberGroundChainSuccess(solver, seen);
2333
2360
  stack.push({ kind: 'goals', goals: rest, env: baseEnv, depth: depth + 1, active });
@@ -73,7 +73,7 @@ Status values are:
73
73
  | 8.17.3 | Other effects of `halt/0` | Terminates EyeProlog execution and returns host/process status `0`; it produces no Prolog solution. | **defined** — `HaltSignal`, `haltBuiltin()`, CLI/runner handling. |
74
74
  | 8.17.4 | Meaning/effects of `halt(Status)` | Integer `Status` is converted to the host process/runner halt code; it produces no Prolog solution. | **defined** — `haltBuiltin()`, `src/execute.js`, `src/cli.js`. |
75
75
  | 9.1.4.1 | Floating-point rounding function `rndF` | Floating values and operations use ECMAScript `Number` (IEEE-754 binary64) and the host's specified binary64 arithmetic/conversions. | **defined** — `src/iso.js`, `src/number-value.js`. |
76
- | 9.1.4.2 | Floating-point result function, including tiny non-zero arithmetic results | For arithmetic operation results that underflow to zero from non-zero operands, EyeProlog chooses the exceptional value `underflow`, exposed as `evaluation_error(underflow)`. Float token/`number_chars/2` input is a separate conversion path and finite input underflow rounds to `0.0`. | **defined** — `evaluateOperation()` and parser/number conversion; executable issue-56 regression below. |
76
+ | 9.1.4.2 | Floating-point result function, including tiny non-zero arithmetic results | EyeProlog chooses `round(x)` rather than the exceptional value `underflow`. ECMAScript binary64 arithmetic therefore preserves a representable subnormal result and rounds a still-smaller result to `0.0`, consistently with float-token and `number_chars/2` input. | **defined** — `evaluateOperation()` and parser/number conversion; executable issue-56 regression below. |
77
77
  | 9.1.4.3 | Approximate-addition function | ECMAScript binary64 addition is used; subtraction is implemented through the corresponding host operation and all finite results remain binary64 values. | **defined** — `evaluateOperation()` in `src/iso.js`. |
78
78
  | 9.4 | Representation of negative integers for bitwise operations | BigInt's unbounded signed binary semantics are used, equivalent to an infinite two's-complement sign extension for bitwise operations. | **defined** — `src/iso.js` BigInt bitwise operators. |
79
79
  | 9.4.1 | Right shift of negative integers and unusual shift counts | `>>` is arithmetic/sign-propagating. A negative count reverses direction according to JavaScript BigInt shift semantics; there is no finite integer bit-size ceiling in the Prolog model. | **defined** — `a >> b` in `src/iso.js`. |
@@ -83,22 +83,18 @@ Status values are:
83
83
  | 9.4.5 | Bitwise complement | BigInt complement, i.e. `~N = -N-1`. | **defined** — `~a`. |
84
84
  | Cor.2 9.4.6 | `xor/2` with negative operands | BigInt infinite-two's-complement semantics. | **defined** — `a ^ b`. |
85
85
 
86
- ### Why issue #56's two float results differ
87
-
88
- The arithmetic expression and the float token go through different specified
89
- layers. Clause 9.1.4 defines arithmetic operations in terms of the
90
- implementation-defined floating rounding/result functions, and EyeProlog's
91
- chosen `resultF` policy reports underflow for a non-zero arithmetic result that
92
- falls below the normal range and rounds to zero. By contrast, reading the token
93
- `0.1e-999` is an input conversion; the 1995 text does not precisely define the
94
- rounding of an inexact float token, a gap also called out in the later
95
- [WG17/STC item #40](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#40).
96
- EyeProlog's documented input policy is therefore to accept finite token
97
- underflow as `0.0` while arithmetic underflow remains an evaluation error.
98
-
99
- The regression suite contains an issue-56 case that checks both observations,
100
- and the existing `stc/float_underflow_input` conformance case covers the related
101
- input-conversion policy.
86
+ ### Issue #56: one underflow policy
87
+
88
+ Clause 9.1.4.2 permits the processor to choose `round(x)` or the exceptional
89
+ value `underflow` for a tiny non-zero arithmetic result. EyeProlog chooses
90
+ `round(x)`. Because its floating-point profile is ECMAScript IEEE-754 binary64,
91
+ representable subnormal values remain non-zero and smaller results round to
92
+ `0.0`. Float-token and `number_chars/2` input use the same finite-double
93
+ rounding policy, so the same mathematical magnitude is no longer accepted as
94
+ `0.0` on input while rejected as an arithmetic result.
95
+
96
+ The regression suite checks both arithmetic and token input, and the existing
97
+ `stc/float_underflow_input` conformance case covers the related input conversion.
102
98
 
103
99
  ## Implementation-specific features required to be documented by 5.4
104
100
 
@@ -33,8 +33,8 @@ clause-by-clause in [ISO-IMPLEMENTATION-DEFINED.md](ISO-IMPLEMENTATION-DEFINED.m
33
33
  with *The Art of EyeProlog* remaining the implementation reference. In
34
34
  particular, the index records the unbounded integer model, `double_quotes=chars`,
35
35
  `//` rounding toward zero, the ECMAScript binary64 float policy (including the
36
- 9.1.4.2 arithmetic-underflow choice), stream/character decisions, and the
37
- normal-profile extension boundary.
36
+ 9.1.4.2 choice to round tiny arithmetic results rather than raise underflow),
37
+ stream/character decisions, and the normal-profile extension boundary.
38
38
 
39
39
  This is an executable conformance matrix, not a certification issued by an
40
40
  independent standards body. Release gating runs the ISO cases and the dedicated
@@ -37,7 +37,8 @@ record the behavior discussed in issue #54:
37
37
  - a positive finite numeric token beyond the representable range raises
38
38
  `representation_error(max_float)`;
39
39
  - the corresponding negative overflow raises `representation_error(min_float)`;
40
- - input underflow may round to `0.0`;
40
+ - float input and arithmetic underflow use the same binary64 rounding policy;
41
+ values smaller than the representable range round to `0.0`;
41
42
  - overflow produced by arithmetic evaluation remains
42
43
  `evaluation_error(float_overflow)`.
43
44
 
@@ -0,0 +1,7 @@
1
+ %% goal: arithmetic_underflow_rounds
2
+
3
+ arithmetic_underflow_rounds :-
4
+ A is 0.1*10** -999,
5
+ B is exp(-1000.0),
6
+ A =:= 0.0,
7
+ B =:= 0.0.
@@ -0,0 +1 @@
1
+ arithmetic_underflow_rounds.
@@ -740,10 +740,10 @@ c4 ?- call((!;1)).
740
740
  },
741
741
  },
742
742
  {
743
- name: 'arithmetic underflow choice differs from float-token input underflow (issue #56)',
743
+ name: 'arithmetic and float-token underflow both round to zero (issue #56)',
744
744
  run: () => {
745
- const result = run('', { goal: 'catch((N is 0.1*10** -999), error(evaluation_error(underflow), _), N = underflow)' });
746
- assertEqual(result.stdout, 'catch(underflow is 0.1 * 10 ** -999, error(evaluation_error(underflow), eyeprolog), underflow = underflow).\n', 'operation underflow');
745
+ assertEqual(run('', { goal: 'N is 0.1*10** -999' }).stdout, '0.0 is 0.1 * 10 ** -999.\n', 'operation underflow');
746
+ assertEqual(run('', { goal: 'N is exp(-1000.0)' }).stdout, '0.0 is exp(-1000.0).\n', 'function underflow');
747
747
  assertEqual(run('', { goal: 'N = 0.1e-999' }).stdout, '0.0 = 0.0.\n', 'float-token input underflow');
748
748
  },
749
749
  },
@@ -1317,6 +1317,22 @@ c4 ?- call((!;1)).
1317
1317
  assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
1318
1318
  },
1319
1319
  },
1320
+ {
1321
+ name: 'runQuads preserves output before a delayed call/1 instantiation error (issue #57)',
1322
+ run: () => {
1323
+ const result = publicApi.runQuads(`16, "7.8.3.4#9"\n?- call((write(3), X)).\n outputs("3"), instantiation_error.\n`);
1324
+ assertEqual(result.passed, 1, 'quad passed');
1325
+ assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
1326
+ },
1327
+ },
1328
+ {
1329
+ name: 'runQuads recognizes recursion-guard cycle evidence as loops (issue #58)',
1330
+ run: () => {
1331
+ const result = publicApi.runQuads(`inf :- inf, inf.\n\n23\n?- inf.\n loops.\n`);
1332
+ assertEqual(result.passed, 1, 'quad passed');
1333
+ assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
1334
+ },
1335
+ },
1320
1336
  {
1321
1337
  name: '--quads runs embedded tests and reports failures through exit status',
1322
1338
  run: () => {
@@ -3431,7 +3447,7 @@ function documentationSyncCases() {
3431
3447
  '8.17.3', '8.17.4', '9.1.3.1', '9.1.4.1', '9.1.4.2', '9.1.4.3', '9.4',
3432
3448
  '9.4.1', '9.4.2', '9.4.3', '9.4.4', '9.4.5', 'Cor.2 9.4.6',
3433
3449
  ]) assertIncludes(text, `| ${clause} |`, `ISO 5.4 clause ${clause}`);
3434
- assertIncludes(text, 'Why issue #56', 'issue #56 explanation');
3450
+ assertIncludes(text, 'Issue #56: one underflow policy', 'issue #56 explanation');
3435
3451
  assertIncludes(text, 'Implementation-specific features required to be documented by 5.4', '5.5 extension inventory');
3436
3452
  },
3437
3453
  },
@@ -6831,9 +6831,12 @@ variable in the renamed exception term. `...` and `ad_infinitum` accept further
6831
6831
  indented descriptions after one query are independent checks: each re-runs the
6832
6832
  query, each is counted in the `quads:` summary, and a failing description does
6833
6833
  not suppress later descriptions for that query. `inputs/1` supplies and checks
6834
- consumed characters; `outputs/1` checks emitted characters. `sto` marks
6835
- an answer description that this finite-tree implementation skips. `loops` is
6836
- checked with a deterministic solver-depth budget. The advanced stream
6834
+ consumed characters; `outputs/1` checks characters emitted while reaching
6835
+ the described answer or error, including output produced before a later
6836
+ exception. `sto` marks an answer description that this finite-tree
6837
+ implementation skips. `loops` accepts direct active-variant cycle evidence from
6838
+ EyeProlog's normal recursion guard, with bounded depth/inference exhaustion as a
6839
+ fallback for loops that have no such structural witness. The advanced stream
6837
6840
  annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
6838
6841
  annotation, are not executed by the current runner.
6839
6842
 
@@ -1,3 +0,0 @@
1
- %% goal: trigger
2
-
3
- trigger :- _ is exp(-1000.0).
@@ -1 +0,0 @@
1
- error(evaluation_error(underflow))