eyeprolog 1.3.83 → 1.3.85
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 +33 -12
- package/src/repl.js +18 -44
- package/src/solver.js +37 -41
- package/test/conformance/ISO-COMPLIANCE.md +1 -1
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +2 -2
- package/test/conformance/expected/iso/exceptions_and_flags.pl +1 -1
- package/test/run-cleanup.mjs +12 -0
- package/test/run-iso-strict.mjs +3 -4
- package/test/run-regression.mjs +183 -52
- package/the-art-of-eyeprolog.md +28 -31
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
|
@@ -804,7 +804,18 @@ function* currentPrologFlagBuiltin({ solver, goal, env }) {
|
|
|
804
804
|
}
|
|
805
805
|
|
|
806
806
|
function* setPrologFlagBuiltin({ solver, goal, env }) {
|
|
807
|
-
|
|
807
|
+
const flag = deref(goal.args[0], env);
|
|
808
|
+
const value = deref(goal.args[1], env);
|
|
809
|
+
if (flag.type === VAR || value.type === VAR) throw new PrologError('instantiation_error');
|
|
810
|
+
if (flag.type !== ATOM) throw new PrologError('type_error(atom)', flag);
|
|
811
|
+
const definition = solver.prologFlags.get(flag.name);
|
|
812
|
+
if (!definition) throw new PrologError('domain_error(prolog_flag)', flag);
|
|
813
|
+
const expectedType = definition.valueType ?? ATOM;
|
|
814
|
+
if (value.type !== expectedType || !definition.allowed.includes(value.name)) {
|
|
815
|
+
throw new PrologError('domain_error(flag_value)', compound('+', [flag, value]));
|
|
816
|
+
}
|
|
817
|
+
if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
|
|
818
|
+
definition.value = atom(value.name);
|
|
808
819
|
yield env;
|
|
809
820
|
}
|
|
810
821
|
|
|
@@ -1544,6 +1555,24 @@ function scopeReadTerm(term) {
|
|
|
1544
1555
|
|
|
1545
1556
|
function parseReadTermText(text, solver) {
|
|
1546
1557
|
const converted = convertedTermText(text, solver);
|
|
1558
|
+
// A standalone number needs no general term parser or operator tables.
|
|
1559
|
+
// Reuse the same bounded numeric scanner as number_chars/2 so stream reads
|
|
1560
|
+
// and number conversion agree on every accepted numeric token. The stream
|
|
1561
|
+
// scanner has already established that the final full stop is a candidate
|
|
1562
|
+
// terminator; trim only ISO layout immediately before that terminator.
|
|
1563
|
+
const numericText = converted.slice(0, -1).replace(/[\u0009-\u000d\u0020]+$/, '');
|
|
1564
|
+
let numericTerm;
|
|
1565
|
+
try {
|
|
1566
|
+
numericTerm = parseIsoNumber(numericText);
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
// parseIsoNumber/1 exposes Prolog errors to number_chars/2. The stream
|
|
1569
|
+
// reader's candidate loop instead recognizes parser representation errors
|
|
1570
|
+
// and must not reinterpret overflow as a later syntax error.
|
|
1571
|
+
if (error instanceof PrologError) throw new NumberRepresentationError(error.formal);
|
|
1572
|
+
throw error;
|
|
1573
|
+
}
|
|
1574
|
+
if (numericTerm != null) return numericTerm;
|
|
1575
|
+
|
|
1547
1576
|
const operatorState = createParserOperatorState(solver.program.operators.values(), false);
|
|
1548
1577
|
return parseTermText(converted, {
|
|
1549
1578
|
operatorState,
|
|
@@ -1712,7 +1741,7 @@ function writeOptionBoolean(value, env, option) {
|
|
|
1712
1741
|
function writeOptionSpacing(value, env, option) {
|
|
1713
1742
|
value = deref(value, env);
|
|
1714
1743
|
if (value.type === VAR) throw new PrologError('instantiation_error');
|
|
1715
|
-
if (value.type !== ATOM || !['
|
|
1744
|
+
if (value.type !== ATOM || !['minimal', 'standard'].includes(value.name)) {
|
|
1716
1745
|
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
1717
1746
|
}
|
|
1718
1747
|
return value.name;
|
|
@@ -1746,10 +1775,8 @@ function writeVariableNames(value, env, option) {
|
|
|
1746
1775
|
return names;
|
|
1747
1776
|
}
|
|
1748
1777
|
|
|
1749
|
-
function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = null
|
|
1750
|
-
const result =
|
|
1751
|
-
? defaultTermWriteOptions(mode)
|
|
1752
|
-
: { ...initial, variableNames: new Map(initial.variableNames ?? []) };
|
|
1778
|
+
function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = null) {
|
|
1779
|
+
const result = defaultTermWriteOptions(mode);
|
|
1753
1780
|
for (const option of options) {
|
|
1754
1781
|
if (option.type !== COMPOUND || option.arity !== 1) {
|
|
1755
1782
|
throw new PrologError('domain_error(write_option)', copyResolved(option, env));
|
|
@@ -1769,12 +1796,6 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
|
|
|
1769
1796
|
return result;
|
|
1770
1797
|
}
|
|
1771
1798
|
|
|
1772
|
-
export function parseTermWriteOptions(optionTerm, env, mode = 'write_term', solver = null, initial = null) {
|
|
1773
|
-
const preflight = preflightOptionList(optionTerm, env);
|
|
1774
|
-
const items = requireProperOptionList(preflight);
|
|
1775
|
-
return termWriteOptionsFromItems(items, env, mode, solver, initial);
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
1799
|
|
|
1779
1800
|
function writeBuiltin(mode) {
|
|
1780
1801
|
return function* ({ solver, goal, env }) {
|
package/src/repl.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import { readSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
|
-
import { formalErrorTerm
|
|
6
|
+
import { formalErrorTerm } from './iso.js';
|
|
7
7
|
import {
|
|
8
8
|
characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
|
|
9
9
|
} from './syntax-scan.js';
|
|
@@ -303,18 +303,20 @@ function makeState(engine, sources, output, options = {}, previousState = null,
|
|
|
303
303
|
const flagOverrides = new Map(previousState?.flagOverrides ?? []);
|
|
304
304
|
for (const [name, value] of flagOverrides) {
|
|
305
305
|
const definition = solver.prologFlags.get(name);
|
|
306
|
-
if (definition?.changeable
|
|
306
|
+
if (definition?.changeable && definition.allowed.includes(value)) {
|
|
307
|
+
definition.value = engine.atom(value);
|
|
308
|
+
}
|
|
307
309
|
}
|
|
308
310
|
return { program: solver.program, solver, strictIso, flagOverrides };
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
function snapshotFlagValues(solver) {
|
|
312
|
-
return new Map([...solver.prologFlags].map(([name, definition]) => [name, definition.value]));
|
|
314
|
+
return new Map([...solver.prologFlags].map(([name, definition]) => [name, definition.value?.name]));
|
|
313
315
|
}
|
|
314
316
|
|
|
315
317
|
function rememberFlagOverrides(state, before) {
|
|
316
318
|
for (const [name, definition] of state.solver.prologFlags) {
|
|
317
|
-
const value = definition.value;
|
|
319
|
+
const value = definition.value?.name;
|
|
318
320
|
if (before.get(name) !== value) state.flagOverrides.set(name, value);
|
|
319
321
|
}
|
|
320
322
|
}
|
|
@@ -718,7 +720,15 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
718
720
|
const valueMaxPriority = equality == null
|
|
719
721
|
? 699
|
|
720
722
|
: equality.specifier === 'xfy' ? equality.priority : equality.priority - 1;
|
|
721
|
-
const
|
|
723
|
+
const answerWriteOptions = {
|
|
724
|
+
quoted: true,
|
|
725
|
+
minimalOperatorSpacing: true,
|
|
726
|
+
operators,
|
|
727
|
+
variableNames: names,
|
|
728
|
+
operatorAtomsAsArgs: true,
|
|
729
|
+
dottedGraphicAtoms: true,
|
|
730
|
+
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
731
|
+
};
|
|
722
732
|
let generated = 0;
|
|
723
733
|
|
|
724
734
|
// Meta-predicate wrappers can make a query variable alias an internal fresh
|
|
@@ -733,28 +743,18 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
733
743
|
if (value.type === 'var' &&
|
|
734
744
|
(value.name === variable.name || !queryVariableNames.has(value.name))) continue;
|
|
735
745
|
bindings.push(`${variable.name} = ${engine.formatTermForWrite(value, env, {
|
|
736
|
-
...
|
|
737
|
-
operators,
|
|
738
|
-
variableNames: names,
|
|
746
|
+
...answerWriteOptions,
|
|
739
747
|
maxPriority: valueMaxPriority,
|
|
740
748
|
// A binding value is the right-hand `arg` of =/2. ISO 6.3.3.1 permits
|
|
741
749
|
// current operator atoms in argument and list-element positions without
|
|
742
750
|
// quotes, just as writeq/1 already prints them.
|
|
743
|
-
operatorAtomsAsArgs: true,
|
|
744
|
-
dottedGraphicAtoms: true,
|
|
745
751
|
})}`);
|
|
746
752
|
}
|
|
747
753
|
for (const constraint of env.variableConstraints?.() ?? []) {
|
|
748
754
|
const residual = constraint.residualGoal?.(env);
|
|
749
755
|
if (residual == null) continue;
|
|
750
756
|
collectUnboundVariables(engine, residual, env, names, () => `_${letterName(generated++)}`);
|
|
751
|
-
bindings.push(engine.formatTermForWrite(residual, env,
|
|
752
|
-
...writeOptions,
|
|
753
|
-
operators,
|
|
754
|
-
variableNames: names,
|
|
755
|
-
operatorAtomsAsArgs: true,
|
|
756
|
-
dottedGraphicAtoms: true,
|
|
757
|
-
}));
|
|
757
|
+
bindings.push(engine.formatTermForWrite(residual, env, answerWriteOptions));
|
|
758
758
|
}
|
|
759
759
|
const projectedAttributeRoots = new Set();
|
|
760
760
|
for (const variable of variables) {
|
|
@@ -764,38 +764,12 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
764
764
|
names.set(root.name, variable.name);
|
|
765
765
|
for (const residual of state.solver.attributeResidualGoals(root, env)) {
|
|
766
766
|
collectUnboundVariables(engine, residual, env, names, () => `_${letterName(generated++)}`);
|
|
767
|
-
bindings.push(engine.formatTermForWrite(residual, env,
|
|
768
|
-
...writeOptions,
|
|
769
|
-
operators,
|
|
770
|
-
variableNames: names,
|
|
771
|
-
operatorAtomsAsArgs: true,
|
|
772
|
-
dottedGraphicAtoms: true,
|
|
773
|
-
}));
|
|
767
|
+
bindings.push(engine.formatTermForWrite(residual, env, answerWriteOptions));
|
|
774
768
|
}
|
|
775
769
|
}
|
|
776
770
|
return bindings.length === 0 ? 'true' : bindings.join(', ');
|
|
777
771
|
}
|
|
778
772
|
|
|
779
|
-
function answerTermWriteOptions(state, env) {
|
|
780
|
-
const doubleQuotes = state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars';
|
|
781
|
-
const initial = {
|
|
782
|
-
quoted: true,
|
|
783
|
-
ignoreOps: false,
|
|
784
|
-
numbervars: true,
|
|
785
|
-
variableNames: new Map(),
|
|
786
|
-
compact: false,
|
|
787
|
-
minimalOperatorSpacing: false,
|
|
788
|
-
operatorAtomsAsArgs: true,
|
|
789
|
-
doubleQuotes,
|
|
790
|
-
};
|
|
791
|
-
const optionTerm = state.solver.prologFlags.get('answer_write_options')?.value;
|
|
792
|
-
if (optionTerm == null) return initial;
|
|
793
|
-
const options = parseTermWriteOptions(optionTerm, env, 'write_term', state.solver, initial);
|
|
794
|
-
if (options.doubleQuotes === true) options.doubleQuotes = doubleQuotes;
|
|
795
|
-
else if (options.doubleQuotes === false) options.doubleQuotes = null;
|
|
796
|
-
return options;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
773
|
function collectUnboundVariables(engine, term, env, names, nextName) {
|
|
800
774
|
const stack = [term];
|
|
801
775
|
const seen = new Set();
|
package/src/solver.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Depth-first EyeProlog solver with builtin dispatch, memoization, and guarded recursion handling.
|
|
2
2
|
// Most semantic decisions still flow through unification; optimizations only select candidates earlier.
|
|
3
3
|
import {
|
|
4
|
-
ATOM, COMPOUND, NUMBER, STRING, VAR, Env,
|
|
4
|
+
ATOM, COMPOUND, NUMBER, STRING, VAR, Env, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
|
|
5
5
|
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
6
|
-
|
|
6
|
+
numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
8
8
|
import { numberValueKey, sameNumberValue } from './number-value.js';
|
|
9
|
-
import { PrologError, getStrictIsoRegistry
|
|
9
|
+
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
10
10
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
11
11
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
|
|
12
12
|
import { StreamManager } from './io.js';
|
|
@@ -144,7 +144,15 @@ export class Solver {
|
|
|
144
144
|
this.prologFlags.get('double_quotes').value = compound(program.doubleQuotes, []);
|
|
145
145
|
}
|
|
146
146
|
for (const [flag, value] of program.prologFlagDirectives ?? []) {
|
|
147
|
-
|
|
147
|
+
if (flag.type === 'var' || value.type === 'var') throw new PrologError('instantiation_error');
|
|
148
|
+
if (flag.type !== 'atom') throw new PrologError('type_error(atom)', flag);
|
|
149
|
+
const definition = this.prologFlags.get(flag.name);
|
|
150
|
+
if (!definition) throw new PrologError('domain_error(prolog_flag)', flag);
|
|
151
|
+
if (value.type !== 'atom' || !definition.allowed.includes(value.name)) {
|
|
152
|
+
throw new PrologError('domain_error(flag_value)', compound('+', [flag, value]));
|
|
153
|
+
}
|
|
154
|
+
if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
|
|
155
|
+
definition.value = value;
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
if (!options.charConversions) {
|
|
@@ -198,35 +206,6 @@ export class Solver {
|
|
|
198
206
|
};
|
|
199
207
|
}
|
|
200
208
|
|
|
201
|
-
setPrologFlag(flagTerm, valueTerm, env = new Env()) {
|
|
202
|
-
const flag = deref(flagTerm, env);
|
|
203
|
-
const value = deref(valueTerm, env);
|
|
204
|
-
if (flag.type === VAR || value.type === VAR) throw new PrologError('instantiation_error');
|
|
205
|
-
if (flag.type !== ATOM) throw new PrologError('type_error(atom)', flag);
|
|
206
|
-
const definition = this.prologFlags.get(flag.name);
|
|
207
|
-
if (!definition) throw new PrologError('domain_error(prolog_flag)', flag);
|
|
208
|
-
|
|
209
|
-
if (definition.writeOptions === true) {
|
|
210
|
-
if (!termIsGround(value, env)) throw new PrologError('instantiation_error');
|
|
211
|
-
try {
|
|
212
|
-
parseTermWriteOptions(value, env, 'write_term', this);
|
|
213
|
-
} catch (error) {
|
|
214
|
-
if (error instanceof PrologError && error.formal === 'instantiation_error') throw error;
|
|
215
|
-
throw new PrologError('domain_error(flag_value)', compound('+', [flag, copyResolved(value, env)]));
|
|
216
|
-
}
|
|
217
|
-
if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
|
|
218
|
-
definition.value = copyResolved(value, env);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const expectedType = definition.valueType ?? ATOM;
|
|
223
|
-
if (value.type !== expectedType || !definition.allowed.includes(value.name)) {
|
|
224
|
-
throw new PrologError('domain_error(flag_value)', compound('+', [flag, value]));
|
|
225
|
-
}
|
|
226
|
-
if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
|
|
227
|
-
definition.value = atom(value.name);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
209
|
cloneForInnerGoal(solutionLimit = this.solutionLimit, options = {}) {
|
|
231
210
|
const solver = new Solver(this.program, {
|
|
232
211
|
registry: this.registry,
|
|
@@ -1093,12 +1072,6 @@ function defaultPrologFlags(unknown = 'error', strictIso = false) {
|
|
|
1093
1072
|
['unknown', { value: compound(unknown, []), allowed: ['error', 'fail', 'warning'], changeable: true }],
|
|
1094
1073
|
['double_quotes', { value: compound('chars', []), allowed: ['chars', 'codes', 'atom'], changeable: true }],
|
|
1095
1074
|
['occurs_check', { value: compound('true', []), allowed: ['true', 'error'], changeable: true }],
|
|
1096
|
-
['answer_write_options', {
|
|
1097
|
-
value: listFromItems([compound('spacing', [compound('standard', [])])]),
|
|
1098
|
-
allowed: [],
|
|
1099
|
-
changeable: true,
|
|
1100
|
-
writeOptions: true,
|
|
1101
|
-
}],
|
|
1102
1075
|
]);
|
|
1103
1076
|
if (strictIso) flags.delete('occurs_check');
|
|
1104
1077
|
return flags;
|
|
@@ -1522,12 +1495,29 @@ function* bundledMemberSolutions(solver, goal, env, state) {
|
|
|
1522
1495
|
|
|
1523
1496
|
function bundledMemberMayMatch(target, cursor, env) {
|
|
1524
1497
|
while (isCons(cursor)) {
|
|
1525
|
-
if (!
|
|
1498
|
+
if (!termsCannotUnify(target, cursor.args[0], env)) return true;
|
|
1526
1499
|
cursor = deref(cursor.args[1], env);
|
|
1527
1500
|
}
|
|
1528
1501
|
return false;
|
|
1529
1502
|
}
|
|
1530
1503
|
|
|
1504
|
+
function termsCannotUnify(left, right, env) {
|
|
1505
|
+
const pending = [[left, right]];
|
|
1506
|
+
while (pending.length > 0) {
|
|
1507
|
+
const [leftItem, rightItem] = pending.pop();
|
|
1508
|
+
const actualLeft = derefForLocal(leftItem, env);
|
|
1509
|
+
const actualRight = derefForLocal(rightItem, env);
|
|
1510
|
+
if (actualLeft.type === VAR || actualRight.type === VAR) continue;
|
|
1511
|
+
if (actualLeft.type !== actualRight.type || actualLeft.name !== actualRight.name ||
|
|
1512
|
+
actualLeft.arity !== actualRight.arity) return true;
|
|
1513
|
+
if (actualLeft.type !== COMPOUND) continue;
|
|
1514
|
+
for (let index = 0; index < actualLeft.arity; index++) {
|
|
1515
|
+
pending.push([actualLeft.args[index], actualRight.args[index]]);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1531
1521
|
function bundledEllipsisPlan(solver, group, goal, rest, env) {
|
|
1532
1522
|
if (solver.registry.eyePrologLibrary !== true ||
|
|
1533
1523
|
group.module !== 'iso_ext' || group.name !== '...' || group.arity !== 2 ||
|
|
@@ -1640,7 +1630,13 @@ function bundledLengthIterator(solver, group, goal, env) {
|
|
|
1640
1630
|
if (length.type === VAR && cursor.name === length.name) return null;
|
|
1641
1631
|
}
|
|
1642
1632
|
}
|
|
1643
|
-
|
|
1633
|
+
const iterator = bundledLengthSolutions(solver, goal, env);
|
|
1634
|
+
// A supplied length selects at most one list, and a closed supplied list has
|
|
1635
|
+
// exactly one length. Only an open list paired with an output variable can
|
|
1636
|
+
// produce another answer after the first one.
|
|
1637
|
+
const hasOpenTail = cursor.type === VAR;
|
|
1638
|
+
iterator.hasPendingAlternatives = () => length.type === VAR && hasOpenTail;
|
|
1639
|
+
return iterator;
|
|
1644
1640
|
}
|
|
1645
1641
|
|
|
1646
1642
|
function* bundledLengthSolutions(solver, goal, env) {
|
|
@@ -98,7 +98,7 @@ 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
100
|
`double_quotes/1` and `spacing/1` write options remain explicitly documented
|
|
101
|
-
EyeProlog extensions.
|
|
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,
|
|
@@ -120,11 +120,11 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
|
|
|
120
120
|
| 5.5.5 Directives | `module/2`, `use_module/1-2`, `meta_predicate/1` and normal-profile library behavior | Rejected as implementation-specific Part 1 directives. |
|
|
121
121
|
| 5.5.6 Side effects | Normal mode adds `statistics/0-2`, cleanup/library state, and optional proof/statistics host instrumentation. | The strict registry excludes Prolog-visible statistics/cleanup/library adapters; ordinary Part 1 I/O/database/flag/operator side effects remain. Host proof/statistics collection observes execution through the embedding API rather than adding a strict Prolog goal effect. **covered** — strict registry/execution regression. |
|
|
122
122
|
| 5.5.7 Control constructs | `tnot/1` and normal-profile execution optimizations | `tnot/1` is absent; automatic tabling/recursion guards are disabled. |
|
|
123
|
-
| 5.5.8 Flags | `occurs_check
|
|
123
|
+
| 5.5.8 Flags | `occurs_check` | Absent in strict mode. |
|
|
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` options `double_quotes(true|false)` and `spacing(standard
|
|
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
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
answer(7, red, caught(eyeprolog), first, caught(eyeprolog)).
|
|
2
2
|
answer(7, red, caught(eyeprolog), second, caught(eyeprolog)).
|
|
3
|
-
flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true)
|
|
3
|
+
flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true)]).
|
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
|
@@ -41,7 +41,6 @@ export function runIsoStrict(reporter = new TestReporter()) {
|
|
|
41
41
|
reporter.test('exposes only ISO core Prolog flags', () => {
|
|
42
42
|
const solver = new Solver(Program.parse('', { isoStrict: true }), { isoStrict: true });
|
|
43
43
|
equal(solver.prologFlags.has('occurs_check'), false, 'occurs_check');
|
|
44
|
-
equal(solver.prologFlags.has('answer_write_options'), false, 'answer_write_options');
|
|
45
44
|
equal(solver.prologFlags.get('unknown')?.value?.name, 'error', 'unknown default');
|
|
46
45
|
});
|
|
47
46
|
|
|
@@ -286,14 +285,14 @@ export function runIsoStrict(reporter = new TestReporter()) {
|
|
|
286
285
|
|
|
287
286
|
const strictSpacingError = capture(() => run('', {
|
|
288
287
|
isoStrict: true,
|
|
289
|
-
goal: 'write_term(1
|
|
288
|
+
goal: 'write_term(1+1,[spacing(minimal)])',
|
|
290
289
|
}));
|
|
291
290
|
equal(strictSpacingError.formal, 'domain_error(write_option)', 'strict spacing extension rejection');
|
|
292
291
|
|
|
293
292
|
const normal = run('', { goal: 'write_term("ab",[double_quotes(true)])' });
|
|
294
293
|
includes(normal.stdout, '"ab"', 'normal-profile extension remains available');
|
|
295
|
-
includes(run('', { goal: 'write_term(1
|
|
296
|
-
'1
|
|
294
|
+
includes(run('', { goal: 'write_term(1+1,[spacing(standard)])' }).stdout,
|
|
295
|
+
'1 + 1', 'normal-profile spacing extension remains available');
|
|
297
296
|
});
|
|
298
297
|
|
|
299
298
|
reporter.test('follows Corrigendum 3 variable metadata traversal and write naming', () => {
|
package/test/run-regression.mjs
CHANGED
|
@@ -186,7 +186,7 @@ function regressionCases() {
|
|
|
186
186
|
});
|
|
187
187
|
assertEqual(residuals.status, 0, 'dif/2 residual display status');
|
|
188
188
|
assertEqual(residuals.stdout,
|
|
189
|
-
'?- C = z, D = z, dif(A
|
|
189
|
+
'?- C = z, D = z, dif(A-z, B-z).\n' +
|
|
190
190
|
'?- A = [[]], B = [], dif([[]], [C]).\n' +
|
|
191
191
|
'?- X = Y.\n' +
|
|
192
192
|
'?- ',
|
|
@@ -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: () => {
|
|
@@ -1449,7 +1551,7 @@ c4 ?- call((!;1)).
|
|
|
1449
1551
|
input: 'read(T).\n!,*.\n.\nread(T).\na\n.\nhalt.\n',
|
|
1450
1552
|
});
|
|
1451
1553
|
assertEqual(continuedGraphic.status, 0, 'continued graphic operator REPL status');
|
|
1452
|
-
assertIncludes(continuedGraphic.stdout, 'T = (
|
|
1554
|
+
assertIncludes(continuedGraphic.stdout, 'T = (!,*.).', 'continued graphic operator answer');
|
|
1453
1555
|
assertIncludes(continuedGraphic.stdout, 'T = a.', 'read after continued graphic operator');
|
|
1454
1556
|
assertNotIncludes(continuedGraphic.stdout, 'syntax_error', 'continued graphic operator syntax');
|
|
1455
1557
|
assertEqual(continuedGraphic.stderr, '', 'continued graphic operator REPL stderr');
|
|
@@ -1956,6 +2058,50 @@ c4 ?- call((!;1)).
|
|
|
1956
2058
|
groupedSolutions.return();
|
|
1957
2059
|
},
|
|
1958
2060
|
},
|
|
2061
|
+
{
|
|
2062
|
+
name: 'bound member tails do not invent predicate choicepoints (issue #74 follow-up)',
|
|
2063
|
+
run: () => {
|
|
2064
|
+
const result = runCli([], {
|
|
2065
|
+
input:
|
|
2066
|
+
'use_module(library(lists)).\n' +
|
|
2067
|
+
'L=[i],append(L,_,[i|L]).\n' +
|
|
2068
|
+
'L=[_],append(L,_,[i|L]).\n' +
|
|
2069
|
+
'L=[i],append(L,_,[i|L]),member(a,[a|L]).\n' +
|
|
2070
|
+
'L=[_],append(L,_,[i|L]),member(a,[a|L]).\n' +
|
|
2071
|
+
'halt.\n',
|
|
2072
|
+
});
|
|
2073
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2074
|
+
assertEqual(result.stdout,
|
|
2075
|
+
'?- true.\n' +
|
|
2076
|
+
'?- L = "i".\n' +
|
|
2077
|
+
'?- L = "i".\n' +
|
|
2078
|
+
'?- L = "i".\n' +
|
|
2079
|
+
'?- L = "i".\n' +
|
|
2080
|
+
'?- ',
|
|
2081
|
+
'ground and formerly variable list elements have identical determinism');
|
|
2082
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2083
|
+
},
|
|
2084
|
+
},
|
|
2085
|
+
{
|
|
2086
|
+
name: 'length/2 reports fixed and closed-list answers as deterministic (issue #78)',
|
|
2087
|
+
run: () => {
|
|
2088
|
+
const result = runCli([], {
|
|
2089
|
+
input:
|
|
2090
|
+
'use_module(library(lists)).\n' +
|
|
2091
|
+
'length(K,0).\n' +
|
|
2092
|
+
'length([],N).\n' +
|
|
2093
|
+
'halt.\n',
|
|
2094
|
+
});
|
|
2095
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2096
|
+
assertEqual(result.stdout,
|
|
2097
|
+
'?- true.\n' +
|
|
2098
|
+
'?- K = [].\n' +
|
|
2099
|
+
'?- N = 0.\n' +
|
|
2100
|
+
'?- ',
|
|
2101
|
+
'no exhausted native length iterator remains');
|
|
2102
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2103
|
+
},
|
|
2104
|
+
},
|
|
1959
2105
|
{
|
|
1960
2106
|
name: 'REPL hides aliases to fresh throw variables while preserving query aliases',
|
|
1961
2107
|
run: () => {
|
|
@@ -2456,10 +2602,10 @@ c4 ?- call((!;1)).
|
|
|
2456
2602
|
});
|
|
2457
2603
|
assertEqual(result.status, 0, 'exit status');
|
|
2458
2604
|
assertEqual(result.stdout,
|
|
2459
|
-
'?- T = (a
|
|
2460
|
-
'?- U = (a,
|
|
2461
|
-
'?- V = (a
|
|
2462
|
-
'?- W = a
|
|
2605
|
+
'?- T = (a=b).\n' +
|
|
2606
|
+
'?- U = (a,b).\n' +
|
|
2607
|
+
'?- V = (a;b).\n' +
|
|
2608
|
+
'?- W = a+b.\n' +
|
|
2463
2609
|
'?- ',
|
|
2464
2610
|
'stdout');
|
|
2465
2611
|
assertEqual(result.stderr, '', 'stderr');
|
|
@@ -2665,9 +2811,9 @@ c4 ?- call((!;1)).
|
|
|
2665
2811
|
});
|
|
2666
2812
|
assertEqual(result.status, 0, 'exit status');
|
|
2667
2813
|
assertEqual(result.stdout,
|
|
2668
|
-
'?- |: X = (_A
|
|
2669
|
-
'?- |: X = (_A
|
|
2670
|
-
"?- |: X = (_A
|
|
2814
|
+
'?- |: X = (_A=a).\n' +
|
|
2815
|
+
'?- |: X = (_A=a).\n' +
|
|
2816
|
+
"?- |: X = (_A=pair(_A, _B)), Vs = [_A, _B], Names = ['X'=_A, 'Y'=_B], Singletons = ['Y'=_B].\n" +
|
|
2671
2817
|
'?- ',
|
|
2672
2818
|
'stdout');
|
|
2673
2819
|
assertEqual(result.stderr, '', 'stderr');
|
|
@@ -3882,67 +4028,45 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
3882
4028
|
},
|
|
3883
4029
|
},
|
|
3884
4030
|
{
|
|
3885
|
-
name: '
|
|
4031
|
+
name: 'REPL answers use only lexically required operator spacing (issue #76)',
|
|
3886
4032
|
run: () => {
|
|
3887
4033
|
const source = [
|
|
3888
4034
|
'emit :-',
|
|
3889
|
-
' write_term(1
|
|
3890
|
-
' write_term(1
|
|
3891
|
-
' write_term(1*1, [spacing(standard),spacing(minimal)]).',
|
|
4035
|
+
' write_term(1+1, [spacing(minimal)]), put_char(\'|\'),',
|
|
4036
|
+
' write_term(1+1, [spacing(standard)]).',
|
|
3892
4037
|
'',
|
|
3893
4038
|
].join('\n');
|
|
3894
|
-
assertEqual(run(source, { goal: 'emit' }).stdout, '1
|
|
3895
|
-
'
|
|
3896
|
-
assertEqual(run([
|
|
3897
|
-
':- set_prolog_flag(answer_write_options, [spacing(minimal)]).',
|
|
3898
|
-
'emit :- current_prolog_flag(answer_write_options, [spacing(minimal)]).',
|
|
3899
|
-
], { goal: 'emit' }).stdout, 'emit.\n', 'source flag directive');
|
|
4039
|
+
assertEqual(run(source, { goal: 'emit' }).stdout, '1+1|1 + 1emit.\n',
|
|
4040
|
+
'documented spacing values select write_term layout');
|
|
3900
4041
|
|
|
3901
4042
|
const result = runCli([], {
|
|
3902
|
-
input:
|
|
3903
|
-
'current_prolog_flag(answer_write_options, Os).\n' +
|
|
3904
|
-
'X=1*1.\n' +
|
|
3905
|
-
'set_prolog_flag(answer_write_options,[spacing(minimal)]).\n' +
|
|
3906
|
-
'use_module(library(lists)).\n' +
|
|
3907
|
-
'X=1*1.\n' +
|
|
3908
|
-
'current_prolog_flag(answer_write_options, Os).\n' +
|
|
3909
|
-
'halt.\n',
|
|
4043
|
+
input: 'X=1*1.\nX=a + -b.\nhalt.\n',
|
|
3910
4044
|
});
|
|
3911
4045
|
assertEqual(result.status, 0, 'exit status');
|
|
3912
4046
|
assertEqual(result.stdout,
|
|
3913
|
-
'?- Os = [spacing(standard)].\n' +
|
|
3914
|
-
'?- X = 1 * 1.\n' +
|
|
3915
|
-
'?- true.\n' +
|
|
3916
|
-
'?- true.\n' +
|
|
3917
4047
|
'?- X = 1*1.\n' +
|
|
3918
|
-
'?-
|
|
4048
|
+
'?- X = a+ -b.\n' +
|
|
3919
4049
|
'?- ',
|
|
3920
|
-
'
|
|
4050
|
+
'minimal operator layout preserves required token separation');
|
|
3921
4051
|
assertEqual(result.stderr, '', 'stderr');
|
|
3922
4052
|
|
|
3923
|
-
let
|
|
3924
|
-
try {
|
|
3925
|
-
run('', { goal: 'write_term(a,[spacing(other)])' });
|
|
3926
|
-
} catch (error) {
|
|
3927
|
-
writeOptionError = error;
|
|
3928
|
-
}
|
|
3929
|
-
assertEqual(writeOptionError?.formal, 'domain_error(write_option)', 'spacing value domain');
|
|
3930
|
-
|
|
3931
|
-
let flagValueError = null;
|
|
4053
|
+
let variableError = null;
|
|
3932
4054
|
try {
|
|
3933
|
-
run('', { goal: '
|
|
4055
|
+
run('', { goal: 'write_term(1+1,[spacing(X)])' });
|
|
3934
4056
|
} catch (error) {
|
|
3935
|
-
|
|
4057
|
+
variableError = error;
|
|
3936
4058
|
}
|
|
3937
|
-
assertEqual(
|
|
4059
|
+
assertEqual(variableError?.formal, 'instantiation_error', 'variable spacing value');
|
|
3938
4060
|
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4061
|
+
for (const value of ['true', 'false', '1', '0']) {
|
|
4062
|
+
let domainError = null;
|
|
4063
|
+
try {
|
|
4064
|
+
run('', { goal: `write_term(1+1,[spacing(${value})])` });
|
|
4065
|
+
} catch (error) {
|
|
4066
|
+
domainError = error;
|
|
4067
|
+
}
|
|
4068
|
+
assertEqual(domainError?.formal, 'domain_error(write_option)', `spacing(${value}) domain`);
|
|
3944
4069
|
}
|
|
3945
|
-
assertEqual(nongroundFlagError?.formal, 'instantiation_error', 'answer options must be ground');
|
|
3946
4070
|
},
|
|
3947
4071
|
},
|
|
3948
4072
|
{
|
|
@@ -5238,6 +5362,7 @@ answer(ok) :-
|
|
|
5238
5362
|
name: 'discarded recursive allocations recover after resource_error(memory)',
|
|
5239
5363
|
run: () => {
|
|
5240
5364
|
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
5365
|
+
const platformUrl = new URL('../src/platform.js', import.meta.url).href;
|
|
5241
5366
|
const programText = `
|
|
5242
5367
|
:- use_module(library(prologue)).
|
|
5243
5368
|
l([]).
|
|
@@ -5246,8 +5371,14 @@ answer(ok) :-
|
|
|
5246
5371
|
const reportedGoal = 'length(_,I),N is 2^I,\\+ \\+ (length(L,N),l(L)),L=[_|_]';
|
|
5247
5372
|
const script = `
|
|
5248
5373
|
import { Program, Solver, Env, deref, variable, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
5374
|
+
import { usedHeapSize } from ${JSON.stringify(platformUrl)};
|
|
5249
5375
|
const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
|
|
5250
|
-
|
|
5376
|
+
// Exercise EyeProlog's recovery window at a stable threshold instead
|
|
5377
|
+
// of relying on V8's version-dependent old-generation promotion.
|
|
5378
|
+
const solver = new Solver(program, {
|
|
5379
|
+
registry: getEyePrologRegistry(),
|
|
5380
|
+
maxMemoryBytes: usedHeapSize() + 20 * 1024 * 1024,
|
|
5381
|
+
});
|
|
5251
5382
|
const execute = (text) => {
|
|
5252
5383
|
const goal = parseGoalText(text, { operatorDefinitions: [...program.operators.values()] });
|
|
5253
5384
|
return [...solver.solve([goal], new Env(), 0)];
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5187,8 +5187,10 @@ pruned, top-level answer enumeration is abandoned, or an exception unwinds the
|
|
|
5187
5187
|
search. `setup_call_cleanup/3` runs Setup once and installs Cleanup only after
|
|
5188
5188
|
Setup succeeds. On cut or ordinary pruning Cleanup sees the current Goal
|
|
5189
5189
|
bindings; on exception unwind the Goal bindings have been removed and Cleanup
|
|
5190
|
-
sees the Setup environment.
|
|
5191
|
-
|
|
5190
|
+
sees the Setup environment. When a deterministic protected goal runs Cleanup
|
|
5191
|
+
before yielding, substitutions produced by a successful Cleanup are included in
|
|
5192
|
+
that answer. Cleanup failure is ignored, and an exception already being
|
|
5193
|
+
propagated takes precedence over a cleanup exception. Nested cleanups run
|
|
5192
5194
|
inside-out. These two controls are EyeProlog extensions and are absent from
|
|
5193
5195
|
`--iso-strict`.
|
|
5194
5196
|
|
|
@@ -5426,10 +5428,16 @@ still retained where adjacent graphic tokens would otherwise merge, as in
|
|
|
5426
5428
|
`write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
|
|
5427
5429
|
`variable_names/1`. Normal mode additionally accepts the EyeProlog extension
|
|
5428
5430
|
`double_quotes(true|false)`: `true` lets eligible character/code lists use the
|
|
5429
|
-
current `double_quotes` representation
|
|
5430
|
-
|
|
5431
|
-
`minimal`
|
|
5432
|
-
|
|
5431
|
+
current `double_quotes` representation, while strict ISO mode rejects this
|
|
5432
|
+
implementation-specific write option. Normal mode also accepts the
|
|
5433
|
+
implementation-specific `spacing(minimal|standard)` option: `minimal` emits
|
|
5434
|
+
only separators required to avoid lexical ambiguity, while `standard` adds
|
|
5435
|
+
conventional layout around operators. For example,
|
|
5436
|
+
`write_term(1+1,[spacing(minimal)])` emits `1+1` and
|
|
5437
|
+
`write_term(1+1,[spacing(standard)])` emits `1 + 1`. The REPL always follows
|
|
5438
|
+
the minimal-separator rule, so `X=1*1` is displayed as `X = 1*1`, while a
|
|
5439
|
+
separator is retained in `X = a+ -b` because the adjacent graphic tokens would
|
|
5440
|
+
otherwise merge. Strict ISO mode rejects both extension options.
|
|
5433
5441
|
|
|
5434
5442
|
Character operations are `get_char`, `peek_char`, `put_char`, `get_code`,
|
|
5435
5443
|
`peek_code`, and `put_code`; byte streams use the corresponding byte
|
|
@@ -6002,7 +6010,7 @@ silently changing a static program.
|
|
|
6002
6010
|
| `char_conversion(+Input,+Output)` | Installs a one-character conversion. In prepared Prolog text, later **unquoted** characters are converted when `char_conversion=on`; quoted characters are unchanged. The same mapping initializes execution-time term input. Mapping a character to itself removes its custom mapping. |
|
|
6003
6011
|
| `current_char_conversion(?Input,?Output)` | Enumerates installed nonidentity conversions. |
|
|
6004
6012
|
| `current_prolog_flag(?Flag,?Value)` | Enumerates flags or retrieves one named flag. An unknown bound flag raises *domain_error(prolog_flag)*. |
|
|
6005
|
-
| `set_prolog_flag(+Flag,+Value)` | Changes a supported mutable flag after validating its value. Read-only flags raise a permission error. |
|
|
6013
|
+
| `set_prolog_flag(+Flag,+Value)` | Changes a supported mutable flag after validating its allowed atom value. Read-only flags raise a permission error. |
|
|
6006
6014
|
|
|
6007
6015
|
| Flag | Default in normal EyeProlog | Allowed values | Mutable |
|
|
6008
6016
|
| --- | --- | --- | --- |
|
|
@@ -6016,7 +6024,6 @@ silently changing a static program.
|
|
|
6016
6024
|
| `unknown` | `error` | `error`, `fail`, `warning` | yes |
|
|
6017
6025
|
| `double_quotes` | `chars` | `chars`, `codes`, `atom` | yes |
|
|
6018
6026
|
| `occurs_check` | `true` | `true`, `error` | yes |
|
|
6019
|
-
| `answer_write_options` | `[spacing(standard)]` | A ground proper list of supported normal-mode `write_term/2-3` options | yes |
|
|
6020
6027
|
|
|
6021
6028
|
Because `bounded=false`, `current_prolog_flag(max_integer, _)` and
|
|
6022
6029
|
`current_prolog_flag(min_integer, _)` fail as required by ISO 7.11.1.1;
|
|
@@ -6051,24 +6058,6 @@ codes("ab"). % codes([97,98])
|
|
|
6051
6058
|
quoted_atom("ab"). % quoted_atom(ab)
|
|
6052
6059
|
```
|
|
6053
6060
|
|
|
6054
|
-
Normal mode also exposes the REPL's term-writing options through the mutable
|
|
6055
|
-
`answer_write_options` flag. Its explicit default explains the spaces in an
|
|
6056
|
-
answer such as `X = 1 * 1`; selecting minimal spacing is discoverable and
|
|
6057
|
-
reusable from Prolog:
|
|
6058
|
-
|
|
6059
|
-
```text
|
|
6060
|
-
?- current_prolog_flag(answer_write_options, Options).
|
|
6061
|
-
Options = [spacing(standard)].
|
|
6062
|
-
?- set_prolog_flag(answer_write_options, [spacing(minimal)]).
|
|
6063
|
-
true.
|
|
6064
|
-
?- X = 1*1.
|
|
6065
|
-
X = 1*1.
|
|
6066
|
-
```
|
|
6067
|
-
|
|
6068
|
-
The setting survives interactive `consult/1` and `use_module/1-2` program
|
|
6069
|
-
rebuilds. `answer_write_options` is a normal-mode host flag and is absent in
|
|
6070
|
-
strict ISO mode.
|
|
6071
|
-
|
|
6072
6061
|
### Atomic-term operations and conversions
|
|
6073
6062
|
|
|
6074
6063
|
| Predicate and principal call | Behavior |
|
|
@@ -6141,7 +6130,7 @@ the beginning. A peek does not mark the stream as past-end.
|
|
|
6141
6130
|
| `write(+Term)`, `write(+Stream,+Term)` | Writes readable operator notation without quoting atoms merely because quoting would be required for reparsing. Number variables are enabled. |
|
|
6142
6131
|
| `writeq(+Term)`, `writeq(+Stream,+Term)` | Like `write`, but quotes atoms when required for unambiguous input syntax. |
|
|
6143
6132
|
| `write_canonical(+Term)`, `write_canonical(+Stream,+Term)` | Writes quoted canonical functor notation while ignoring operators and without interpreting *$VAR/1*. |
|
|
6144
|
-
| `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(
|
|
6133
|
+
| `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)*. |
|
|
6145
6134
|
|
|
6146
6135
|
Term input uses the program's current operator table and the same ISO quoted-character
|
|
6147
6136
|
syntax as source text, including backslash-terminated octal and hexadecimal
|
|
@@ -6151,6 +6140,12 @@ when the `char_conversion` flag is `on`. `variable_names/1` and `singletons/1` o
|
|
|
6151
6140
|
predicates do not append a period or newline; call `write/1`, then `write('.')`
|
|
6152
6141
|
and `nl/0` when emitting a complete source term manually.
|
|
6153
6142
|
|
|
6143
|
+
A standalone numeric term read by `read/1-2` or `read_term/2-3` uses the same
|
|
6144
|
+
bounded numeric scanner and canonical value conversion as `number_chars/2`.
|
|
6145
|
+
Consequently every numeric character sequence accepted by `number_chars/2` has
|
|
6146
|
+
the same value when read as a full-stop-terminated term, without requiring EOF
|
|
6147
|
+
after that term.
|
|
6148
|
+
|
|
6154
6149
|
### Arithmetic expressions
|
|
6155
6150
|
|
|
6156
6151
|
| Predicate and principal call | Behavior |
|
|
@@ -6228,7 +6223,7 @@ so side effects occur in Prolog execution order.
|
|
|
6228
6223
|
|
|
6229
6224
|
### Normal-mode cleanup controls
|
|
6230
6225
|
|
|
6231
|
-
`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.
|
|
6226
|
+
`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.
|
|
6232
6227
|
|
|
6233
6228
|
### The EyeProlog library
|
|
6234
6229
|
|
|
@@ -6382,7 +6377,9 @@ conservative cross-engine subset.
|
|
|
6382
6377
|
`length(Xs, N)` enumerates `Xs = [], N = 0`, then one-element lists with
|
|
6383
6378
|
`N = 1`, and so on. Open-ended generation uses the normal memory guard with
|
|
6384
6379
|
recovery headroom so finite-heap exhaustion remains a catchable
|
|
6385
|
-
`resource_error(memory)`.
|
|
6380
|
+
`resource_error(memory)`. A supplied nonnegative length selects at most one
|
|
6381
|
+
answer, and a supplied closed list has exactly one length; these modes do not
|
|
6382
|
+
retain an exhausted choicepoint.
|
|
6386
6383
|
|
|
6387
6384
|
`library(iso_ext)` is a common interop module name, but only part of its
|
|
6388
6385
|
EyeProlog API belongs to the shared profile. `call_nth/2`, `time/1`, and
|
|
@@ -7786,8 +7783,8 @@ Notable implementation boundaries are:
|
|
|
7786
7783
|
- `write_term/2-3` implements the Part 1 plus Corrigendum 3 `quoted/1`,
|
|
7787
7784
|
`ignore_ops/1`, `numbervars/1`, and `variable_names/1` option surface,
|
|
7788
7785
|
including option validation and traversal rules; normal mode also offers
|
|
7789
|
-
`double_quotes(true|false)` and `spacing(standard
|
|
7790
|
-
|
|
7786
|
+
`double_quotes(true|false)` and `spacing(minimal|standard)` as explicitly
|
|
7787
|
+
implementation-specific extensions, which strict mode rejects;
|
|
7791
7788
|
- unification consistently performs an occurs check, rejecting rational-tree
|
|
7792
7789
|
bindings accepted as extensions by some systems.
|
|
7793
7790
|
|