eyeprolog 1.3.71 → 1.3.73
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/README.md +5 -2
- package/examples/dif-constraints.pl +2 -2
- package/package.json +1 -1
- package/src/atts.js +16 -5
- package/src/iso.js +74 -0
- package/src/lib/atts.pl +6 -1
- package/src/program.js +86 -36
- package/src/repl.js +7 -7
- package/src/solver.js +78 -13
- package/src/standard-library.js +4 -1
- package/src/term.js +120 -6
- package/test/run-regression.mjs +73 -6
- package/the-art-of-eyeprolog.md +1 -1
package/README.md
CHANGED
|
@@ -151,7 +151,9 @@ top level includes pending `dif/2` goals in residual answers. See
|
|
|
151
151
|
|
|
152
152
|
`library(atts)` exposes Scryer-style attributed-variable operations on the same
|
|
153
153
|
persistent `Env` machinery used by `dif/2`: `put_atts/2`, `get_atts/2`,
|
|
154
|
-
`put_attr/3`, `get_attr/3`, `del_attr/2`,
|
|
154
|
+
`put_attr/3`, `get_attr/3`, `del_attr/2`, `term_attributed_variables/2`, and
|
|
155
|
+
`call_residue_vars/2`. The latter reports variables whose residual attributes or
|
|
156
|
+
constraints were created or modified by a goal, including native `dif/2` residues.
|
|
155
157
|
`:- attribute ...` declarations are accepted, `verify_attributes/3` runs before
|
|
156
158
|
an attributed binding is committed, and the hook's returned goals run after
|
|
157
159
|
that binding. Attribute state follows variable representatives and ordinary
|
|
@@ -359,7 +361,8 @@ lambda syntax also changes the active operator table.
|
|
|
359
361
|
Outside `--iso-strict`, an otherwise undefined unqualified call may autoload a
|
|
360
362
|
predicate only when the interop profile has one canonical EyeProlog provider.
|
|
361
363
|
For example, `member/2` autoloads from `library(lists)`, `call_nth/2` from
|
|
362
|
-
`library(iso_ext)`, and `between/3`
|
|
364
|
+
`library(iso_ext)`, `call_residue_vars/2` from `library(atts)`, and `between/3`
|
|
365
|
+
from EyeProlog's internal
|
|
363
366
|
`library(prologue)` implementation. Use `--no-autoload` to disable this
|
|
364
367
|
convenience. Strict ISO mode never autoloads library predicates.
|
|
365
368
|
|
|
@@ -11,8 +11,8 @@ allowed_pair(X, Y) :-
|
|
|
11
11
|
X = left,
|
|
12
12
|
Y = right.
|
|
13
13
|
|
|
14
|
-
%
|
|
15
|
-
%
|
|
14
|
+
% X=Y already proves X-Y and 1-2 different: X-X cannot unify with 1-2,
|
|
15
|
+
% so the dif/2 constraint is discharged before the final specialization Y=1.
|
|
16
16
|
specialization(X, Y) :-
|
|
17
17
|
dif(X-Y, 1-2),
|
|
18
18
|
X = Y,
|
package/package.json
CHANGED
package/src/atts.js
CHANGED
|
@@ -69,7 +69,8 @@ function* getAttsBuiltin({ goal, env }) {
|
|
|
69
69
|
if (pattern.type === VAR) {
|
|
70
70
|
if (attributes.length === 0) return;
|
|
71
71
|
const next = env.clone();
|
|
72
|
-
if (unify(goal.args[1], listFromItems(attributes), next
|
|
72
|
+
if (unify(goal.args[1], listFromItems(attributes), next,
|
|
73
|
+
{ knownNonoccurringVariables: goal._firstUseVariables })) yield next;
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -89,7 +90,10 @@ function* getAttsBuiltin({ goal, env }) {
|
|
|
89
90
|
}
|
|
90
91
|
if (stored == null) return;
|
|
91
92
|
const next = env.clone();
|
|
92
|
-
if (unify(inner, stored, next, {
|
|
93
|
+
if (unify(inner, stored, next, {
|
|
94
|
+
skipVariableConstraints: true,
|
|
95
|
+
knownNonoccurringVariables: goal._firstUseVariables,
|
|
96
|
+
})) yield next;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
function attributeName(term, env) {
|
|
@@ -117,7 +121,10 @@ function* getAttrBuiltin({ goal, env }) {
|
|
|
117
121
|
const stored = env.getPrologAttribute(variableTerm.name, owner, name, 1);
|
|
118
122
|
if (stored == null) return;
|
|
119
123
|
const next = env.clone();
|
|
120
|
-
if (unify(goal.args[2], stored.args[0], next, {
|
|
124
|
+
if (unify(goal.args[2], stored.args[0], next, {
|
|
125
|
+
skipVariableConstraints: true,
|
|
126
|
+
knownNonoccurringVariables: goal._firstUseVariables,
|
|
127
|
+
})) yield next;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
function* delAttrBuiltin({ goal, env }) {
|
|
@@ -163,7 +170,8 @@ function* getAttrListBuiltin({ goal, env }) {
|
|
|
163
170
|
if (entries.length === 0) return;
|
|
164
171
|
const encoded = entries.map(({ module, attribute }) => compound(':', [atom(module), attribute]));
|
|
165
172
|
const next = env.clone();
|
|
166
|
-
if (unify(goal.args[1], listFromItems(encoded), next
|
|
173
|
+
if (unify(goal.args[1], listFromItems(encoded), next,
|
|
174
|
+
{ knownNonoccurringVariables: goal._firstUseVariables })) yield next;
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
function* getFromAttrListBuiltin({ goal, env }) {
|
|
@@ -179,7 +187,10 @@ function* getFromAttrListBuiltin({ goal, env }) {
|
|
|
179
187
|
const stored = env.getPrologAttribute(variableTerm.name, moduleTerm.name, signature.name, signature.arity);
|
|
180
188
|
if (stored == null) return;
|
|
181
189
|
const next = env.clone();
|
|
182
|
-
if (unify(pattern, stored, next, {
|
|
190
|
+
if (unify(pattern, stored, next, {
|
|
191
|
+
skipVariableConstraints: true,
|
|
192
|
+
knownNonoccurringVariables: goal._firstUseVariables,
|
|
193
|
+
})) yield next;
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
function* putToAttrListBuiltin({ goal, env }) {
|
package/src/iso.js
CHANGED
|
@@ -165,6 +165,7 @@ export const isoBuiltins = {
|
|
|
165
165
|
export const eyePrologLibraryBuiltins = {
|
|
166
166
|
register(registry) {
|
|
167
167
|
registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
|
|
168
|
+
registry.add('eyeprolog__call_residue_vars', 2, callResidueVarsBuiltin, { eyePrologLibrary: true });
|
|
168
169
|
registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
|
|
169
170
|
registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
|
|
170
171
|
registry.add('dif', 2, difBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
@@ -2497,6 +2498,79 @@ function* callNthBuiltin({ solver, goal, env }) {
|
|
|
2497
2498
|
}
|
|
2498
2499
|
}
|
|
2499
2500
|
|
|
2501
|
+
function residueSnapshot(env) {
|
|
2502
|
+
const attributes = new Map();
|
|
2503
|
+
for (const name of env.attributedVariableNames?.() ?? []) {
|
|
2504
|
+
attributes.set(name, env.prologAttributes(name));
|
|
2505
|
+
}
|
|
2506
|
+
const delays = new Map();
|
|
2507
|
+
for (const name of env.delayedVariableNames?.() ?? []) {
|
|
2508
|
+
delays.set(name, env.delayedGoals(name));
|
|
2509
|
+
}
|
|
2510
|
+
const constraints = new Map();
|
|
2511
|
+
for (const constraint of env.variableConstraints?.() ?? []) {
|
|
2512
|
+
constraints.set(constraint, [...constraint.variables(env)]);
|
|
2513
|
+
}
|
|
2514
|
+
return { attributes, delays, constraints };
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
function sameAttributeEntries(before, after) {
|
|
2518
|
+
if (before == null || before.length !== after.length) return false;
|
|
2519
|
+
for (let i = 0; i < after.length; i++) {
|
|
2520
|
+
if (before[i].module !== after[i].module || before[i].attribute !== after[i].attribute) return false;
|
|
2521
|
+
}
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
function sameDelayEntries(before, after) {
|
|
2526
|
+
if (before == null || before.length !== after.length) return false;
|
|
2527
|
+
for (let i = 0; i < after.length; i++) if (before[i] !== after[i]) return false;
|
|
2528
|
+
return true;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function residueVariablesSince(snapshot, env) {
|
|
2532
|
+
const names = [];
|
|
2533
|
+
const seen = new Set();
|
|
2534
|
+
const add = (name) => {
|
|
2535
|
+
const root = deref(variable(name), env);
|
|
2536
|
+
if (root.type !== VAR || seen.has(root.name)) return;
|
|
2537
|
+
seen.add(root.name);
|
|
2538
|
+
names.push(root);
|
|
2539
|
+
};
|
|
2540
|
+
|
|
2541
|
+
for (const name of env.attributedVariableNames?.() ?? []) {
|
|
2542
|
+
const after = env.prologAttributes(name);
|
|
2543
|
+
if (!sameAttributeEntries(snapshot.attributes.get(name), after)) add(name);
|
|
2544
|
+
}
|
|
2545
|
+
for (const name of env.delayedVariableNames?.() ?? []) {
|
|
2546
|
+
const after = env.delayedGoals(name);
|
|
2547
|
+
if (!sameDelayEntries(snapshot.delays.get(name), after)) add(name);
|
|
2548
|
+
}
|
|
2549
|
+
for (const constraint of env.variableConstraints?.() ?? []) {
|
|
2550
|
+
const after = [...constraint.variables(env)];
|
|
2551
|
+
const before = snapshot.constraints.get(constraint);
|
|
2552
|
+
const unchanged = before != null && before.length === after.length && before.every((name, index) => name === after[index]);
|
|
2553
|
+
if (unchanged) continue;
|
|
2554
|
+
for (const name of after) add(name);
|
|
2555
|
+
}
|
|
2556
|
+
return names;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
function* callResidueVarsBuiltin({ solver, goal, env }) {
|
|
2560
|
+
const invoked = callable(goal.args[0], env);
|
|
2561
|
+
if (invoked.module == null) invoked.module = goal.module ?? 'user';
|
|
2562
|
+
const snapshot = residueSnapshot(env);
|
|
2563
|
+
const child = solver.cloneForInnerGoal();
|
|
2564
|
+
try {
|
|
2565
|
+
for (const answerEnv of child.solve([invoked], env, 0)) {
|
|
2566
|
+
const next = answerEnv.clone();
|
|
2567
|
+
if (unify(goal.args[1], listFromItems(residueVariablesSince(snapshot, answerEnv)), next)) yield next;
|
|
2568
|
+
}
|
|
2569
|
+
} finally {
|
|
2570
|
+
solver.absorbStatsFrom(child);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2500
2574
|
function* freezeBuiltin({ solver, goal, env }) {
|
|
2501
2575
|
const watched = deref(goal.args[0], env);
|
|
2502
2576
|
if (watched.type !== VAR) {
|
package/src/lib/atts.pl
CHANGED
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
put_attr/3,
|
|
13
13
|
get_attr/3,
|
|
14
14
|
del_attr/2,
|
|
15
|
-
term_attributed_variables/2
|
|
15
|
+
term_attributed_variables/2,
|
|
16
|
+
call_residue_vars/2
|
|
16
17
|
]).
|
|
17
18
|
:- op(1199, fx, attribute).
|
|
19
|
+
:- meta_predicate(call_residue_vars(0, '?')).
|
|
18
20
|
|
|
19
21
|
term_attributed_variables(Term, Variables) :-
|
|
20
22
|
'$term_attributed_variables'(Term, Variables).
|
|
23
|
+
|
|
24
|
+
call_residue_vars(Goal, Variables) :-
|
|
25
|
+
eyeprolog__call_residue_vars(Goal, Variables).
|
package/src/program.js
CHANGED
|
@@ -34,6 +34,28 @@ import {
|
|
|
34
34
|
const DEFER_PROGRAM_BUILD = Symbol('deferProgramBuild');
|
|
35
35
|
const FAST_PARSE_ABORT = Symbol('fastParseAbort');
|
|
36
36
|
const PROGRAM_BUILD_BATCH_SIZE = 16384;
|
|
37
|
+
const preparedBundledLibraryCache = new Map();
|
|
38
|
+
|
|
39
|
+
function preparedBundledLibraryCacheKey(program, options) {
|
|
40
|
+
const filename = String(options.filename ?? '');
|
|
41
|
+
if (filename !== standardLibrarySources.get('clpz')?.filename) return null;
|
|
42
|
+
// A user hook deliberately defined before use_module/1 is allowed to
|
|
43
|
+
// transform bundled source. Such a program needs its own ordinary expansion.
|
|
44
|
+
if (program.groups.has(modulePredicateKey('user', 'term_expansion', 2)) ||
|
|
45
|
+
program.groups.has(modulePredicateKey('user', 'goal_expansion', 2))) return null;
|
|
46
|
+
return `${filename}\u0000${options.sourceMetadata === false ? 'compact' : 'metadata'}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cachedClauseCopy(clause) {
|
|
50
|
+
const copy = { ...clause };
|
|
51
|
+
if (Array.isArray(clause.body)) copy.body = [...clause.body];
|
|
52
|
+
if (clause.source != null) copy.source = { ...clause.source };
|
|
53
|
+
delete copy.index;
|
|
54
|
+
delete copy.groundHead;
|
|
55
|
+
delete copy.scalarHead;
|
|
56
|
+
delete copy._localFreshPlan;
|
|
57
|
+
return copy;
|
|
58
|
+
}
|
|
37
59
|
|
|
38
60
|
function termContainsStringExtension(term, seen = new Set()) {
|
|
39
61
|
if (term == null || typeof term !== 'object' || seen.has(term)) return false;
|
|
@@ -1074,6 +1096,8 @@ function sourcePath(options) {
|
|
|
1074
1096
|
function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules, fast, context) {
|
|
1075
1097
|
const batch = [];
|
|
1076
1098
|
const deferredGoalExpansions = [];
|
|
1099
|
+
const preparedCacheKey = preparedBundledLibraryCacheKey(builder.program, options);
|
|
1100
|
+
const preparedForCache = preparedCacheKey == null ? null : [];
|
|
1077
1101
|
const flush = () => {
|
|
1078
1102
|
if (batch.length === 0) return;
|
|
1079
1103
|
builder.addClauses(batch);
|
|
@@ -1106,41 +1130,12 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1106
1130
|
processPreparedClause(clause);
|
|
1107
1131
|
};
|
|
1108
1132
|
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
// Grammar-rule expansion belongs to ISO/IEC TS 13211-3 rather than the
|
|
1114
|
-
// Part 1 strict-core language. In strict core mode -->/2 remains the
|
|
1115
|
-
// ordinary predefined operator term from Table 7 and is not rewritten.
|
|
1116
|
-
const grammarClause = builder.program.strictIso ? null : expandDcgRuleClause(clause, lexicalModule);
|
|
1117
|
-
if (grammarClause) clause = grammarClause;
|
|
1118
|
-
|
|
1119
|
-
// A source module qualifier on a clause head selects the procedure owner,
|
|
1120
|
-
// while unqualified calls in the body retain the lexical module of the
|
|
1121
|
-
// source text. This distinction is essential for conventional hooks such
|
|
1122
|
-
// as user:term_expansion/2 defined from library modules.
|
|
1123
|
-
clause = normalizeQualifiedClauseHead(clause, lexicalModule);
|
|
1124
|
-
|
|
1125
|
-
if (!builder.program.strictIso && !isDirectiveClause(clause)) {
|
|
1126
|
-
try {
|
|
1127
|
-
expandClauseGoals(builder.program, clause, lexicalModule, {
|
|
1128
|
-
module: lexicalModule,
|
|
1129
|
-
filename: options.filename ?? '<input>',
|
|
1130
|
-
});
|
|
1131
|
-
} catch (error) {
|
|
1132
|
-
// Scryer-compatible goal-expansion hooks may refer to helper predicates
|
|
1133
|
-
// that occur later in the same source file. Keep loading so those
|
|
1134
|
-
// helpers become visible, then retry this clause at end-of-source.
|
|
1135
|
-
// A still-missing helper (or any other expansion error) is reported on
|
|
1136
|
-
// the final retry rather than being silently ignored.
|
|
1137
|
-
if (!(error instanceof PrologError) || error.formal !== 'existence_error(procedure)') throw error;
|
|
1138
|
-
clause.module ??= lexicalModule;
|
|
1139
|
-
clause.textUnit ??= context.textUnit;
|
|
1140
|
-
deferredGoalExpansions.push({ clause, lexicalModule });
|
|
1141
|
-
return;
|
|
1133
|
+
const acceptPreparedClause = (clause, lexicalModule, record = true) => {
|
|
1134
|
+
const remember = () => {
|
|
1135
|
+
if (record && preparedForCache != null) {
|
|
1136
|
+
preparedForCache.push({ clause: cachedClauseCopy(clause), lexicalModule });
|
|
1142
1137
|
}
|
|
1143
|
-
}
|
|
1138
|
+
};
|
|
1144
1139
|
|
|
1145
1140
|
const moduleDeclaration = moduleDirective(clause);
|
|
1146
1141
|
if (moduleDeclaration) {
|
|
@@ -1148,6 +1143,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1148
1143
|
clause.module = moduleDeclaration.name;
|
|
1149
1144
|
clause.textUnit = context.textUnit;
|
|
1150
1145
|
clause.moduleFilename = options.filename ?? '<input>';
|
|
1146
|
+
remember();
|
|
1151
1147
|
builder.addClauses([clause]);
|
|
1152
1148
|
context.module = moduleDeclaration.name;
|
|
1153
1149
|
loadedModules.add(moduleDeclaration.name);
|
|
@@ -1158,6 +1154,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1158
1154
|
flush();
|
|
1159
1155
|
clause.module = context.module;
|
|
1160
1156
|
clause.textUnit = context.textUnit;
|
|
1157
|
+
remember();
|
|
1161
1158
|
builder.addClauses([clause]);
|
|
1162
1159
|
const libraryName = libraryDesignationName(use.designation);
|
|
1163
1160
|
if (libraryName != null) {
|
|
@@ -1185,6 +1182,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1185
1182
|
const bodyModule = clause.lexicalModule ?? lexicalModule;
|
|
1186
1183
|
for (const goal of clause.body) annotateGoalModule(goal, bodyModule);
|
|
1187
1184
|
}
|
|
1185
|
+
remember();
|
|
1188
1186
|
const isExpansionHook = !isDirectiveClause(clause) &&
|
|
1189
1187
|
clause.head?.arity === 2 && ['term_expansion', 'goal_expansion'].includes(clause.head?.name);
|
|
1190
1188
|
if (isExpansionHook) {
|
|
@@ -1199,6 +1197,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1199
1197
|
return;
|
|
1200
1198
|
}
|
|
1201
1199
|
flush();
|
|
1200
|
+
remember();
|
|
1202
1201
|
if (builder.program.strictIso && include.name === 'ensure_loaded') {
|
|
1203
1202
|
builder.lastPredicateByText.set(context.textUnit ?? '<input>', '@directive');
|
|
1204
1203
|
}
|
|
@@ -1212,6 +1211,54 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1212
1211
|
}
|
|
1213
1212
|
};
|
|
1214
1213
|
|
|
1214
|
+
const processPreparedClause = (inputClause) => {
|
|
1215
|
+
let clause = inputClause;
|
|
1216
|
+
const lexicalModule = context.module;
|
|
1217
|
+
|
|
1218
|
+
// Grammar-rule expansion belongs to ISO/IEC TS 13211-3 rather than the
|
|
1219
|
+
// Part 1 strict-core language. In strict core mode -->/2 remains the
|
|
1220
|
+
// ordinary predefined operator term from Table 7 and is not rewritten.
|
|
1221
|
+
const grammarClause = builder.program.strictIso ? null : expandDcgRuleClause(clause, lexicalModule);
|
|
1222
|
+
if (grammarClause) clause = grammarClause;
|
|
1223
|
+
|
|
1224
|
+
// A source module qualifier on a clause head selects the procedure owner,
|
|
1225
|
+
// while unqualified calls in the body retain the lexical module of the
|
|
1226
|
+
// source text. This distinction is essential for conventional hooks such
|
|
1227
|
+
// as user:term_expansion/2 defined from library modules.
|
|
1228
|
+
clause = normalizeQualifiedClauseHead(clause, lexicalModule);
|
|
1229
|
+
|
|
1230
|
+
if (!builder.program.strictIso && !isDirectiveClause(clause)) {
|
|
1231
|
+
try {
|
|
1232
|
+
expandClauseGoals(builder.program, clause, lexicalModule, {
|
|
1233
|
+
module: lexicalModule,
|
|
1234
|
+
filename: options.filename ?? '<input>',
|
|
1235
|
+
});
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
// Scryer-compatible goal-expansion hooks may refer to helper predicates
|
|
1238
|
+
// that occur later in the same source file. Keep loading so those
|
|
1239
|
+
// helpers become visible, then retry this clause at end-of-source.
|
|
1240
|
+
// A still-missing helper (or any other expansion error) is reported on
|
|
1241
|
+
// the final retry rather than being silently ignored.
|
|
1242
|
+
if (!(error instanceof PrologError) || error.formal !== 'existence_error(procedure)') throw error;
|
|
1243
|
+
clause.module ??= lexicalModule;
|
|
1244
|
+
clause.textUnit ??= context.textUnit;
|
|
1245
|
+
deferredGoalExpansions.push({ clause, lexicalModule });
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
acceptPreparedClause(clause, lexicalModule);
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
const cachedEntry = preparedCacheKey == null ? null : preparedBundledLibraryCache.get(preparedCacheKey);
|
|
1254
|
+
if (cachedEntry?.source === source) {
|
|
1255
|
+
for (const item of cachedEntry.clauses) {
|
|
1256
|
+
acceptPreparedClause(cachedClauseCopy(item.clause), item.lexicalModule, false);
|
|
1257
|
+
}
|
|
1258
|
+
flush();
|
|
1259
|
+
return true;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1215
1262
|
if (fast) {
|
|
1216
1263
|
const acceptBinary = (headName, head0Type, head0Name, head1Type, head1Name,
|
|
1217
1264
|
bodyName, body0Type, body0Name, body1Type, body1Name) => {
|
|
@@ -1235,9 +1282,12 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1235
1282
|
module: lexicalModule,
|
|
1236
1283
|
filename: options.filename ?? '<input>',
|
|
1237
1284
|
});
|
|
1238
|
-
|
|
1239
|
-
builder.addClauses([clause]);
|
|
1285
|
+
acceptPreparedClause(clause, lexicalModule);
|
|
1240
1286
|
}
|
|
1287
|
+
flush();
|
|
1288
|
+
}
|
|
1289
|
+
if (preparedCacheKey != null) {
|
|
1290
|
+
preparedBundledLibraryCache.set(preparedCacheKey, { source, clauses: preparedForCache });
|
|
1241
1291
|
}
|
|
1242
1292
|
return true;
|
|
1243
1293
|
}
|
package/src/repl.js
CHANGED
|
@@ -672,18 +672,18 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
672
672
|
}
|
|
673
673
|
|
|
674
674
|
function pullSolution(solver, solutions, reader) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
675
|
+
// Prolog output is an observable side effect of execution, so never hold it
|
|
676
|
+
// until the search reaches its next leaf. In particular an infinite search
|
|
677
|
+
// that periodically writes progress must remain observable at the terminal.
|
|
678
|
+
// Completed queries keep the same byte order because user_output writes are
|
|
679
|
+
// synchronous and occur before solutions.next() returns its answer.
|
|
679
680
|
const suspended = reader.suspendForComputation();
|
|
680
681
|
try {
|
|
681
|
-
return { result: solutions.next(), output:
|
|
682
|
+
return { result: solutions.next(), output: '' };
|
|
682
683
|
} catch (error) {
|
|
683
|
-
return { error, output:
|
|
684
|
+
return { error, output: '' };
|
|
684
685
|
} finally {
|
|
685
686
|
reader.resumeAfterComputation(suspended);
|
|
686
|
-
if (stream) stream.write = originalWrite;
|
|
687
687
|
}
|
|
688
688
|
}
|
|
689
689
|
|
package/src/solver.js
CHANGED
|
@@ -1269,22 +1269,80 @@ function clauseLocalFreshPlan(clause) {
|
|
|
1269
1269
|
|
|
1270
1270
|
function freshVariableSet(names, freshVariables) {
|
|
1271
1271
|
if (names.length === 0) return null;
|
|
1272
|
-
const fresh =
|
|
1272
|
+
const fresh = [];
|
|
1273
1273
|
for (const name of names) {
|
|
1274
1274
|
const term = freshVariables.get(name);
|
|
1275
|
-
if (term != null) fresh.
|
|
1275
|
+
if (term != null) fresh.push(term.name);
|
|
1276
1276
|
}
|
|
1277
|
-
|
|
1277
|
+
if (fresh.length === 0) return null;
|
|
1278
|
+
// Most clauses introduce only one or two first-use variables. A tiny linear
|
|
1279
|
+
// membership object avoids allocating and populating an OrderedHashSet on
|
|
1280
|
+
// every invocation; use the native Set once linear lookup would lose.
|
|
1281
|
+
return fresh.length <= 4 ? new SmallFreshVariableSet(fresh) : new Set(fresh);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
class SmallFreshVariableSet {
|
|
1285
|
+
constructor(values) {
|
|
1286
|
+
this.values = values;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
has(value) {
|
|
1290
|
+
for (let index = 0; index < this.values.length; index++) {
|
|
1291
|
+
if (this.values[index] === value) return true;
|
|
1292
|
+
}
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function goalUsesFirstUseProof(term) {
|
|
1298
|
+
if (term?.type !== COMPOUND) return false;
|
|
1299
|
+
if ((term.name === '=' && term.arity === 2)
|
|
1300
|
+
|| (term.name === 'get_atts' && term.arity === 2)
|
|
1301
|
+
|| (term.name === 'get_attr' && term.arity === 3)
|
|
1302
|
+
|| (term.name === '$get_attr_list' && term.arity === 2)
|
|
1303
|
+
|| (term.name === '$get_from_attr_list' && term.arity === 3)) return true;
|
|
1304
|
+
if ((term.name === ',' || term.name === ';' || term.name === '->') && term.arity === 2) {
|
|
1305
|
+
return goalUsesFirstUseProof(term.args[0]) || goalUsesFirstUseProof(term.args[1]);
|
|
1306
|
+
}
|
|
1307
|
+
if (term.name === '\\+' && term.arity === 1) return goalUsesFirstUseProof(term.args[0]);
|
|
1308
|
+
if (term.name === ':' && term.arity === 2) return goalUsesFirstUseProof(term.args[1]);
|
|
1309
|
+
return false;
|
|
1278
1310
|
}
|
|
1279
1311
|
|
|
1280
1312
|
function attachBodyLocalFreshVariables(freshBody, plan, freshVariables) {
|
|
1313
|
+
const attach = (term, knownNonoccurringVariables) => {
|
|
1314
|
+
if (term?.type !== COMPOUND) return;
|
|
1315
|
+
if (term.name === '=' && term.arity === 2) {
|
|
1316
|
+
term._knownNonoccurringVariables = knownNonoccurringVariables;
|
|
1317
|
+
} else if ((term.name === 'get_atts' && term.arity === 2)
|
|
1318
|
+
|| (term.name === 'get_attr' && term.arity === 3)
|
|
1319
|
+
|| (term.name === '$get_attr_list' && term.arity === 2)
|
|
1320
|
+
|| (term.name === '$get_from_attr_list' && term.arity === 3)) {
|
|
1321
|
+
term._firstUseVariables = knownNonoccurringVariables;
|
|
1322
|
+
}
|
|
1323
|
+
// Goal expansion commonly wraps a primitive in a conjunction or
|
|
1324
|
+
// if-then-else. Preserve the proof on the executable child; attaching it
|
|
1325
|
+
// only to the outer control term leaves get_atts/2 unable to use it.
|
|
1326
|
+
if ((term.name === ',' || term.name === ';' || term.name === '->') && term.arity === 2) {
|
|
1327
|
+
attach(term.args[0], knownNonoccurringVariables);
|
|
1328
|
+
attach(term.args[1], knownNonoccurringVariables);
|
|
1329
|
+
} else if (term.name === '\\+' && term.arity === 1) {
|
|
1330
|
+
attach(term.args[0], knownNonoccurringVariables);
|
|
1331
|
+
} else if (term.name === ':' && term.arity === 2) {
|
|
1332
|
+
attach(term.args[1], knownNonoccurringVariables);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1281
1335
|
for (let index = 0; index < freshBody.length; index++) {
|
|
1282
1336
|
const goal = freshBody[index];
|
|
1337
|
+
if (!goalUsesFirstUseProof(goal)) continue;
|
|
1283
1338
|
const knownNonoccurringVariables = freshVariableSet(plan[index] ?? [], freshVariables);
|
|
1284
1339
|
if (knownNonoccurringVariables == null || goal?.type !== COMPOUND) continue;
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1340
|
+
// Host builtins that only unify an output with already-existing logical
|
|
1341
|
+
// data can use the same first-use proof without treating the variable as a
|
|
1342
|
+
// WAM-style local across the whole call. This is especially important for
|
|
1343
|
+
// get_atts/2: CLP(Z) attributes are large trees that would otherwise be
|
|
1344
|
+
// traversed once per freshly introduced pattern variable.
|
|
1345
|
+
attach(goal, knownNonoccurringVariables);
|
|
1288
1346
|
}
|
|
1289
1347
|
}
|
|
1290
1348
|
|
|
@@ -2376,10 +2434,14 @@ function tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, dep
|
|
|
2376
2434
|
// A fully-ground call to a single flat rule can be checked without freshening
|
|
2377
2435
|
// the rule or allocating an Env for every body literal. This is especially
|
|
2378
2436
|
// valuable for data-validation joins such as rb(A,B,C,D,E) in ORB Join2.
|
|
2379
|
-
|
|
2437
|
+
// Reject ineligible rule shapes before recursively resolving the goal. Most
|
|
2438
|
+
// library predicates have several clauses, and CLP(Z) calls are commonly
|
|
2439
|
+
// large non-ground structures for which that groundness walk is pure cost.
|
|
2440
|
+
if (group.clauses.length !== 1) return false;
|
|
2380
2441
|
const clause = group.clauses[0];
|
|
2381
2442
|
if (clause?.compactBinary === true || clause.body.length < 2 || clause.head.type !== COMPOUND) return false;
|
|
2382
2443
|
if (clause.head.name !== goal.name || clause.head.arity !== goal.arity) return false;
|
|
2444
|
+
if (!termIsGround(goal, env)) return false;
|
|
2383
2445
|
|
|
2384
2446
|
const bindings = new Map();
|
|
2385
2447
|
const resolvedGoal = copyResolved(goal, env);
|
|
@@ -2438,14 +2500,17 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
|
|
|
2438
2500
|
// This is a search-control optimization only. It fires only while each step
|
|
2439
2501
|
// has exactly one matching clause and a single ground body goal; otherwise the
|
|
2440
2502
|
// normal clause path below remains authoritative.
|
|
2441
|
-
if (!termIsGround(goal, env)) return false;
|
|
2442
2503
|
// The chain matcher below only propagates variables bound to flat scalar
|
|
2443
2504
|
// head arguments (or literal scalars). Keep this optimization on that flat
|
|
2444
|
-
// domain
|
|
2445
|
-
//
|
|
2446
|
-
//
|
|
2447
|
-
const resolvedArgs = goal.
|
|
2448
|
-
|
|
2505
|
+
// domain. Resolving the top-level arguments first proves groundness when all
|
|
2506
|
+
// of them are scalars and avoids recursively walking arbitrary CLP(Z) terms
|
|
2507
|
+
// only to reject them immediately afterward.
|
|
2508
|
+
const resolvedArgs = new Array(goal.arity);
|
|
2509
|
+
for (let index = 0; index < goal.arity; index++) {
|
|
2510
|
+
const resolved = derefForLocal(goal.args[index], env);
|
|
2511
|
+
if (!isScalarTerm(resolved)) return false;
|
|
2512
|
+
resolvedArgs[index] = resolved;
|
|
2513
|
+
}
|
|
2449
2514
|
|
|
2450
2515
|
const baseEnv = env;
|
|
2451
2516
|
let currentGroup = group;
|
package/src/standard-library.js
CHANGED
|
@@ -63,7 +63,7 @@ function libraryUrl(filename) {
|
|
|
63
63
|
|
|
64
64
|
export const eyePrologNativeLibraryIndicators = Object.freeze([
|
|
65
65
|
'call_nth/2', 'freeze/2', 'dif/2', 'countall/2', 'time/1',
|
|
66
|
-
'put_atts/2', 'get_atts/2', 'put_attr/3', 'get_attr/3', 'del_attr/2', 'term_attributed_variables/2',
|
|
66
|
+
'put_atts/2', 'get_atts/2', 'put_attr/3', 'get_attr/3', 'del_attr/2', 'term_attributed_variables/2', 'call_residue_vars/2',
|
|
67
67
|
'#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
|
|
68
68
|
'#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
|
|
69
69
|
'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
|
|
@@ -133,6 +133,9 @@ export const eyePrologInteropAutoload = Object.freeze({
|
|
|
133
133
|
// it from library(iso_ext), matching the explicit Scryer import while still
|
|
134
134
|
// allowing Trealla-style unqualified source to use the same autoload entry.
|
|
135
135
|
'call_nth/2': 'iso_ext',
|
|
136
|
+
// Scryer exposes call_residue_vars/2 from library(atts); autoload it for
|
|
137
|
+
// constraint-test source that uses the predicate without an explicit import.
|
|
138
|
+
'call_residue_vars/2': 'atts',
|
|
136
139
|
// Trealla exposes time/1 as a meta timing predicate and library(iso_ext)
|
|
137
140
|
// supplies ... //0. Autoload both so UWN's DCG hand-off benchmark runs
|
|
138
141
|
// unchanged while their implementations remain outside the ISO core.
|
package/src/term.js
CHANGED
|
@@ -8,7 +8,17 @@ export const STRING = 'string';
|
|
|
8
8
|
export const NUMBER = 'number';
|
|
9
9
|
export const COMPOUND = 'compound';
|
|
10
10
|
const EMPTY_ARGS = Object.freeze([]);
|
|
11
|
-
|
|
11
|
+
// Deep persistent binding chains make the many negative variable lookups in
|
|
12
|
+
// constraint propagation linear in the complete history. Periodically fold
|
|
13
|
+
// the chain into its indexed Map before that lookup cost dominates. Ordinary
|
|
14
|
+
// execution keeps the memory-friendly 512-layer threshold; attributed-variable
|
|
15
|
+
// propagation uses 256 because its repeated negative lookups dominate sooner.
|
|
16
|
+
const ENV_FLATTEN_DEPTH = 512;
|
|
17
|
+
const ATTRIBUTED_ENV_FLATTEN_DEPTH = 256;
|
|
18
|
+
// Runtime terms are structurally immutable: environments hold bindings beside
|
|
19
|
+
// them rather than rewriting their argument arrays. Cache only the syntactic
|
|
20
|
+
// variable names; binding reachability is still checked against each Env.
|
|
21
|
+
const structuralVariableCache = new WeakMap();
|
|
12
22
|
|
|
13
23
|
export class Term {
|
|
14
24
|
constructor(type, name, args = []) {
|
|
@@ -227,7 +237,10 @@ export class Env {
|
|
|
227
237
|
return undefined;
|
|
228
238
|
}
|
|
229
239
|
bind(name, term) {
|
|
230
|
-
|
|
240
|
+
const flattenDepth = this._prologAttributes == null
|
|
241
|
+
? ENV_FLATTEN_DEPTH
|
|
242
|
+
: ATTRIBUTED_ENV_FLATTEN_DEPTH;
|
|
243
|
+
if (this._state.depth >= flattenDepth) {
|
|
231
244
|
const flattened = new Map();
|
|
232
245
|
for (let state = this._state; state != null; state = state.parent) {
|
|
233
246
|
if (state.bindingName != null && !flattened.has(state.bindingName)) {
|
|
@@ -268,6 +281,28 @@ export class Env {
|
|
|
268
281
|
delays.set(name, [...(delays.get(name) ?? []), { goal, module }]);
|
|
269
282
|
this._delays = delays;
|
|
270
283
|
}
|
|
284
|
+
delayedVariableNames() {
|
|
285
|
+
if (this._delays == null || this._delays.size === 0) return [];
|
|
286
|
+
const names = [];
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
for (const name of this._delays.keys()) {
|
|
289
|
+
const root = deref(variable(name), this);
|
|
290
|
+
if (root.type !== VAR || seen.has(root.name)) continue;
|
|
291
|
+
seen.add(root.name);
|
|
292
|
+
names.push(root.name);
|
|
293
|
+
}
|
|
294
|
+
return names;
|
|
295
|
+
}
|
|
296
|
+
delayedGoals(name) {
|
|
297
|
+
const root = deref(variable(name), this);
|
|
298
|
+
if (root.type !== VAR || this._delays == null) return [];
|
|
299
|
+
const result = [];
|
|
300
|
+
for (const [source, goals] of this._delays) {
|
|
301
|
+
const current = deref(variable(source), this);
|
|
302
|
+
if (current.type === VAR && current.name === root.name) result.push(...goals);
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
271
306
|
takeReadyDelays() {
|
|
272
307
|
if (this._delays == null || this._delays.size === 0) return [];
|
|
273
308
|
const ready = [];
|
|
@@ -509,7 +544,35 @@ export function isConjunction(term) {
|
|
|
509
544
|
return term?.type === COMPOUND && term.name === ',' && term.arity === 2;
|
|
510
545
|
}
|
|
511
546
|
|
|
512
|
-
function
|
|
547
|
+
function structuralVariableNames(term) {
|
|
548
|
+
if (isScalar(term)) return EMPTY_ARGS;
|
|
549
|
+
if (term?.type === VAR) return [term.name];
|
|
550
|
+
if (term?.type !== COMPOUND) return EMPTY_ARGS;
|
|
551
|
+
|
|
552
|
+
const cached = structuralVariableCache.get(term);
|
|
553
|
+
if (cached !== undefined) return cached;
|
|
554
|
+
|
|
555
|
+
const names = new Set();
|
|
556
|
+
const stack = [term];
|
|
557
|
+
const seenTerms = new Set();
|
|
558
|
+
while (stack.length) {
|
|
559
|
+
const current = stack.pop();
|
|
560
|
+
if (current?.type === VAR) {
|
|
561
|
+
names.add(current.name);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (isCompactList(current)) return null;
|
|
565
|
+
if (current?.type !== COMPOUND || seenTerms.has(current)) continue;
|
|
566
|
+
seenTerms.add(current);
|
|
567
|
+
for (let i = 0; i < current.arity; i++) stack.push(current.args[i]);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const result = names.size === 0 ? EMPTY_ARGS : [...names];
|
|
571
|
+
structuralVariableCache.set(term, result);
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function occursUncached(variableName, term, env) {
|
|
513
576
|
// Walk bindings and compound arguments iteratively so the occurs check also
|
|
514
577
|
// remains safe for very deep terms. The visited sets make this defensive
|
|
515
578
|
// against cycles introduced through the public Env API.
|
|
@@ -537,6 +600,50 @@ function occurs(variableName, term, env) {
|
|
|
537
600
|
return false;
|
|
538
601
|
}
|
|
539
602
|
|
|
603
|
+
function occurs(variableName, term, env) {
|
|
604
|
+
if (isScalar(term)) return false;
|
|
605
|
+
const initial = structuralVariableNames(term);
|
|
606
|
+
if (initial == null) return occursUncached(variableName, term, env);
|
|
607
|
+
if (initial.length === 0) return false;
|
|
608
|
+
|
|
609
|
+
// Lists, wrappers, and arithmetic expressions commonly contain one logical
|
|
610
|
+
// variable. Follow that unary binding chain without allocating a work queue
|
|
611
|
+
// and hash set for every occurs check. Fall back to the general graph walk
|
|
612
|
+
// as soon as a binding fans out.
|
|
613
|
+
if (initial.length === 1) {
|
|
614
|
+
let name = initial[0];
|
|
615
|
+
const seen = [];
|
|
616
|
+
while (true) {
|
|
617
|
+
if (name === variableName) return true;
|
|
618
|
+
for (let index = 0; index < seen.length; index++) {
|
|
619
|
+
if (seen[index] === name) return false;
|
|
620
|
+
}
|
|
621
|
+
seen.push(name);
|
|
622
|
+
const binding = env?.get(name);
|
|
623
|
+
if (binding === undefined) return false;
|
|
624
|
+
const names = structuralVariableNames(binding);
|
|
625
|
+
if (names == null || names.length > 1) return occursUncached(variableName, term, env);
|
|
626
|
+
if (names.length === 0) return false;
|
|
627
|
+
name = names[0];
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const pending = initial.slice();
|
|
632
|
+
const seenVariables = new Set();
|
|
633
|
+
for (let index = 0; index < pending.length; index++) {
|
|
634
|
+
const name = pending[index];
|
|
635
|
+
if (name === variableName) return true;
|
|
636
|
+
if (seenVariables.has(name)) continue;
|
|
637
|
+
seenVariables.add(name);
|
|
638
|
+
const binding = env?.get(name);
|
|
639
|
+
if (binding === undefined) continue;
|
|
640
|
+
const names = structuralVariableNames(binding);
|
|
641
|
+
if (names == null) return occursUncached(variableName, term, env);
|
|
642
|
+
for (const child of names) pending.push(child);
|
|
643
|
+
}
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
|
|
540
647
|
export function unify(left, right, env, options = {}) {
|
|
541
648
|
// Iterative unification avoids deep JavaScript recursion on long lists or
|
|
542
649
|
// deeply nested compounds. The occurs check gives EyeProlog finite-tree
|
|
@@ -664,9 +771,16 @@ export function freshTerm(term, suffix, variables = new Map()) {
|
|
|
664
771
|
}
|
|
665
772
|
return fresh;
|
|
666
773
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
774
|
+
let fresh;
|
|
775
|
+
if (term.type === COMPOUND && term.arity === 0) {
|
|
776
|
+
fresh = atom(term.name);
|
|
777
|
+
} else {
|
|
778
|
+
const args = new Array(term.args.length);
|
|
779
|
+
for (let index = 0; index < args.length; index++) {
|
|
780
|
+
args[index] = freshTerm(term.args[index], suffix, variables);
|
|
781
|
+
}
|
|
782
|
+
fresh = new Term(term.type, term.name, args);
|
|
783
|
+
}
|
|
670
784
|
if (term.module != null) fresh.module = term.module;
|
|
671
785
|
return fresh;
|
|
672
786
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -132,7 +132,7 @@ function regressionCases() {
|
|
|
132
132
|
{
|
|
133
133
|
name: 'library(atts) provides Scryer-style attributed-variable hooks',
|
|
134
134
|
run: () => {
|
|
135
|
-
const source = `:- module(attr_probe, [check_get/0, check_alias/0, check_ok/0, check_bad/0, check_conflict/0, check_backtrack/0, check_term_vars/0]).
|
|
135
|
+
const source = `:- module(attr_probe, [check_get/0, check_alias/0, check_ok/0, check_bad/0, check_conflict/0, check_backtrack/0, check_term_vars/0, check_residue_new/0, check_residue_unchanged/0, check_residue_modified/0, check_residue_dif/0]).
|
|
136
136
|
:- use_module(library(atts)).
|
|
137
137
|
:- attribute mark/1.
|
|
138
138
|
attach(X, V) :- put_atts(X, mark(V)).
|
|
@@ -151,6 +151,10 @@ check_bad :- attach(X, 3), X = 4.
|
|
|
151
151
|
check_conflict :- attach(X, a), attach(Y, b), X = Y.
|
|
152
152
|
check_backtrack :- (attach(X, a), fail ; true), \\+ get_atts(X, mark(_)).
|
|
153
153
|
check_term_vars :- attach(X, a), term_attributed_variables(pair(X,Y), [X]), var(Y), X = a.
|
|
154
|
+
check_residue_new :- call_residue_vars(attach(X, a), Vs), Vs = [X].
|
|
155
|
+
check_residue_unchanged :- attach(X, a), call_residue_vars(true, Vs), Vs = [], X = a.
|
|
156
|
+
check_residue_modified :- attach(X, a), call_residue_vars(put_atts(X, mark(b)), Vs), Vs = [X], X = b.
|
|
157
|
+
check_residue_dif :- call_residue_vars(dif(X,Y), Vs), Vs = [X,Y].
|
|
154
158
|
`;
|
|
155
159
|
assertIncludes(run(source, { goal: 'attr_probe:check_get' }).stdout, 'attr_probe:check_get.', 'get_atts/2');
|
|
156
160
|
assertIncludes(run(source, { goal: 'attr_probe:check_alias' }).stdout, 'attr_probe:check_alias.', 'attribute transfer on aliasing');
|
|
@@ -159,6 +163,20 @@ check_term_vars :- attach(X, a), term_attributed_variables(pair(X,Y), [X]), var(
|
|
|
159
163
|
assertEqual(run(source, { goal: 'attr_probe:check_conflict' }).stdout, '', 'same-module attributes are verified before aliasing');
|
|
160
164
|
assertIncludes(run(source, { goal: 'attr_probe:check_backtrack' }).stdout, 'attr_probe:check_backtrack.', 'attributes backtrack with Env branches');
|
|
161
165
|
assertIncludes(run(source, { goal: 'attr_probe:check_term_vars' }).stdout, 'attr_probe:check_term_vars.', 'term_attributed_variables/2');
|
|
166
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_residue_new' }).stdout, 'attr_probe:check_residue_new.', 'call_residue_vars/2 reports newly attributed variables');
|
|
167
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_residue_unchanged' }).stdout, 'attr_probe:check_residue_unchanged.', 'call_residue_vars/2 ignores unchanged pre-existing attributes');
|
|
168
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_residue_modified' }).stdout, 'attr_probe:check_residue_modified.', 'call_residue_vars/2 reports modified attributes');
|
|
169
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_residue_dif' }).stdout, 'attr_probe:check_residue_dif.', 'call_residue_vars/2 includes native dif/2 residual variables');
|
|
170
|
+
const freezeResidue = `:- use_module(library(atts)).
|
|
171
|
+
:- use_module(library(prologue), [freeze/2]).
|
|
172
|
+
check :- call_residue_vars(freeze(X,true), Vs), Vs = [X].
|
|
173
|
+
`;
|
|
174
|
+
assertIncludes(run(freezeResidue, { goal: 'check' }).stdout, 'check.', 'call_residue_vars/2 includes delayed freeze/2 variables');
|
|
175
|
+
const clpzResidue = `:- use_module(library(atts)).
|
|
176
|
+
:- use_module(library(clpz)).
|
|
177
|
+
check :- call_residue_vars(X in 1..3, Vs), Vs = [X].
|
|
178
|
+
`;
|
|
179
|
+
assertIncludes(run(clpzResidue, { goal: 'check' }).stdout, 'check.', 'call_residue_vars/2 includes CLP(Z) attributed variables');
|
|
162
180
|
|
|
163
181
|
const residualFile = path.join(tmp, `atts-residual-${++tmpCounter}.pl`);
|
|
164
182
|
fs.writeFileSync(residualFile, `:- use_module(library(atts)).
|
|
@@ -2090,6 +2108,53 @@ c4 ?- call((!;1)).
|
|
|
2090
2108
|
assertEqual(advanced.stderr, '', 'advanced stderr');
|
|
2091
2109
|
},
|
|
2092
2110
|
},
|
|
2111
|
+
{
|
|
2112
|
+
name: 'REPL exposes output side effects while search is still running (issue #72)',
|
|
2113
|
+
run: () => {
|
|
2114
|
+
const helper = `
|
|
2115
|
+
import { spawn } from 'node:child_process';
|
|
2116
|
+
|
|
2117
|
+
const child = spawn(${JSON.stringify(process.execPath)}, [${JSON.stringify(bin)}], {
|
|
2118
|
+
cwd: ${JSON.stringify(packageRoot)},
|
|
2119
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2120
|
+
});
|
|
2121
|
+
child.stdout.setEncoding('utf8');
|
|
2122
|
+
child.stderr.setEncoding('utf8');
|
|
2123
|
+
let stdout = '';
|
|
2124
|
+
let stderr = '';
|
|
2125
|
+
child.stdout.on('data', (text) => { stdout += text; });
|
|
2126
|
+
child.stderr.on('data', (text) => { stderr += text; });
|
|
2127
|
+
|
|
2128
|
+
async function waitFor(predicate, label) {
|
|
2129
|
+
const deadline = Date.now() + 5000;
|
|
2130
|
+
while (!predicate()) {
|
|
2131
|
+
if (Date.now() >= deadline) {
|
|
2132
|
+
child.kill('SIGKILL');
|
|
2133
|
+
throw new Error(label + ' timeout; stdout=' + JSON.stringify(stdout) + '; stderr=' + JSON.stringify(stderr));
|
|
2134
|
+
}
|
|
2135
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
child.stdin.write('use_module(library(iso_ext)).\\n');
|
|
2140
|
+
await waitFor(() => stdout.includes(' true.\\n?- '), 'module import');
|
|
2141
|
+
child.stdin.write('call_nth(repeat,Nth), (Nth mod 2=:=0->writeq(r(Nth)),nl;true),Nth<0.\\n');
|
|
2142
|
+
await waitFor(() => stdout.includes('r(2)\\nr(4)\\n'), 'progress output');
|
|
2143
|
+
if (stdout.includes('false.')) throw new Error('infinite search unexpectedly reached a leaf');
|
|
2144
|
+
child.kill('SIGKILL');
|
|
2145
|
+
await new Promise((resolve) => child.once('exit', resolve));
|
|
2146
|
+
process.stdout.write('progress-visible');
|
|
2147
|
+
`;
|
|
2148
|
+
const result = spawnSync(process.execPath, [
|
|
2149
|
+
'--input-type=module',
|
|
2150
|
+
'--eval',
|
|
2151
|
+
helper,
|
|
2152
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
|
|
2153
|
+
if (result.error) throw result.error;
|
|
2154
|
+
assertEqual(result.status, 0, `progress helper status; stderr=${result.stderr}`);
|
|
2155
|
+
assertEqual(result.stdout, 'progress-visible', 'newline-terminated progress is visible before a leaf answer');
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2093
2158
|
{
|
|
2094
2159
|
name: 'REPL bindings use argument syntax for operator atoms',
|
|
2095
2160
|
run: () => {
|
|
@@ -4939,7 +5004,7 @@ answer(ok) :-
|
|
|
4939
5004
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
4940
5005
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
4941
5006
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
4942
|
-
assertEqual(library.defs.size,
|
|
5007
|
+
assertEqual(library.defs.size, 155, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and generic library adapters');
|
|
4943
5008
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
4944
5009
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
4945
5010
|
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
|
@@ -4956,21 +5021,23 @@ answer(ok) :-
|
|
|
4956
5021
|
assertEqual(Boolean(library.get('call_cleanup', 2)), true, 'call_cleanup/2 is an EyeProlog cleanup control');
|
|
4957
5022
|
assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
|
|
4958
5023
|
assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
|
|
4959
|
-
assertEqual(registeredNativeEyePrologLibraryNames().length,
|
|
5024
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 49, 'public native EyeProlog builtin count');
|
|
4960
5025
|
assertEqual(eyePrologPortableLibraryIndicators.length, 87, 'portable Prolog library count');
|
|
4961
|
-
assertEqual(eyePrologInteropLibraryIndicators.length,
|
|
5026
|
+
assertEqual(eyePrologInteropLibraryIndicators.length, 30, 'cross-implementation interop profile count');
|
|
4962
5027
|
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda,atts', 'common explicit library module profile');
|
|
4963
5028
|
assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
|
|
4964
5029
|
assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
|
|
4965
5030
|
assertEqual(eyePrologInteropAutoload['call_nth/2'], 'iso_ext', 'call_nth/2 canonical interop autoload');
|
|
5031
|
+
assertEqual(eyePrologInteropAutoload['call_residue_vars/2'], 'atts', 'call_residue_vars/2 canonical interop autoload');
|
|
4966
5032
|
assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
|
|
4967
5033
|
assertEqual(eyePrologInteropAutoload['.../2'], 'iso_ext', '.../2 canonical interop autoload');
|
|
4968
5034
|
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
4969
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
5035
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 49, 'native host library count');
|
|
4970
5036
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 3).join(','), 'call_nth/2,freeze/2,dif/2', 'control and constraint predicates requiring host support');
|
|
4971
|
-
assertEqual(eyePrologLibraryIndicators.length,
|
|
5037
|
+
assertEqual(eyePrologLibraryIndicators.length, 136, 'complete EyeProlog library surface');
|
|
4972
5038
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
4973
5039
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
5040
|
+
assertEqual(Boolean(library.get('eyeprolog__call_residue_vars', 2)), true, 'private call_residue_vars adapter is registered for EyeProlog');
|
|
4974
5041
|
assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
|
|
4975
5042
|
assertEqual(registry.get('eyeprolog__freeze', 2), null, 'private freeze adapter is absent from ISO registry');
|
|
4976
5043
|
assertEqual(Boolean(library.get('eyeprolog__freeze', 2)), true, 'private freeze adapter is registered for EyeProlog');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6253,7 +6253,7 @@ between solution branches.
|
|
|
6253
6253
|
| Module | Exported predicate indicators |
|
|
6254
6254
|
| --- | --- |
|
|
6255
6255
|
| `library(aggregate)` | `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
|
|
6256
|
-
| `library(atts)` | `put_atts/2`, `get_atts/2`, `put_attr/3`, `get_attr/3`, `del_attr/2`, `term_attributed_variables/2` |
|
|
6256
|
+
| `library(atts)` | `put_atts/2`, `get_atts/2`, `put_attr/3`, `get_attr/3`, `del_attr/2`, `term_attributed_variables/2`, `call_residue_vars/2` |
|
|
6257
6257
|
| `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `nvalue/2`, `sum/3`, `scalar_product/4`, `tuples_in/2`, `labeling/2`, `label/1`, `indomain/1`, `lex_chain/1`, `serialized/2`, `global_cardinality/2`, `global_cardinality/3`, `circuit/1`, `chain/2`, `element/3`, `zcompare/3`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2` |
|
|
6258
6258
|
| `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
|
|
6259
6259
|
| `library(dates)` | `difference/3` |
|