eyeprolog 1.3.84 → 1.3.86
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 +1 -1
- package/src/cleanup.js +9 -7
- package/src/iso.js +154 -24
- package/src/scryer-compat.js +2 -2
- package/src/solver.js +78 -42
- package/test/conformance/ISO-COMPLIANCE.md +2 -2
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +1 -1
- package/test/run-cleanup.mjs +12 -0
- package/test/run-iso-strict.mjs +8 -0
- package/test/run-regression.mjs +237 -7
- package/the-art-of-eyeprolog.md +52 -20
package/package.json
CHANGED
package/src/cleanup.js
CHANGED
|
@@ -237,10 +237,11 @@ function cleanupProtectedIterator(solver, protectedGoal, cleanup, initialEnv) {
|
|
|
237
237
|
let unwindToSetupBindings = false;
|
|
238
238
|
|
|
239
239
|
const performCleanup = () => {
|
|
240
|
-
if (cleaned) return;
|
|
240
|
+
if (cleaned) return cleanupEnv;
|
|
241
241
|
// Mark first so a throwing Cleanup is still considered executed exactly once.
|
|
242
242
|
cleaned = true;
|
|
243
|
-
runCleanupOnce(solver, cleanup, cleanupEnv);
|
|
243
|
+
cleanupEnv = runCleanupOnce(solver, cleanup, cleanupEnv);
|
|
244
|
+
return cleanupEnv;
|
|
244
245
|
};
|
|
245
246
|
|
|
246
247
|
const iterator = (function* protectedWithCleanup() {
|
|
@@ -266,7 +267,7 @@ function cleanupProtectedIterator(solver, protectedGoal, cleanup, initialEnv) {
|
|
|
266
267
|
performCleanup();
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
yield result.value;
|
|
270
|
+
yield pending ? result.value : cleanupEnv;
|
|
270
271
|
if (!pending) return;
|
|
271
272
|
}
|
|
272
273
|
} catch (error) {
|
|
@@ -321,8 +322,9 @@ function runCleanupOnce(solver, cleanup, env) {
|
|
|
321
322
|
solver.absorbStatsFrom(child);
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
|
-
// Cleanup is
|
|
325
|
-
//
|
|
326
|
-
// already
|
|
327
|
-
|
|
325
|
+
// Cleanup failure is ignored. When Cleanup runs before a deterministic
|
|
326
|
+
// protected answer is yielded, however, its substitutions are part of that
|
|
327
|
+
// answer; callers that prune a pending answer have already observed it and
|
|
328
|
+
// therefore cannot receive later Cleanup substitutions.
|
|
329
|
+
return result.done ? env : result.value;
|
|
328
330
|
}
|
package/src/iso.js
CHANGED
|
@@ -137,8 +137,10 @@ export const isoBuiltins = {
|
|
|
137
137
|
registry.add('number_chars', 2, numberCharsBuiltin, { deterministic: true });
|
|
138
138
|
registry.add('number_codes', 2, numberCodesBuiltin, { deterministic: true });
|
|
139
139
|
|
|
140
|
-
registry.add('call', 1, callBuiltin);
|
|
141
|
-
for (let arity = 2; arity <= 8; arity++)
|
|
140
|
+
registry.add('call', 1, callBuiltin, { expandGoal: expandCallGoal });
|
|
141
|
+
for (let arity = 2; arity <= 8; arity++) {
|
|
142
|
+
registry.add('call', arity, callClosureBuiltin, { expandGoal: expandCallClosureGoal });
|
|
143
|
+
}
|
|
142
144
|
registry.add('catch', 3, catchBuiltin);
|
|
143
145
|
registry.add('throw', 1, throwBuiltin, { deterministic: true });
|
|
144
146
|
registry.add('\\+', 1, negationBuiltin, { deterministic: true });
|
|
@@ -166,7 +168,7 @@ export const eyePrologLibraryBuiltins = {
|
|
|
166
168
|
register(registry) {
|
|
167
169
|
registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
|
|
168
170
|
registry.add('eyeprolog__call_residue_vars', 2, callResidueVarsBuiltin, { eyePrologLibrary: true });
|
|
169
|
-
registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
|
|
171
|
+
registry.add('eyeprolog__countall', 2, countAllBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
170
172
|
registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
|
|
171
173
|
registry.add('dif', 2, difBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
172
174
|
registry.add('time', 1, timeBuiltin, { eyePrologLibrary: true });
|
|
@@ -712,27 +714,43 @@ function assertBuiltin(atStart) {
|
|
|
712
714
|
};
|
|
713
715
|
}
|
|
714
716
|
|
|
715
|
-
function
|
|
717
|
+
function retractBuiltin(context) {
|
|
718
|
+
const state = { pending: true };
|
|
719
|
+
const iterator = retractSolutions(context, state);
|
|
720
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
721
|
+
return iterator;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function* retractSolutions({ solver, goal, env }, state) {
|
|
716
725
|
const parts = clauseParts(goal.args[0], env);
|
|
717
726
|
requireClauseHead(parts.head);
|
|
718
727
|
const group = solver.program.findGroup(parts.head.name, parts.head.arity, parts.head.module ?? goal.module ?? 'user');
|
|
719
728
|
if (isProcessorStaticProcedure(solver, parts.head) || (group && !group.dynamic)) {
|
|
720
729
|
throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(parts.head));
|
|
721
730
|
}
|
|
722
|
-
if (!group)
|
|
731
|
+
if (!group) {
|
|
732
|
+
state.pending = false;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
723
735
|
// ISO logical update view: this call keeps the clauses that were visible
|
|
724
736
|
// when it began. A later retract/1 may erase one of those clauses from the
|
|
725
737
|
// live procedure, but must not invalidate this call's pending alternatives.
|
|
726
|
-
const
|
|
727
|
-
for (const clause of
|
|
738
|
+
const matches = [];
|
|
739
|
+
for (const clause of [...group.clauses]) {
|
|
728
740
|
const copied = freshCopy(compound('$clause', [clause.head, clauseBodyTerm(clause.body)]), new Env());
|
|
729
741
|
const next = env.clone();
|
|
730
742
|
if (!unify(parts.head, copied.args[0], next)) continue;
|
|
731
743
|
if (parts.rule && !unify(parts.body, copied.args[1], next)) continue;
|
|
732
744
|
if (!parts.rule && !(copied.args[1].type === ATOM && copied.args[1].name === 'true')) continue;
|
|
745
|
+
matches.push({ clause, next });
|
|
746
|
+
}
|
|
747
|
+
for (let index = 0; index < matches.length; index++) {
|
|
748
|
+
const { clause, next } = matches[index];
|
|
733
749
|
solver.program.removeDynamicClause(group, clause);
|
|
750
|
+
state.pending = index + 1 < matches.length;
|
|
734
751
|
yield next;
|
|
735
752
|
}
|
|
753
|
+
state.pending = false;
|
|
736
754
|
}
|
|
737
755
|
|
|
738
756
|
function* retractAllBuiltin({ solver, goal, env }) {
|
|
@@ -1555,6 +1573,24 @@ function scopeReadTerm(term) {
|
|
|
1555
1573
|
|
|
1556
1574
|
function parseReadTermText(text, solver) {
|
|
1557
1575
|
const converted = convertedTermText(text, solver);
|
|
1576
|
+
// A standalone number needs no general term parser or operator tables.
|
|
1577
|
+
// Reuse the same bounded numeric scanner as number_chars/2 so stream reads
|
|
1578
|
+
// and number conversion agree on every accepted numeric token. The stream
|
|
1579
|
+
// scanner has already established that the final full stop is a candidate
|
|
1580
|
+
// terminator; trim only ISO layout immediately before that terminator.
|
|
1581
|
+
const numericText = converted.slice(0, -1).replace(/[\u0009-\u000d\u0020]+$/, '');
|
|
1582
|
+
let numericTerm;
|
|
1583
|
+
try {
|
|
1584
|
+
numericTerm = parseIsoNumber(numericText);
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
// parseIsoNumber/1 exposes Prolog errors to number_chars/2. The stream
|
|
1587
|
+
// reader's candidate loop instead recognizes parser representation errors
|
|
1588
|
+
// and must not reinterpret overflow as a later syntax error.
|
|
1589
|
+
if (error instanceof PrologError) throw new NumberRepresentationError(error.formal);
|
|
1590
|
+
throw error;
|
|
1591
|
+
}
|
|
1592
|
+
if (numericTerm != null) return numericTerm;
|
|
1593
|
+
|
|
1558
1594
|
const operatorState = createParserOperatorState(solver.program.operators.values(), false);
|
|
1559
1595
|
return parseTermText(converted, {
|
|
1560
1596
|
operatorState,
|
|
@@ -1720,6 +1756,15 @@ function writeOptionBoolean(value, env, option) {
|
|
|
1720
1756
|
return value.name === 'true';
|
|
1721
1757
|
}
|
|
1722
1758
|
|
|
1759
|
+
function writeOptionSpacing(value, env, option) {
|
|
1760
|
+
value = deref(value, env);
|
|
1761
|
+
if (value.type === VAR) throw new PrologError('instantiation_error');
|
|
1762
|
+
if (value.type !== ATOM || !['minimal', 'standard'].includes(value.name)) {
|
|
1763
|
+
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1764
|
+
}
|
|
1765
|
+
return value.name;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1723
1768
|
function writeVariableNames(value, env, option) {
|
|
1724
1769
|
value = deref(value, env);
|
|
1725
1770
|
if (value.type === VAR) throw new PrologError('instantiation_error');
|
|
@@ -1760,6 +1805,8 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
|
|
|
1760
1805
|
else if (option.name === 'variable_names') result.variableNames = writeVariableNames(option.args[0], env, option);
|
|
1761
1806
|
else if (option.name === 'double_quotes' && !solver?.isoStrict) {
|
|
1762
1807
|
result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
|
|
1808
|
+
} else if (option.name === 'spacing' && !solver?.isoStrict) {
|
|
1809
|
+
result.minimalOperatorSpacing = writeOptionSpacing(option.args[0], env, option) === 'minimal';
|
|
1763
1810
|
} else {
|
|
1764
1811
|
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1765
1812
|
}
|
|
@@ -2384,6 +2431,11 @@ function callable(term, env) {
|
|
|
2384
2431
|
validateControlCallable(term, term, env);
|
|
2385
2432
|
return term;
|
|
2386
2433
|
}
|
|
2434
|
+
|
|
2435
|
+
function withPendingState(iterator, state) {
|
|
2436
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
2437
|
+
return iterator;
|
|
2438
|
+
}
|
|
2387
2439
|
function validateControlCallable(term, culprit, env) {
|
|
2388
2440
|
// Only control constructs need their nested goals validated at meta-call
|
|
2389
2441
|
// entry. Walk them iteratively and dereference each nested goal lazily so
|
|
@@ -2409,9 +2461,14 @@ function validateControlCallable(term, culprit, env) {
|
|
|
2409
2461
|
}
|
|
2410
2462
|
}
|
|
2411
2463
|
}
|
|
2412
|
-
function
|
|
2464
|
+
function expandCallGoal({ goal, env }) {
|
|
2413
2465
|
const invoked = callable(goal.args[0], env);
|
|
2414
2466
|
if (invoked.module == null) invoked.module = goal.module ?? 'user';
|
|
2467
|
+
return invoked;
|
|
2468
|
+
}
|
|
2469
|
+
function* callBuiltin(context) {
|
|
2470
|
+
const { solver, env } = context;
|
|
2471
|
+
const invoked = expandCallGoal(context);
|
|
2415
2472
|
const child = solver.cloneForInnerGoal();
|
|
2416
2473
|
try {
|
|
2417
2474
|
yield* child.solve([invoked], env, 0);
|
|
@@ -2419,7 +2476,7 @@ function* callBuiltin({ solver, goal, env }) {
|
|
|
2419
2476
|
solver.absorbStatsFrom(child);
|
|
2420
2477
|
}
|
|
2421
2478
|
}
|
|
2422
|
-
function
|
|
2479
|
+
function expandCallClosureGoal({ goal, env }) {
|
|
2423
2480
|
const closure = callable(goal.args[0], env);
|
|
2424
2481
|
const existing = closure.type === COMPOUND ? closure.args : [];
|
|
2425
2482
|
const extra = goal.args.slice(1);
|
|
@@ -2428,6 +2485,11 @@ function* callClosureBuiltin({ solver, goal, env }) {
|
|
|
2428
2485
|
}
|
|
2429
2486
|
const invoked = compound(closure.name, [...existing, ...extra]);
|
|
2430
2487
|
invoked.module = closure.module ?? goal.module ?? 'user';
|
|
2488
|
+
return invoked;
|
|
2489
|
+
}
|
|
2490
|
+
function* callClosureBuiltin(context) {
|
|
2491
|
+
const { solver, env } = context;
|
|
2492
|
+
const invoked = expandCallClosureGoal(context);
|
|
2431
2493
|
const child = solver.cloneForInnerGoal();
|
|
2432
2494
|
try {
|
|
2433
2495
|
yield* child.solve([invoked], env, 0);
|
|
@@ -2480,7 +2542,12 @@ function writeElapsedTime(solver, startedAt, inferences) {
|
|
|
2480
2542
|
);
|
|
2481
2543
|
}
|
|
2482
2544
|
|
|
2483
|
-
function
|
|
2545
|
+
function timeBuiltin(context) {
|
|
2546
|
+
const state = { pending: true };
|
|
2547
|
+
return withPendingState(timeSolutions(context, state), state);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
function* timeSolutions({ solver, goal, env }, state) {
|
|
2484
2551
|
const invoked = callable(goal.args[0], env);
|
|
2485
2552
|
const child = solver.cloneForInnerGoal();
|
|
2486
2553
|
let startedAt = monotonicMilliseconds();
|
|
@@ -2490,12 +2557,15 @@ function* timeBuiltin({ solver, goal, env }) {
|
|
|
2490
2557
|
for (const answerEnv of child.solve([invoked], env, 0)) {
|
|
2491
2558
|
writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
|
|
2492
2559
|
yieldedAny = true;
|
|
2560
|
+
state.pending = child.hasPendingAlternatives();
|
|
2493
2561
|
yield answerEnv;
|
|
2562
|
+
if (!state.pending) return;
|
|
2494
2563
|
// A resumed time/1 measures only the work required to reach the next
|
|
2495
2564
|
// answer, so nondeterministic calls get one timing line per solution.
|
|
2496
2565
|
startedAt = monotonicMilliseconds();
|
|
2497
2566
|
startedInferences = child.inferenceObservation.value;
|
|
2498
2567
|
}
|
|
2568
|
+
state.pending = false;
|
|
2499
2569
|
// A call that fails without producing an answer still reports the work.
|
|
2500
2570
|
if (!yieldedAny) writeElapsedTime(solver, startedAt, child.inferenceObservation.value - startedInferences);
|
|
2501
2571
|
} finally {
|
|
@@ -2503,10 +2573,18 @@ function* timeBuiltin({ solver, goal, env }) {
|
|
|
2503
2573
|
}
|
|
2504
2574
|
}
|
|
2505
2575
|
|
|
2506
|
-
function
|
|
2576
|
+
function callNthBuiltin(context) {
|
|
2577
|
+
const state = { pending: true };
|
|
2578
|
+
return withPendingState(callNthSolutions(context, state), state);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function* callNthSolutions({ solver, goal, env }, state) {
|
|
2507
2582
|
const requestedTerm = deref(goal.args[1], env);
|
|
2508
2583
|
// Zero is the one Nth value that fails before Goal is inspected.
|
|
2509
|
-
if (requestedTerm.type === NUMBER && isDecimalInteger(requestedTerm.name) && BigInt(requestedTerm.name) === 0n)
|
|
2584
|
+
if (requestedTerm.type === NUMBER && isDecimalInteger(requestedTerm.name) && BigInt(requestedTerm.name) === 0n) {
|
|
2585
|
+
state.pending = false;
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2510
2588
|
|
|
2511
2589
|
const invoked = callable(goal.args[0], env);
|
|
2512
2590
|
let requested = null;
|
|
@@ -2525,9 +2603,13 @@ function* callNthBuiltin({ solver, goal, env }) {
|
|
|
2525
2603
|
nth++;
|
|
2526
2604
|
if (requested != null && nth < requested) continue;
|
|
2527
2605
|
const next = answerEnv.clone();
|
|
2528
|
-
if (unify(goal.args[1], numberTerm(nth.toString()), next))
|
|
2529
|
-
|
|
2606
|
+
if (unify(goal.args[1], numberTerm(nth.toString()), next)) {
|
|
2607
|
+
state.pending = requested == null && child.hasPendingAlternatives();
|
|
2608
|
+
yield next;
|
|
2609
|
+
if (requested != null || !state.pending) return;
|
|
2610
|
+
}
|
|
2530
2611
|
}
|
|
2612
|
+
state.pending = false;
|
|
2531
2613
|
} finally {
|
|
2532
2614
|
solver.absorbStatsFrom(child);
|
|
2533
2615
|
}
|
|
@@ -2619,12 +2701,22 @@ function callResidueVarsBuiltin({ solver, goal, env }) {
|
|
|
2619
2701
|
return iterator;
|
|
2620
2702
|
}
|
|
2621
2703
|
|
|
2622
|
-
function
|
|
2704
|
+
function freezeBuiltin(context) {
|
|
2705
|
+
const state = { pending: true };
|
|
2706
|
+
return withPendingState(freezeSolutions(context, state), state);
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
function* freezeSolutions({ solver, goal, env }, state) {
|
|
2623
2710
|
const watched = deref(goal.args[0], env);
|
|
2624
2711
|
if (watched.type !== VAR) {
|
|
2625
2712
|
const child = solver.cloneForInnerGoal();
|
|
2626
2713
|
try {
|
|
2627
|
-
|
|
2714
|
+
for (const answerEnv of child.solve([callable(goal.args[1], env)], env, 0)) {
|
|
2715
|
+
state.pending = child.hasPendingAlternatives();
|
|
2716
|
+
yield answerEnv;
|
|
2717
|
+
if (!state.pending) return;
|
|
2718
|
+
}
|
|
2719
|
+
state.pending = false;
|
|
2628
2720
|
} finally {
|
|
2629
2721
|
solver.absorbStatsFrom(child);
|
|
2630
2722
|
}
|
|
@@ -2632,10 +2724,16 @@ function* freezeBuiltin({ solver, goal, env }) {
|
|
|
2632
2724
|
}
|
|
2633
2725
|
const next = env.clone();
|
|
2634
2726
|
next.delay(watched.name, goal.args[1], goal.module ?? 'user');
|
|
2727
|
+
state.pending = false;
|
|
2635
2728
|
yield next;
|
|
2636
2729
|
}
|
|
2637
2730
|
|
|
2638
|
-
function
|
|
2731
|
+
function phraseBuiltin(context) {
|
|
2732
|
+
const state = { pending: true };
|
|
2733
|
+
return withPendingState(phraseSolutions(context, state), state);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function* phraseSolutions({ solver, goal, env }, state) {
|
|
2639
2737
|
const grammarBody0 = deref(goal.args[0], env);
|
|
2640
2738
|
if (grammarBody0.type === VAR) throw new PrologError('instantiation_error');
|
|
2641
2739
|
if (grammarBody0.type !== ATOM && grammarBody0.type !== COMPOUND) {
|
|
@@ -2692,7 +2790,12 @@ function* phraseBuiltin({ solver, goal, env }) {
|
|
|
2692
2790
|
skipListTailTabling: !repeatedInvocation,
|
|
2693
2791
|
});
|
|
2694
2792
|
try {
|
|
2695
|
-
|
|
2793
|
+
for (const answerEnv of child.solve(finish == null ? [expanded] : [expanded, finish], env, 0)) {
|
|
2794
|
+
state.pending = child.hasPendingAlternatives();
|
|
2795
|
+
yield answerEnv;
|
|
2796
|
+
if (!state.pending) return;
|
|
2797
|
+
}
|
|
2798
|
+
state.pending = false;
|
|
2696
2799
|
} finally {
|
|
2697
2800
|
solver.absorbStatsFrom(child);
|
|
2698
2801
|
solver.trimInnerTableScope('phrase');
|
|
@@ -2752,7 +2855,12 @@ function prologErrorBall(error) {
|
|
|
2752
2855
|
if (hasDefaultGroundErrorShape(error) || termIsGround(term)) return term;
|
|
2753
2856
|
return freshCopy(term, new Env());
|
|
2754
2857
|
}
|
|
2755
|
-
function
|
|
2858
|
+
function catchBuiltin(context) {
|
|
2859
|
+
const state = { pending: true };
|
|
2860
|
+
return withPendingState(catchSolutions(context, state), state);
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function* catchSolutions({ solver, goal, env }, state) {
|
|
2756
2864
|
let child = null;
|
|
2757
2865
|
try {
|
|
2758
2866
|
// Corrigendum 2 removed catch/3's own callability errors so that errors
|
|
@@ -2769,12 +2877,19 @@ function* catchBuiltin({ solver, goal, env }) {
|
|
|
2769
2877
|
if (result.done) solver.stats.deterministic_builtin_failures++;
|
|
2770
2878
|
else {
|
|
2771
2879
|
solver.stats.deterministic_builtin_successes++;
|
|
2880
|
+
state.pending = false;
|
|
2772
2881
|
yield result.value;
|
|
2773
2882
|
}
|
|
2883
|
+
state.pending = false;
|
|
2774
2884
|
return;
|
|
2775
2885
|
}
|
|
2776
2886
|
child = solver.cloneForInnerGoal();
|
|
2777
|
-
|
|
2887
|
+
for (const answerEnv of child.solve([invoked], env.clone(), 0)) {
|
|
2888
|
+
state.pending = child.hasPendingAlternatives();
|
|
2889
|
+
yield answerEnv;
|
|
2890
|
+
if (!state.pending) return;
|
|
2891
|
+
}
|
|
2892
|
+
state.pending = false;
|
|
2778
2893
|
} catch (error) {
|
|
2779
2894
|
const ball = error instanceof ThrownTerm
|
|
2780
2895
|
? error.term
|
|
@@ -2785,12 +2900,18 @@ function* catchBuiltin({ solver, goal, env }) {
|
|
|
2785
2900
|
const recovered = env.clone();
|
|
2786
2901
|
if (!unify(goal.args[1], ball, recovered)) throw error;
|
|
2787
2902
|
const recovery = callable(goal.args[2], recovered);
|
|
2788
|
-
if (recovery.type === ATOM && (recovery.name === 'false' || recovery.name === 'fail'))
|
|
2903
|
+
if (recovery.type === ATOM && (recovery.name === 'false' || recovery.name === 'fail')) {
|
|
2904
|
+
state.pending = false;
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2789
2907
|
if (recovery.type === ATOM && recovery.name === 'true') {
|
|
2908
|
+
state.pending = false;
|
|
2790
2909
|
yield recovered;
|
|
2791
2910
|
return;
|
|
2792
2911
|
}
|
|
2793
|
-
yield* solver
|
|
2912
|
+
yield* solveControlBranch(solver, recovery, recovered,
|
|
2913
|
+
(pending) => { state.pending = pending; });
|
|
2914
|
+
state.pending = false;
|
|
2794
2915
|
} finally {
|
|
2795
2916
|
if (child != null) solver.absorbStatsFrom(child);
|
|
2796
2917
|
}
|
|
@@ -2888,11 +3009,19 @@ function* disjunctionSolutions({ solver, goal, env }, state) {
|
|
|
2888
3009
|
(pending) => { state.pending = pending; });
|
|
2889
3010
|
state.pending = false;
|
|
2890
3011
|
}
|
|
2891
|
-
function
|
|
3012
|
+
function ifThenBuiltin(context) {
|
|
3013
|
+
const state = { pending: true };
|
|
3014
|
+
return withPendingState(ifThenSolutions(context, state), state);
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
function* ifThenSolutions({ solver, goal, env }, state) {
|
|
2892
3018
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
|
|
2893
|
-
yield* solveControlBranch(solver, goal.args[1], conditionEnv
|
|
3019
|
+
yield* solveControlBranch(solver, goal.args[1], conditionEnv,
|
|
3020
|
+
(pending) => { state.pending = pending; });
|
|
3021
|
+
state.pending = false;
|
|
2894
3022
|
return;
|
|
2895
3023
|
}
|
|
3024
|
+
state.pending = false;
|
|
2896
3025
|
}
|
|
2897
3026
|
|
|
2898
3027
|
export { arithmeticValueTerm, evaluateArithmetic, compareArithmeticValues } from './iso-arithmetic.js';
|
|
@@ -2911,6 +3040,7 @@ export class BuiltinRegistry {
|
|
|
2911
3040
|
handler,
|
|
2912
3041
|
deterministic: options.deterministic ?? false,
|
|
2913
3042
|
deterministicWhen: options.deterministicWhen ?? null,
|
|
3043
|
+
expandGoal: options.expandGoal ?? null,
|
|
2914
3044
|
ready: options.ready ?? null,
|
|
2915
3045
|
fallbackWhenNotReady: options.fallbackWhenNotReady ?? false,
|
|
2916
3046
|
shouldUse: options.shouldUse ?? null,
|
package/src/scryer-compat.js
CHANGED
|
@@ -207,9 +207,9 @@ function* termCharsBuiltin({ solver, goal, env }) {
|
|
|
207
207
|
|
|
208
208
|
export const scryerCompatibilityBuiltins = {
|
|
209
209
|
register(registry) {
|
|
210
|
-
registry.add('eyeprolog__bb_get', 2, bbGetBuiltin, { eyePrologLibrary: true });
|
|
210
|
+
registry.add('eyeprolog__bb_get', 2, bbGetBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
211
211
|
registry.add('eyeprolog__bb_b_put', 2, bbPutBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
212
|
-
registry.add('eyeprolog__bb_global_get', 2, bbGlobalGetBuiltin, { eyePrologLibrary: true });
|
|
212
|
+
registry.add('eyeprolog__bb_global_get', 2, bbGlobalGetBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
213
213
|
registry.add('eyeprolog__bb_global_put', 2, bbGlobalPutBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
214
214
|
registry.add('eyeprolog__char_type', 2, charTypeBuiltin, { eyePrologLibrary: true });
|
|
215
215
|
registry.add('eyeprolog__current_time', 1, currentTimeBuiltin, { deterministic: true, eyePrologLibrary: true });
|
package/src/solver.js
CHANGED
|
@@ -614,7 +614,21 @@ export class Solver {
|
|
|
614
614
|
|
|
615
615
|
const def = callable ? this.registry.get(goal.name, goal.arity) : null;
|
|
616
616
|
this.active = active;
|
|
617
|
-
|
|
617
|
+
const builtinReady = def && builtinIsReadyOrAuthoritative(def, this, goal, env);
|
|
618
|
+
if (builtinReady && typeof def.expandGoal === 'function') {
|
|
619
|
+
// Meta-calls execute in this continuation instead of hiding their
|
|
620
|
+
// search behind a host-generator frame. Their real clause, control,
|
|
621
|
+
// and builtin frames therefore determine whether another answer is
|
|
622
|
+
// pending exactly as they do for a direct call. A fresh active marker
|
|
623
|
+
// keeps cut opaque at the call/N boundary.
|
|
624
|
+
const expanded = def.expandGoal({ solver: this, goal, env });
|
|
625
|
+
const invocation = { goal, env };
|
|
626
|
+
active = [...active, invocation];
|
|
627
|
+
goals = [expanded, { kind: 'releaseActive' }, ...rest];
|
|
628
|
+
depth++;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (builtinReady) {
|
|
618
632
|
const deterministic = def.deterministic ||
|
|
619
633
|
def.deterministicWhen?.({ solver: this, goal, env }) === true;
|
|
620
634
|
const iterator = def.handler({ solver: this, goal, env });
|
|
@@ -1414,10 +1428,7 @@ function bundledBetweenIterator(solver, group, goal, env) {
|
|
|
1414
1428
|
// the generated value in the caller repeatedly revisits all earlier frames.
|
|
1415
1429
|
// Enumerate the canonical bundled relation directly while leaving user
|
|
1416
1430
|
// definitions and non-EyeProlog registries on the ordinary Prolog path.
|
|
1417
|
-
|
|
1418
|
-
const iterator = bundledBetweenSolutions(solver, goal, env, state);
|
|
1419
|
-
iterator.hasPendingAlternatives = () => state.pending;
|
|
1420
|
-
return iterator;
|
|
1431
|
+
return bundledBetweenSolutions(solver, goal, env);
|
|
1421
1432
|
}
|
|
1422
1433
|
|
|
1423
1434
|
function requireBetweenInteger(term, env) {
|
|
@@ -1429,7 +1440,7 @@ function requireBetweenInteger(term, env) {
|
|
|
1429
1440
|
return BigInt(value.name);
|
|
1430
1441
|
}
|
|
1431
1442
|
|
|
1432
|
-
function* bundledBetweenSolutions(solver, goal, env
|
|
1443
|
+
function* bundledBetweenSolutions(solver, goal, env) {
|
|
1433
1444
|
const lower = requireBetweenInteger(goal.args[0], env);
|
|
1434
1445
|
const upper = requireBetweenInteger(goal.args[1], env);
|
|
1435
1446
|
const requested = deref(goal.args[2], env);
|
|
@@ -1439,23 +1450,15 @@ function* bundledBetweenSolutions(solver, goal, env, state) {
|
|
|
1439
1450
|
throw new PrologError('type_error(integer)', requested);
|
|
1440
1451
|
}
|
|
1441
1452
|
const value = BigInt(requested.name);
|
|
1442
|
-
if (value >= lower && value <= upper)
|
|
1443
|
-
state.pending = false;
|
|
1444
|
-
yield env;
|
|
1445
|
-
}
|
|
1446
|
-
state.pending = false;
|
|
1453
|
+
if (value >= lower && value <= upper) yield env;
|
|
1447
1454
|
return;
|
|
1448
1455
|
}
|
|
1449
1456
|
|
|
1450
1457
|
for (let value = lower; value <= upper; value++) {
|
|
1451
1458
|
const next = env.clone();
|
|
1452
1459
|
solver.stats.unify_calls++;
|
|
1453
|
-
if (unify(goal.args[2], numberTerm(value), next))
|
|
1454
|
-
state.pending = value < upper;
|
|
1455
|
-
yield next;
|
|
1456
|
-
}
|
|
1460
|
+
if (unify(goal.args[2], numberTerm(value), next)) yield next;
|
|
1457
1461
|
}
|
|
1458
|
-
state.pending = false;
|
|
1459
1462
|
}
|
|
1460
1463
|
|
|
1461
1464
|
function bundledMemberIterator(solver, group, goal, env) {
|
|
@@ -1472,33 +1475,18 @@ function bundledMemberIterator(solver, group, goal, env) {
|
|
|
1472
1475
|
let cursor = deref(goal.args[1], env);
|
|
1473
1476
|
while (isCons(cursor)) cursor = deref(cursor.args[1], env);
|
|
1474
1477
|
if (!isEmptyList(cursor)) return null;
|
|
1475
|
-
|
|
1476
|
-
const iterator = bundledMemberSolutions(solver, goal, env, state);
|
|
1477
|
-
iterator.hasPendingAlternatives = () => state.pending;
|
|
1478
|
-
return iterator;
|
|
1478
|
+
return bundledMemberSolutions(solver, goal, env);
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
-
function* bundledMemberSolutions(solver, goal, env
|
|
1481
|
+
function* bundledMemberSolutions(solver, goal, env) {
|
|
1482
1482
|
let cursor = deref(goal.args[1], env);
|
|
1483
1483
|
while (isCons(cursor)) {
|
|
1484
1484
|
const item = cursor.args[0];
|
|
1485
1485
|
cursor = deref(cursor.args[1], env);
|
|
1486
1486
|
const next = env.clone();
|
|
1487
1487
|
solver.stats.unify_calls++;
|
|
1488
|
-
if (unify(goal.args[0], item, next))
|
|
1489
|
-
state.pending = bundledMemberMayMatch(goal.args[0], cursor, env);
|
|
1490
|
-
yield next;
|
|
1491
|
-
}
|
|
1488
|
+
if (unify(goal.args[0], item, next)) yield next;
|
|
1492
1489
|
}
|
|
1493
|
-
state.pending = false;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function bundledMemberMayMatch(target, cursor, env) {
|
|
1497
|
-
while (isCons(cursor)) {
|
|
1498
|
-
if (!goalHeadTermsCannotMatch(target, cursor.args[0], env)) return true;
|
|
1499
|
-
cursor = deref(cursor.args[1], env);
|
|
1500
|
-
}
|
|
1501
|
-
return false;
|
|
1502
1490
|
}
|
|
1503
1491
|
|
|
1504
1492
|
function bundledEllipsisPlan(solver, group, goal, rest, env) {
|
|
@@ -2187,13 +2175,7 @@ function pushStreamingScalarFactRun(stack, solver, runGoals, groups, rest, env,
|
|
|
2187
2175
|
const iterator = scalarFactRunSolutions(solver, runGoals, groups, env, depth, active);
|
|
2188
2176
|
const first = iterator.next();
|
|
2189
2177
|
if (first.done) return;
|
|
2190
|
-
stack.
|
|
2191
|
-
kind: 'resumeBuiltin',
|
|
2192
|
-
iterator,
|
|
2193
|
-
goals: rest,
|
|
2194
|
-
depth: depth + runGoals.length,
|
|
2195
|
-
active,
|
|
2196
|
-
});
|
|
2178
|
+
pushResumeBuiltinFrame(stack, iterator, rest, depth + runGoals.length, active);
|
|
2197
2179
|
stack.push({
|
|
2198
2180
|
kind: 'goals',
|
|
2199
2181
|
goals: rest,
|
|
@@ -2831,10 +2813,64 @@ function builtinIsReadyOrAuthoritative(def, solver, goal, env) {
|
|
|
2831
2813
|
|
|
2832
2814
|
function iteratorMayHavePendingAlternatives(iterator) {
|
|
2833
2815
|
const predicate = iterator?.hasPendingAlternatives;
|
|
2834
|
-
return typeof predicate
|
|
2816
|
+
return typeof predicate === 'function' && predicate.call(iterator) !== false;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
function lookaheadChoiceIterator(iterator) {
|
|
2820
|
+
let buffered = null;
|
|
2821
|
+
let bufferedReady = false;
|
|
2822
|
+
let closed = false;
|
|
2823
|
+
|
|
2824
|
+
const fill = () => {
|
|
2825
|
+
if (closed || bufferedReady) return;
|
|
2826
|
+
buffered = iterator.next();
|
|
2827
|
+
bufferedReady = true;
|
|
2828
|
+
if (buffered.done) closed = true;
|
|
2829
|
+
};
|
|
2830
|
+
|
|
2831
|
+
return {
|
|
2832
|
+
next() {
|
|
2833
|
+
fill();
|
|
2834
|
+
if (buffered?.done !== false) return buffered ?? { done: true, value: undefined };
|
|
2835
|
+
const result = buffered;
|
|
2836
|
+
buffered = null;
|
|
2837
|
+
bufferedReady = false;
|
|
2838
|
+
// Ordinary relational iterators are advanced by one answer so their
|
|
2839
|
+
// exhausted state is known before this answer reaches the caller.
|
|
2840
|
+
fill();
|
|
2841
|
+
return result;
|
|
2842
|
+
},
|
|
2843
|
+
return(value) {
|
|
2844
|
+
if (!closed) iterator.return?.();
|
|
2845
|
+
closed = true;
|
|
2846
|
+
buffered = { done: true, value };
|
|
2847
|
+
bufferedReady = true;
|
|
2848
|
+
return buffered;
|
|
2849
|
+
},
|
|
2850
|
+
throw(error) {
|
|
2851
|
+
closed = true;
|
|
2852
|
+
if (typeof iterator.throw === 'function') return iterator.throw(error);
|
|
2853
|
+
throw error;
|
|
2854
|
+
},
|
|
2855
|
+
prepareClose(reason) {
|
|
2856
|
+
iterator.prepareClose?.(reason);
|
|
2857
|
+
},
|
|
2858
|
+
hasPendingAlternatives() {
|
|
2859
|
+
fill();
|
|
2860
|
+
return buffered?.done === false;
|
|
2861
|
+
},
|
|
2862
|
+
[Symbol.iterator]() { return this; },
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function choicepointAwareIterator(iterator) {
|
|
2867
|
+
return typeof iterator?.hasPendingAlternatives === 'function'
|
|
2868
|
+
? iterator
|
|
2869
|
+
: lookaheadChoiceIterator(iterator);
|
|
2835
2870
|
}
|
|
2836
2871
|
|
|
2837
2872
|
function pushResumeBuiltinFrame(stack, iterator, goals, depth, active) {
|
|
2873
|
+
iterator = choicepointAwareIterator(iterator);
|
|
2838
2874
|
if (!iteratorMayHavePendingAlternatives(iterator)) {
|
|
2839
2875
|
// Finish the suspended generator now so its finally blocks release child
|
|
2840
2876
|
// solvers, aggregate statistics, and perform predicate-local cleanup.
|
|
@@ -97,8 +97,8 @@ required permission error. `read_term/3`, `write_term/3`, `op/3`, and
|
|
|
97
97
|
EyeProlog's chosen simultaneous-error behavior stable. Section 7.12 makes that
|
|
98
98
|
choice implementation dependent when several error conditions hold at once. Strict `write_term/2-3` accepts only
|
|
99
99
|
the Part 1 plus Corrigendum 3 option surface; the normal-profile
|
|
100
|
-
`double_quotes/1` write
|
|
101
|
-
|
|
100
|
+
`double_quotes/1` and `spacing/1` write options remain explicitly documented
|
|
101
|
+
implementation-specific EyeProlog extensions.
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
The preparation, stream, conversion, sorting, and arithmetic audit expands the coverage into Prolog-text,
|
|
@@ -124,7 +124,7 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
|
|
|
124
124
|
| 5.5.9 Built-in predicates | EyeProlog libraries, CLP(Z), statistics, Part 3 `phrase/2-3`, and interop autoloaded predicates | Strict registry contains only the Part 1 + Corrigenda core registry. |
|
|
125
125
|
| 5.5.10 Evaluable functors | Normal mode additionally accepts the EyeProlog evaluable atom `e`; the remaining arithmetic functors accepted by strict mode are the Part 1 + Corrigenda set. | Strict mode rejects `e/0` as non-evaluable and retains the Corrigendum arithmetic additions. **covered** — strict extension-boundary regression plus `src/iso-arithmetic.js`. |
|
|
126
126
|
| 5.5.11 Reserved atoms | None | None. |
|
|
127
|
-
| Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3`
|
|
127
|
+
| Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3` options `double_quotes(true|false)` and `spacing(minimal|standard)`. `minimal` emits only lexically required separators; `standard` adds conventional layout around operators. Strict core excludes these implementation-specific extensions and accepts only the Part 1 plus Corrigendum 3 write-option surface; unknown extension options raise `domain_error(write_option,...)`. |
|
|
128
128
|
|
|
129
129
|
Normal mode provides documented module and DCG compatibility profiles whose
|
|
130
130
|
features overlap standardized Part 2 and Part 3 facilities. They are extensions
|
package/test/run-cleanup.mjs
CHANGED
|
@@ -89,6 +89,18 @@ export function runCleanup(reporter) {
|
|
|
89
89
|
assertEqual(result.stderr, '', 'stderr');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
reporter.test('setup_call_cleanup/3 exposes deterministic Cleanup substitutions (issue #77)', () => {
|
|
93
|
+
const result = runRepl(
|
|
94
|
+
'setup_call_cleanup(true,A=1,throw(called(A))).\n' +
|
|
95
|
+
'setup_call_cleanup(true,A=1,B=1).\n' +
|
|
96
|
+
'halt.\n',
|
|
97
|
+
);
|
|
98
|
+
assertEqual(result.status, 0, 'exit status');
|
|
99
|
+
assertIncludes(result.stdout, 'throw(called(1)).', 'Cleanup sees Goal substitutions');
|
|
100
|
+
assertIncludes(result.stdout, 'A = 1, B = 1.', 'answer includes Cleanup substitutions');
|
|
101
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
102
|
+
});
|
|
103
|
+
|
|
92
104
|
reporter.test('setup_call_cleanup/3 does not install cleanup when Setup fails', () => {
|
|
93
105
|
const result = runRepl('setup_call_cleanup(fail,true,write(should_not_run)).\nhalt.\n');
|
|
94
106
|
assertEqual(result.status, 0, 'status');
|
package/test/run-iso-strict.mjs
CHANGED
|
@@ -283,8 +283,16 @@ export function runIsoStrict(reporter = new TestReporter()) {
|
|
|
283
283
|
}));
|
|
284
284
|
equal(strictError.formal, 'domain_error(write_option)', 'strict extension rejection');
|
|
285
285
|
|
|
286
|
+
const strictSpacingError = capture(() => run('', {
|
|
287
|
+
isoStrict: true,
|
|
288
|
+
goal: 'write_term(1+1,[spacing(minimal)])',
|
|
289
|
+
}));
|
|
290
|
+
equal(strictSpacingError.formal, 'domain_error(write_option)', 'strict spacing extension rejection');
|
|
291
|
+
|
|
286
292
|
const normal = run('', { goal: 'write_term("ab",[double_quotes(true)])' });
|
|
287
293
|
includes(normal.stdout, '"ab"', 'normal-profile extension remains available');
|
|
294
|
+
includes(run('', { goal: 'write_term(1+1,[spacing(standard)])' }).stdout,
|
|
295
|
+
'1 + 1', 'normal-profile spacing extension remains available');
|
|
288
296
|
});
|
|
289
297
|
|
|
290
298
|
reporter.test('follows Corrigendum 3 variable metadata traversal and write naming', () => {
|
package/test/run-regression.mjs
CHANGED
|
@@ -1032,6 +1032,108 @@ c4 ?- call((!;1)).
|
|
|
1032
1032
|
}
|
|
1033
1033
|
},
|
|
1034
1034
|
},
|
|
1035
|
+
{
|
|
1036
|
+
name: 'read/2 and number_chars/2 agree on bounded numeric syntax (issue #29)',
|
|
1037
|
+
run: () => {
|
|
1038
|
+
const registry = createDefaultRegistry();
|
|
1039
|
+
const numberChars = registry.get('number_chars', 2).handler;
|
|
1040
|
+
const read = registry.get('read', 2).handler;
|
|
1041
|
+
const solver = new Solver(Program.parse(''), { registry, ioOptions: { input: '' } });
|
|
1042
|
+
const stream = solver.io.resolve(0);
|
|
1043
|
+
const streamTerm = compound('$stream', [numberTerm('0')]);
|
|
1044
|
+
const alphabet = ['0', '1', '2', '7', '8', '9', 'a', 'f', 'x', 'e', 'E', '+', '-', '.', "'", '\\', ' '];
|
|
1045
|
+
let checked = 0;
|
|
1046
|
+
let accepted = 0;
|
|
1047
|
+
|
|
1048
|
+
const visit = (prefix, remaining) => {
|
|
1049
|
+
if (remaining === 0) {
|
|
1050
|
+
checked++;
|
|
1051
|
+
const converted = variable('Converted');
|
|
1052
|
+
const conversionGoal = compound('number_chars', [
|
|
1053
|
+
converted,
|
|
1054
|
+
listFromItems(Array.from(prefix, atom)),
|
|
1055
|
+
]);
|
|
1056
|
+
let conversion;
|
|
1057
|
+
try {
|
|
1058
|
+
conversion = numberChars({ goal: conversionGoal, env: new Env() }).next();
|
|
1059
|
+
} catch (_) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (conversion.done) return;
|
|
1063
|
+
accepted++;
|
|
1064
|
+
const expected = copyResolved(converted, conversion.value);
|
|
1065
|
+
|
|
1066
|
+
stream.content = `${prefix}. `;
|
|
1067
|
+
stream.position = 0;
|
|
1068
|
+
stream.pastEnd = false;
|
|
1069
|
+
const readValue = variable('ReadValue');
|
|
1070
|
+
const readGoal = compound('read', [streamTerm, readValue]);
|
|
1071
|
+
const answer = read({ solver, goal: readGoal, env: new Env() }).next();
|
|
1072
|
+
if (answer.done) throw new Error(`read/2 rejected number_chars/2 spelling ${JSON.stringify(prefix)}`);
|
|
1073
|
+
const actual = copyResolved(readValue, answer.value);
|
|
1074
|
+
assertEqual(actual.type, 'number', `read/2 type for ${JSON.stringify(prefix)}`);
|
|
1075
|
+
assertEqual(actual.name, expected.name, `read/2 value for ${JSON.stringify(prefix)}`);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
for (const character of alphabet) visit(prefix + character, remaining - 1);
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
for (let length = 1; length <= 4; length++) visit('', length);
|
|
1082
|
+
assertEqual(checked, 88740, 'bounded numeric spellings checked');
|
|
1083
|
+
if (accepted < 1000) throw new Error(`unexpectedly small accepted numeric corpus: ${accepted}`);
|
|
1084
|
+
},
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
name: 'number_chars/2 to read/2 cross-check stays bounded under a small heap (issue #29)',
|
|
1088
|
+
run: () => {
|
|
1089
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
1090
|
+
const script = `
|
|
1091
|
+
import {
|
|
1092
|
+
Program, Solver, Env, atom, compound, variable, listFromItems,
|
|
1093
|
+
numberTerm, copyResolved, createDefaultRegistry,
|
|
1094
|
+
} from ${JSON.stringify(engineUrl)};
|
|
1095
|
+
const registry = createDefaultRegistry();
|
|
1096
|
+
const numberChars = registry.get('number_chars', 2).handler;
|
|
1097
|
+
const read = registry.get('read', 2).handler;
|
|
1098
|
+
const solver = new Solver(Program.parse(''), { registry, ioOptions: { input: '' } });
|
|
1099
|
+
const stream = solver.io.resolve(0);
|
|
1100
|
+
const streamTerm = compound('$stream', [numberTerm('0')]);
|
|
1101
|
+
for (let i = 0; i < 250000; i++) {
|
|
1102
|
+
const text = String(i);
|
|
1103
|
+
const converted = variable('Converted');
|
|
1104
|
+
const conversionGoal = compound('number_chars', [
|
|
1105
|
+
converted,
|
|
1106
|
+
listFromItems(Array.from(text, atom)),
|
|
1107
|
+
]);
|
|
1108
|
+
const conversion = numberChars({ goal: conversionGoal, env: new Env() }).next();
|
|
1109
|
+
if (conversion.done) throw new Error('number_chars/2 failed at ' + i);
|
|
1110
|
+
const expected = copyResolved(converted, conversion.value);
|
|
1111
|
+
|
|
1112
|
+
stream.content = text + '. ';
|
|
1113
|
+
stream.position = 0;
|
|
1114
|
+
stream.pastEnd = false;
|
|
1115
|
+
const readValue = variable('ReadValue');
|
|
1116
|
+
const readGoal = compound('read', [streamTerm, readValue]);
|
|
1117
|
+
const answer = read({ solver, goal: readGoal, env: new Env() }).next();
|
|
1118
|
+
if (answer.done) throw new Error('read/2 failed at ' + i);
|
|
1119
|
+
const actual = copyResolved(readValue, answer.value);
|
|
1120
|
+
if (actual.type !== 'number' || actual.name !== expected.name) {
|
|
1121
|
+
throw new Error('number mismatch at ' + i + ': ' + actual.name + ' != ' + expected.name);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
process.stdout.write('250000');
|
|
1125
|
+
`;
|
|
1126
|
+
const result = spawnSync(process.execPath, [
|
|
1127
|
+
'--max-old-space-size=32',
|
|
1128
|
+
'--input-type=module',
|
|
1129
|
+
'--eval',
|
|
1130
|
+
script,
|
|
1131
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
1132
|
+
if (result.error) throw result.error;
|
|
1133
|
+
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
1134
|
+
assertEqual(result.stdout, '250000', 'number_chars/read cross-check count');
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1035
1137
|
{
|
|
1036
1138
|
name: 'number_chars and number_codes keep exponent floats syntactically readable (issue #50)',
|
|
1037
1139
|
run: () => {
|
|
@@ -1897,11 +1999,13 @@ c4 ?- call((!;1)).
|
|
|
1897
1999
|
},
|
|
1898
2000
|
},
|
|
1899
2001
|
{
|
|
1900
|
-
name: 'predicates discard exhausted choicepoints
|
|
2002
|
+
name: 'direct and meta-called predicates discard exhausted choicepoints (issue #74 comment 5427164365)',
|
|
1901
2003
|
run: () => {
|
|
1902
2004
|
const result = runCli([], {
|
|
1903
2005
|
input:
|
|
1904
2006
|
'X=1;X=2.\n;\n' +
|
|
2007
|
+
'call(X=1).\n' +
|
|
2008
|
+
'call(=(X),1).\n' +
|
|
1905
2009
|
'setof(X,true,Xs).\n' +
|
|
1906
2010
|
'findall(t,false,Xs).\n' +
|
|
1907
2011
|
'use_module(library(lists)).\n' +
|
|
@@ -1913,6 +2017,8 @@ c4 ?- call((!;1)).
|
|
|
1913
2017
|
assertEqual(result.stdout,
|
|
1914
2018
|
'?- X = 1\n' +
|
|
1915
2019
|
'; X = 2.\n' +
|
|
2020
|
+
'?- X = 1.\n' +
|
|
2021
|
+
'?- X = 1.\n' +
|
|
1916
2022
|
'?- Xs = [_A].\n' +
|
|
1917
2023
|
'?- Xs = [].\n' +
|
|
1918
2024
|
'?- true.\n' +
|
|
@@ -1942,6 +2048,16 @@ c4 ?- call((!;1)).
|
|
|
1942
2048
|
};
|
|
1943
2049
|
|
|
1944
2050
|
finalAnswerHasNoFrames('', 'X=1;X=2', 2);
|
|
2051
|
+
finalAnswerHasNoFrames('', 'call(X=1)', 1);
|
|
2052
|
+
finalAnswerHasNoFrames('', 'call(=(X),1)', 1);
|
|
2053
|
+
finalAnswerHasNoFrames('', 'atom_concat(a,X,ab)', 1);
|
|
2054
|
+
finalAnswerHasNoFrames('', 'sub_atom(ab,0,2,0,S)', 1);
|
|
2055
|
+
finalAnswerHasNoFrames('', 'current_prolog_flag(double_quotes,_Value)', 1);
|
|
2056
|
+
finalAnswerHasNoFrames('', "current_op(700,xfx,'=')", 1);
|
|
2057
|
+
finalAnswerHasNoFrames(':- dynamic(p/1).\np(a).', 'clause(p(X),true)', 1);
|
|
2058
|
+
finalAnswerHasNoFrames(':- dynamic(p/1).\np(a).', 'retract(p(X))', 1);
|
|
2059
|
+
finalAnswerHasNoFrames('', 'catch(true,_,fail)', 1);
|
|
2060
|
+
finalAnswerHasNoFrames('', 'phrase([],[])', 1);
|
|
1945
2061
|
finalAnswerHasNoFrames('', 'setof(X,true,Xs)', 1);
|
|
1946
2062
|
finalAnswerHasNoFrames('', 'findall(t,false,Xs)', 1);
|
|
1947
2063
|
finalAnswerHasNoFrames(':- use_module(library(lists)).', 'member(a,"ba")', 1, 'lists');
|
|
@@ -1954,6 +2070,82 @@ c4 ?- call((!;1)).
|
|
|
1954
2070
|
assertEqual(groupedSolutions.next().done, false, 'first grouped setof/3 answer');
|
|
1955
2071
|
assertEqual(grouped.hasPendingAlternatives(), true, 'genuine grouped setof/3 choicepoint');
|
|
1956
2072
|
groupedSolutions.return();
|
|
2073
|
+
|
|
2074
|
+
const called = new Solver(Program.parse(''), { registry: getEyePrologRegistry() });
|
|
2075
|
+
const calledSolutions = called.solve([parseGoalText('call((X=1;X=2))')], new Env(), 0);
|
|
2076
|
+
assertEqual(calledSolutions.next().done, false, 'called disjunction first answer');
|
|
2077
|
+
assertEqual(called.hasPendingAlternatives(), true, 'called disjunction retains its real alternative');
|
|
2078
|
+
assertEqual(calledSolutions.next().done, false, 'called disjunction second answer');
|
|
2079
|
+
assertEqual(called.hasPendingAlternatives(), false, 'called disjunction removes its exhausted alternative');
|
|
2080
|
+
calledSolutions.return();
|
|
2081
|
+
|
|
2082
|
+
const customRegistry = new BuiltinRegistry();
|
|
2083
|
+
customRegistry.add('single', 1, function* ({ goal, env }) {
|
|
2084
|
+
const next = env.clone();
|
|
2085
|
+
if (unify(goal.args[0], atom('only'), next)) yield next;
|
|
2086
|
+
});
|
|
2087
|
+
const custom = new Solver(Program.parse(''), { registry: customRegistry });
|
|
2088
|
+
const customSolutions = custom.solve([parseGoalText('single(X)')], new Env(), 0);
|
|
2089
|
+
assertEqual(customSolutions.next().done, false, 'custom relational iterator answer');
|
|
2090
|
+
assertEqual(custom.hasPendingAlternatives(), false,
|
|
2091
|
+
'unannotated relational iterator uses the universal lookahead protocol');
|
|
2092
|
+
customSolutions.return();
|
|
2093
|
+
|
|
2094
|
+
const dynamicProgram = Program.parse(':- dynamic(p/1).\np(a).\np(b).\n');
|
|
2095
|
+
const dynamicSolver = new Solver(dynamicProgram, { registry: getEyePrologRegistry() });
|
|
2096
|
+
const dynamicSolutions = dynamicSolver.solve([parseGoalText('catch(retract(p(X)),_,fail)')], new Env(), 0);
|
|
2097
|
+
assertEqual(dynamicSolutions.next().done, false, 'stateful protected iterator first answer');
|
|
2098
|
+
assertEqual(dynamicProgram.findGroup('p', 1).clauses.length, 1,
|
|
2099
|
+
'lookahead does not perform the next retraction');
|
|
2100
|
+
assertEqual(dynamicSolver.hasPendingAlternatives(), true,
|
|
2101
|
+
'stateful protected iterator retains its real second retraction');
|
|
2102
|
+
dynamicSolutions.return();
|
|
2103
|
+
assertEqual(dynamicProgram.findGroup('p', 1).clauses.length, 1,
|
|
2104
|
+
'closing stateful iterator leaves the unrequested clause intact');
|
|
2105
|
+
},
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
name: 'bound member tails do not invent predicate choicepoints (issue #74 follow-up)',
|
|
2109
|
+
run: () => {
|
|
2110
|
+
const result = runCli([], {
|
|
2111
|
+
input:
|
|
2112
|
+
'use_module(library(lists)).\n' +
|
|
2113
|
+
'L=[i],append(L,_,[i|L]).\n' +
|
|
2114
|
+
'L=[_],append(L,_,[i|L]).\n' +
|
|
2115
|
+
'L=[i],append(L,_,[i|L]),member(a,[a|L]).\n' +
|
|
2116
|
+
'L=[_],append(L,_,[i|L]),member(a,[a|L]).\n' +
|
|
2117
|
+
'halt.\n',
|
|
2118
|
+
});
|
|
2119
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2120
|
+
assertEqual(result.stdout,
|
|
2121
|
+
'?- true.\n' +
|
|
2122
|
+
'?- L = "i".\n' +
|
|
2123
|
+
'?- L = "i".\n' +
|
|
2124
|
+
'?- L = "i".\n' +
|
|
2125
|
+
'?- L = "i".\n' +
|
|
2126
|
+
'?- ',
|
|
2127
|
+
'ground and formerly variable list elements have identical determinism');
|
|
2128
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2129
|
+
},
|
|
2130
|
+
},
|
|
2131
|
+
{
|
|
2132
|
+
name: 'length/2 reports fixed and closed-list answers as deterministic (issue #78)',
|
|
2133
|
+
run: () => {
|
|
2134
|
+
const result = runCli([], {
|
|
2135
|
+
input:
|
|
2136
|
+
'use_module(library(lists)).\n' +
|
|
2137
|
+
'length(K,0).\n' +
|
|
2138
|
+
'length([],N).\n' +
|
|
2139
|
+
'halt.\n',
|
|
2140
|
+
});
|
|
2141
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2142
|
+
assertEqual(result.stdout,
|
|
2143
|
+
'?- true.\n' +
|
|
2144
|
+
'?- K = [].\n' +
|
|
2145
|
+
'?- N = 0.\n' +
|
|
2146
|
+
'?- ',
|
|
2147
|
+
'no exhausted native length iterator remains');
|
|
2148
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1957
2149
|
},
|
|
1958
2150
|
},
|
|
1959
2151
|
{
|
|
@@ -3884,6 +4076,15 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
3884
4076
|
{
|
|
3885
4077
|
name: 'REPL answers use only lexically required operator spacing (issue #76)',
|
|
3886
4078
|
run: () => {
|
|
4079
|
+
const source = [
|
|
4080
|
+
'emit :-',
|
|
4081
|
+
' write_term(1+1, [spacing(minimal)]), put_char(\'|\'),',
|
|
4082
|
+
' write_term(1+1, [spacing(standard)]).',
|
|
4083
|
+
'',
|
|
4084
|
+
].join('\n');
|
|
4085
|
+
assertEqual(run(source, { goal: 'emit' }).stdout, '1+1|1 + 1emit.\n',
|
|
4086
|
+
'documented spacing values select write_term layout');
|
|
4087
|
+
|
|
3887
4088
|
const result = runCli([], {
|
|
3888
4089
|
input: 'X=1*1.\nX=a + -b.\nhalt.\n',
|
|
3889
4090
|
});
|
|
@@ -3894,6 +4095,24 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
3894
4095
|
'?- ',
|
|
3895
4096
|
'minimal operator layout preserves required token separation');
|
|
3896
4097
|
assertEqual(result.stderr, '', 'stderr');
|
|
4098
|
+
|
|
4099
|
+
let variableError = null;
|
|
4100
|
+
try {
|
|
4101
|
+
run('', { goal: 'write_term(1+1,[spacing(X)])' });
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
variableError = error;
|
|
4104
|
+
}
|
|
4105
|
+
assertEqual(variableError?.formal, 'instantiation_error', 'variable spacing value');
|
|
4106
|
+
|
|
4107
|
+
for (const value of ['true', 'false', '1', '0']) {
|
|
4108
|
+
let domainError = null;
|
|
4109
|
+
try {
|
|
4110
|
+
run('', { goal: `write_term(1+1,[spacing(${value})])` });
|
|
4111
|
+
} catch (error) {
|
|
4112
|
+
domainError = error;
|
|
4113
|
+
}
|
|
4114
|
+
assertEqual(domainError?.formal, 'domain_error(write_option)', `spacing(${value}) domain`);
|
|
4115
|
+
}
|
|
3897
4116
|
},
|
|
3898
4117
|
},
|
|
3899
4118
|
{
|
|
@@ -5189,6 +5408,7 @@ answer(ok) :-
|
|
|
5189
5408
|
name: 'discarded recursive allocations recover after resource_error(memory)',
|
|
5190
5409
|
run: () => {
|
|
5191
5410
|
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
5411
|
+
const platformUrl = new URL('../src/platform.js', import.meta.url).href;
|
|
5192
5412
|
const programText = `
|
|
5193
5413
|
:- use_module(library(prologue)).
|
|
5194
5414
|
l([]).
|
|
@@ -5197,17 +5417,15 @@ answer(ok) :-
|
|
|
5197
5417
|
const reportedGoal = 'length(_,I),N is 2^I,\\+ \\+ (length(L,N),l(L)),L=[_|_]';
|
|
5198
5418
|
const script = `
|
|
5199
5419
|
import { Program, Solver, Env, deref, variable, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
5420
|
+
import { usedHeapSize } from ${JSON.stringify(platformUrl)};
|
|
5200
5421
|
const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
|
|
5201
|
-
const
|
|
5202
|
-
const execute = (text) => {
|
|
5203
|
-
const goal = parseGoalText(text, { operatorDefinitions: [...program.operators.values()] });
|
|
5204
|
-
return [...solver.solve([goal], new Env(), 0)];
|
|
5205
|
-
};
|
|
5422
|
+
const registry = getEyePrologRegistry();
|
|
5206
5423
|
const reported = parseGoalText(${JSON.stringify(reportedGoal)}, {
|
|
5207
5424
|
operatorDefinitions: [...program.operators.values()],
|
|
5208
5425
|
});
|
|
5426
|
+
const reportedSolver = new Solver(program, { registry, maxMemoryBytes: Infinity });
|
|
5209
5427
|
let reportedAnswers = 0;
|
|
5210
|
-
for (const answer of
|
|
5428
|
+
for (const answer of reportedSolver.solve([reported], new Env(), 0)) {
|
|
5211
5429
|
reportedAnswers++;
|
|
5212
5430
|
if (reportedAnswers !== 14) continue;
|
|
5213
5431
|
if (deref(variable('I'), answer).name !== '13' || deref(variable('N'), answer).name !== '8192') {
|
|
@@ -5216,6 +5434,18 @@ answer(ok) :-
|
|
|
5216
5434
|
break;
|
|
5217
5435
|
}
|
|
5218
5436
|
if (reportedAnswers !== 14) throw new Error('reported query produced too few answers');
|
|
5437
|
+
|
|
5438
|
+
// Exercise EyeProlog's recovery window at a stable threshold relative
|
|
5439
|
+
// to the heap after the preliminary reproduction. Keeping that probe
|
|
5440
|
+
// unlimited prevents host GC timing from exhausting the test early.
|
|
5441
|
+
const solver = new Solver(program, {
|
|
5442
|
+
registry,
|
|
5443
|
+
maxMemoryBytes: usedHeapSize() + 8 * 1024 * 1024,
|
|
5444
|
+
});
|
|
5445
|
+
const execute = (text) => {
|
|
5446
|
+
const goal = parseGoalText(text, { operatorDefinitions: [...program.operators.values()] });
|
|
5447
|
+
return [...solver.solve([goal], new Env(), 0)];
|
|
5448
|
+
};
|
|
5219
5449
|
let caught = null;
|
|
5220
5450
|
try { execute('length(L,32768),l(L)'); } catch (error) { caught = error; }
|
|
5221
5451
|
if (caught?.formal !== 'resource_error(memory)') throw caught ?? new Error('no resource error');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -2017,7 +2017,12 @@ answer(X) :- host_status(service, X).
|
|
|
2017
2017
|
```
|
|
2018
2018
|
|
|
2019
2019
|
Only mark a built-in deterministic when it can produce at most one environment
|
|
2020
|
-
for a call.
|
|
2020
|
+
for a call. An unmarked resumable handler is treated as an effect-free relation:
|
|
2021
|
+
the solver may buffer one answer to determine its exact pending state. A
|
|
2022
|
+
stateful or meta-control handler must instead return an iterator with a
|
|
2023
|
+
`hasPendingAlternatives()` method, updated before each yield, so the solver can
|
|
2024
|
+
remain demand-driven without advancing it speculatively. A mode-sensitive
|
|
2025
|
+
extension can additionally provide `ready`,
|
|
2021
2026
|
`fallbackWhenNotReady`, and `shouldUse` metadata. This metadata affects
|
|
2022
2027
|
dispatch and safe early filtering, so it belongs to the extension's contract.
|
|
2023
2028
|
|
|
@@ -5136,8 +5141,12 @@ at a named boundary.
|
|
|
5136
5141
|
## 34. Control, exceptions, and grouped solutions
|
|
5137
5142
|
|
|
5138
5143
|
The control predicates accept goals as arguments. `call/1` invokes a callable
|
|
5139
|
-
term, `
|
|
5140
|
-
|
|
5144
|
+
term, and `call/2-8` appends arguments to a callable closure. The expanded goal
|
|
5145
|
+
runs in the current search continuation, so a direct goal and its meta-called
|
|
5146
|
+
form expose the same remaining alternatives; the meta-call still establishes
|
|
5147
|
+
its own cut boundary. `once/1` keeps its first solution, and `!/0` commits
|
|
5148
|
+
within the clause that contains it. If-then-else commits to the first successful
|
|
5149
|
+
condition:
|
|
5141
5150
|
|
|
5142
5151
|
<figure>
|
|
5143
5152
|
<img src="book-assets/iso-control-board.svg" alt="A goal passes through choice and exception recovery before finite solutions enter findall, bagof, and setof collectors.">
|
|
@@ -5187,8 +5196,10 @@ pruned, top-level answer enumeration is abandoned, or an exception unwinds the
|
|
|
5187
5196
|
search. `setup_call_cleanup/3` runs Setup once and installs Cleanup only after
|
|
5188
5197
|
Setup succeeds. On cut or ordinary pruning Cleanup sees the current Goal
|
|
5189
5198
|
bindings; on exception unwind the Goal bindings have been removed and Cleanup
|
|
5190
|
-
sees the Setup environment.
|
|
5191
|
-
|
|
5199
|
+
sees the Setup environment. When a deterministic protected goal runs Cleanup
|
|
5200
|
+
before yielding, substitutions produced by a successful Cleanup are included in
|
|
5201
|
+
that answer. Cleanup failure is ignored, and an exception already being
|
|
5202
|
+
propagated takes precedence over a cleanup exception. Nested cleanups run
|
|
5192
5203
|
inside-out. These two controls are EyeProlog extensions and are absent from
|
|
5193
5204
|
`--iso-strict`.
|
|
5194
5205
|
|
|
@@ -5427,9 +5438,15 @@ still retained where adjacent graphic tokens would otherwise merge, as in
|
|
|
5427
5438
|
`variable_names/1`. Normal mode additionally accepts the EyeProlog extension
|
|
5428
5439
|
`double_quotes(true|false)`: `true` lets eligible character/code lists use the
|
|
5429
5440
|
current `double_quotes` representation, while strict ISO mode rejects this
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5441
|
+
implementation-specific write option. Normal mode also accepts the
|
|
5442
|
+
implementation-specific `spacing(minimal|standard)` option: `minimal` emits
|
|
5443
|
+
only separators required to avoid lexical ambiguity, while `standard` adds
|
|
5444
|
+
conventional layout around operators. For example,
|
|
5445
|
+
`write_term(1+1,[spacing(minimal)])` emits `1+1` and
|
|
5446
|
+
`write_term(1+1,[spacing(standard)])` emits `1 + 1`. The REPL always follows
|
|
5447
|
+
the minimal-separator rule, so `X=1*1` is displayed as `X = 1*1`, while a
|
|
5448
|
+
separator is retained in `X = a+ -b` because the adjacent graphic tokens would
|
|
5449
|
+
otherwise merge. Strict ISO mode rejects both extension options.
|
|
5433
5450
|
|
|
5434
5451
|
Character operations are `get_char`, `peek_char`, `put_char`, `get_code`,
|
|
5435
5452
|
`peek_code`, and `put_code`; byte streams use the corresponding byte
|
|
@@ -6122,7 +6139,7 @@ the beginning. A peek does not mark the stream as past-end.
|
|
|
6122
6139
|
| `write(+Term)`, `write(+Stream,+Term)` | Writes readable operator notation without quoting atoms merely because quoting would be required for reparsing. Number variables are enabled. |
|
|
6123
6140
|
| `writeq(+Term)`, `writeq(+Stream,+Term)` | Like `write`, but quotes atoms when required for unambiguous input syntax. |
|
|
6124
6141
|
| `write_canonical(+Term)`, `write_canonical(+Stream,+Term)` | Writes quoted canonical functor notation while ignoring operators and without interpreting *$VAR/1*. |
|
|
6125
|
-
| `write_term(+Term,+Options)`, `write_term(+Stream,+Term,+Options)` | Writes with *quoted(true or false)*, *ignore_ops(true or false)*, *numbervars(true or false)*, and *variable_names([Name=Variable,...])*. Normal mode also supports *double_quotes(true or false)*. |
|
|
6142
|
+
| `write_term(+Term,+Options)`, `write_term(+Stream,+Term,+Options)` | Writes with *quoted(true or false)*, *ignore_ops(true or false)*, *numbervars(true or false)*, and *variable_names([Name=Variable,...])*. Normal mode also supports *double_quotes(true or false)* and *spacing(minimal or standard)*. |
|
|
6126
6143
|
|
|
6127
6144
|
Term input uses the program's current operator table and the same ISO quoted-character
|
|
6128
6145
|
syntax as source text, including backslash-terminated octal and hexadecimal
|
|
@@ -6132,6 +6149,12 @@ when the `char_conversion` flag is `on`. `variable_names/1` and `singletons/1` o
|
|
|
6132
6149
|
predicates do not append a period or newline; call `write/1`, then `write('.')`
|
|
6133
6150
|
and `nl/0` when emitting a complete source term manually.
|
|
6134
6151
|
|
|
6152
|
+
A standalone numeric term read by `read/1-2` or `read_term/2-3` uses the same
|
|
6153
|
+
bounded numeric scanner and canonical value conversion as `number_chars/2`.
|
|
6154
|
+
Consequently every numeric character sequence accepted by `number_chars/2` has
|
|
6155
|
+
the same value when read as a full-stop-terminated term, without requiring EOF
|
|
6156
|
+
after that term.
|
|
6157
|
+
|
|
6135
6158
|
### Arithmetic expressions
|
|
6136
6159
|
|
|
6137
6160
|
| Predicate and principal call | Behavior |
|
|
@@ -6209,7 +6232,7 @@ so side effects occur in Prolog execution order.
|
|
|
6209
6232
|
|
|
6210
6233
|
### Normal-mode cleanup controls
|
|
6211
6234
|
|
|
6212
|
-
`call_cleanup/2` and `setup_call_cleanup/3` are normal EyeProlog runtime extensions rather than members of the isolated ISO builtin registry. They protect a goal across deterministic completion, exhaustion, cut, top-level abandonment, and exception unwinding, running Cleanup exactly once. `setup_call_cleanup/3` runs Setup once and installs Cleanup only after Setup succeeds. Nested cleanups run inside-out, and strict ISO mode does not provide either predicate.
|
|
6235
|
+
`call_cleanup/2` and `setup_call_cleanup/3` are normal EyeProlog runtime extensions rather than members of the isolated ISO builtin registry. They protect a goal across deterministic completion, exhaustion, cut, top-level abandonment, and exception unwinding, running Cleanup exactly once. `setup_call_cleanup/3` runs Setup once and installs Cleanup only after Setup succeeds. A successful Cleanup run before a deterministic answer contributes its substitutions to that answer. Nested cleanups run inside-out, and strict ISO mode does not provide either predicate.
|
|
6213
6236
|
|
|
6214
6237
|
### The EyeProlog library
|
|
6215
6238
|
|
|
@@ -6363,7 +6386,9 @@ conservative cross-engine subset.
|
|
|
6363
6386
|
`length(Xs, N)` enumerates `Xs = [], N = 0`, then one-element lists with
|
|
6364
6387
|
`N = 1`, and so on. Open-ended generation uses the normal memory guard with
|
|
6365
6388
|
recovery headroom so finite-heap exhaustion remains a catchable
|
|
6366
|
-
`resource_error(memory)`.
|
|
6389
|
+
`resource_error(memory)`. A supplied nonnegative length selects at most one
|
|
6390
|
+
answer, and a supplied closed list has exactly one length; these modes do not
|
|
6391
|
+
retain an exhausted choicepoint.
|
|
6367
6392
|
|
|
6368
6393
|
`library(iso_ext)` is a common interop module name, but only part of its
|
|
6369
6394
|
EyeProlog API belongs to the shared profile. `call_nth/2`, `time/1`, and
|
|
@@ -6815,11 +6840,15 @@ When another answer exists in an interactive terminal, press `;`, Space, or
|
|
|
6815
6840
|
enumeration, `a` enumerates all remaining answers, and `f` advances to the
|
|
6816
6841
|
next five-answer boundary (5, 10, 15, ... displayed leaf answers), regardless
|
|
6817
6842
|
of how many answers were stepped through individually beforehand. `h` displays
|
|
6818
|
-
the answer-control help. Enumeration is demand-driven:
|
|
6819
|
-
found, the top level does not
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6843
|
+
the answer-control help. Enumeration of goal continuations is demand-driven:
|
|
6844
|
+
after an answer is found, the top level does not execute a later program branch
|
|
6845
|
+
or side effect merely to discover whether the current answer is the last one.
|
|
6846
|
+
The solver's uniform choicepoint protocol uses explicit clause and control
|
|
6847
|
+
frames, permits a one-answer buffer only for effect-free host relations, and
|
|
6848
|
+
requires stateful or meta-control iterators to report their pending state
|
|
6849
|
+
directly. Search that can perform an effect starts only after an answer-control
|
|
6850
|
+
command asks to continue. Stopping enumeration closes any active
|
|
6851
|
+
`call_cleanup/2` or
|
|
6823
6852
|
`setup_call_cleanup/3` protection exactly once; detecting that a choicepoint
|
|
6824
6853
|
remains does not execute that next branch. If an unresolved alternative
|
|
6825
6854
|
ultimately has no solution, asking for it may therefore finish with `false.`. In scripted non-TTY input, a new
|
|
@@ -7767,8 +7796,8 @@ Notable implementation boundaries are:
|
|
|
7767
7796
|
- `write_term/2-3` implements the Part 1 plus Corrigendum 3 `quoted/1`,
|
|
7768
7797
|
`ignore_ops/1`, `numbervars/1`, and `variable_names/1` option surface,
|
|
7769
7798
|
including option validation and traversal rules; normal mode also offers
|
|
7770
|
-
`double_quotes(true|false)`
|
|
7771
|
-
strict mode rejects;
|
|
7799
|
+
`double_quotes(true|false)` and `spacing(minimal|standard)` as explicitly
|
|
7800
|
+
implementation-specific extensions, which strict mode rejects;
|
|
7772
7801
|
- unification consistently performs an occurs check, rejecting rational-tree
|
|
7773
7802
|
bindings accepted as extensions by some systems.
|
|
7774
7803
|
|
|
@@ -7979,8 +8008,11 @@ decidable by structural equality.
|
|
|
7979
8008
|
**Clause.** A fact or rule terminated by a period.
|
|
7980
8009
|
|
|
7981
8010
|
**Choicepoint.** A remaining search alternative that may produce another
|
|
7982
|
-
answer if the caller asks the solver to continue.
|
|
7983
|
-
|
|
8011
|
+
answer if the caller asks the solver to continue. Every resumable engine
|
|
8012
|
+
iterator follows the same pending-alternative protocol. Ordinary effect-free
|
|
8013
|
+
host relations may buffer one answer to prove exhaustion; stateful and
|
|
8014
|
+
meta-control iterators report their state without executing an unrequested
|
|
8015
|
+
effect or program branch.
|
|
7984
8016
|
|
|
7985
8017
|
**Cleanup.** A protected finalization goal installed by normal-mode
|
|
7986
8018
|
`call_cleanup/2` or `setup_call_cleanup/3`. It runs exactly once when the
|