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.
- package/conformance-report.md +2 -2
- package/package.json +1 -1
- package/src/iso.js +7 -8
- package/src/quads.js +4 -1
- package/src/solver.js +34 -7
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +13 -17
- package/test/conformance/ISO-MATRIX.md +2 -2
- package/test/conformance/STC-DRAFT-STATUS.md +2 -1
- package/test/conformance/cases/iso/arithmetic_underflow_rounds.pl +7 -0
- package/test/conformance/expected/iso/arithmetic_underflow_rounds.pl +1 -0
- package/test/run-regression.mjs +20 -4
- package/the-art-of-eyeprolog.md +6 -3
- package/test/conformance/errors/iso/arithmetic_underflow.pl +0 -3
- package/test/conformance/expected-errors/iso/arithmetic_underflow.txt +0 -1
package/conformance-report.md
CHANGED
|
@@ -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 |
|
|
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** | **
|
|
27
|
+
| **Total** | **490** | **270** | **19** | **21** | **800** |
|
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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
|
|
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))
|
|
2330
|
-
|
|
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 |
|
|
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
|
-
###
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
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 @@
|
|
|
1
|
+
arithmetic_underflow_rounds.
|
package/test/run-regression.mjs
CHANGED
|
@@ -740,10 +740,10 @@ c4 ?- call((!;1)).
|
|
|
740
740
|
},
|
|
741
741
|
},
|
|
742
742
|
{
|
|
743
|
-
name: 'arithmetic
|
|
743
|
+
name: 'arithmetic and float-token underflow both round to zero (issue #56)',
|
|
744
744
|
run: () => {
|
|
745
|
-
|
|
746
|
-
assertEqual(
|
|
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, '
|
|
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
|
},
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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
|
|
6835
|
-
|
|
6836
|
-
|
|
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 +0,0 @@
|
|
|
1
|
-
error(evaluation_error(underflow))
|