eyeprolog 1.3.67 → 1.3.69
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 +10 -6
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +19 -2
- package/src/CLPZ-MIGRATION.md +49 -11
- package/src/expansion-builtins.js +99 -0
- package/src/lib/arithmetic.pl +36 -0
- package/src/lib/assoc.pl +42 -0
- package/src/lib/between.pl +54 -0
- package/src/lib/dcgs.pl +23 -0
- package/src/lib/debug.pl +17 -0
- package/src/lib/error.pl +123 -0
- package/src/lib/format.pl +35 -0
- package/src/lib/freeze.pl +28 -0
- package/src/lib/pairs.pl +36 -0
- package/src/lib/si.pl +62 -0
- package/src/lib/terms.pl +18 -0
- package/src/parser.js +26 -0
- package/src/program.js +73 -7
- package/src/scryer-compat.js +39 -0
- package/src/solver.js +1 -1
- package/src/source-expansion.js +173 -0
- package/src/standard-library.js +15 -0
- package/src/term.js +13 -0
- package/test/run-regression.mjs +118 -1
- package/the-art-of-eyeprolog.md +12 -5
package/src/lib/si.pl
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Sufficient-instantiation tests compatible with Scryer library(si). */
|
|
2
|
+
|
|
3
|
+
:- module(si, [
|
|
4
|
+
atom_si/1,
|
|
5
|
+
integer_si/1,
|
|
6
|
+
atomic_si/1,
|
|
7
|
+
list_si/1,
|
|
8
|
+
character_si/1,
|
|
9
|
+
term_si/1,
|
|
10
|
+
chars_si/1,
|
|
11
|
+
dif_si/2,
|
|
12
|
+
not_si/1,
|
|
13
|
+
when_si/2
|
|
14
|
+
]).
|
|
15
|
+
|
|
16
|
+
:- meta_predicate(not_si(0)).
|
|
17
|
+
:- meta_predicate(when_si(+, 0)).
|
|
18
|
+
|
|
19
|
+
atom_si(A) :- ( var(A) -> throw(error(instantiation_error, atom_si/1)) ; atom(A) ).
|
|
20
|
+
integer_si(I) :- ( var(I) -> throw(error(instantiation_error, integer_si/1)) ; integer(I) ).
|
|
21
|
+
atomic_si(A) :- ( var(A) -> throw(error(instantiation_error, atomic_si/1)) ; atomic(A) ).
|
|
22
|
+
|
|
23
|
+
list_si(List) :- si__list(List).
|
|
24
|
+
si__list([]).
|
|
25
|
+
si__list([_|Tail]) :- !, si__list(Tail).
|
|
26
|
+
si__list(Term) :-
|
|
27
|
+
( var(Term) -> throw(error(instantiation_error, list_si/1)) ; fail ).
|
|
28
|
+
|
|
29
|
+
character_si(C) :-
|
|
30
|
+
( var(C) -> throw(error(instantiation_error, character_si/1))
|
|
31
|
+
; atom(C), atom_length(C, 1)
|
|
32
|
+
).
|
|
33
|
+
|
|
34
|
+
term_si(Term) :-
|
|
35
|
+
( ground(Term) -> acyclic_term(Term)
|
|
36
|
+
; throw(error(instantiation_error, term_si/1))
|
|
37
|
+
).
|
|
38
|
+
|
|
39
|
+
chars_si(Chars) :-
|
|
40
|
+
list_si(Chars),
|
|
41
|
+
si__chars(Chars).
|
|
42
|
+
si__chars([]).
|
|
43
|
+
si__chars([C|Cs]) :- character_si(C), si__chars(Cs).
|
|
44
|
+
|
|
45
|
+
dif_si(X, Y) :-
|
|
46
|
+
X \== Y,
|
|
47
|
+
( X \= Y -> true ; throw(error(instantiation_error, dif_si/2)) ).
|
|
48
|
+
|
|
49
|
+
not_si(Goal) :- term_si(Goal), \+ Goal.
|
|
50
|
+
|
|
51
|
+
when_si(Condition, Goal) :-
|
|
52
|
+
( si__condition(Condition) ->
|
|
53
|
+
( call(Condition) -> call(Goal)
|
|
54
|
+
; throw(error(instantiation_error, when_si/2)) )
|
|
55
|
+
; throw(error(domain_error(when_condition_si, Condition), when_si/2))
|
|
56
|
+
).
|
|
57
|
+
|
|
58
|
+
si__condition(Condition) :- var(Condition), !, throw(error(instantiation_error, when_si/2)).
|
|
59
|
+
si__condition(ground(_)).
|
|
60
|
+
si__condition(nonvar(_)).
|
|
61
|
+
si__condition((A,B)) :- si__condition(A), si__condition(B).
|
|
62
|
+
si__condition((A;B)) :- si__condition(A), si__condition(B).
|
package/src/lib/terms.pl
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Term helpers compatible with the Scryer library surface used by CLP(Z). */
|
|
2
|
+
|
|
3
|
+
:- module(terms, [numbervars/3, copy_term_nat/2]).
|
|
4
|
+
|
|
5
|
+
:- use_module(library(error), [must_be/2, can_be/2]).
|
|
6
|
+
|
|
7
|
+
copy_term_nat(Term, Copy) :- copy_term(Term, Copy).
|
|
8
|
+
|
|
9
|
+
numbervars(Term, N0, N) :-
|
|
10
|
+
must_be(integer, N0),
|
|
11
|
+
can_be(integer, N),
|
|
12
|
+
term_variables(Term, Variables),
|
|
13
|
+
terms__numbervars(Variables, N0, N).
|
|
14
|
+
|
|
15
|
+
terms__numbervars([], N, N).
|
|
16
|
+
terms__numbervars(['$VAR'(N0)|Variables], N0, N) :-
|
|
17
|
+
N1 is N0 + 1,
|
|
18
|
+
terms__numbervars(Variables, N1, N).
|
package/src/parser.js
CHANGED
|
@@ -1119,6 +1119,32 @@ class Parser {
|
|
|
1119
1119
|
head = items.pop();
|
|
1120
1120
|
while (items.length > 0) head = compound(',', [items.pop(), head]);
|
|
1121
1121
|
}
|
|
1122
|
+
// The clause grammar keeps priority-1200 neck/DCG tokens outside the
|
|
1123
|
+
// initial head parse. A user-defined operator at priority 1199 or 1200,
|
|
1124
|
+
// however, can itself be the outermost source term. Scryer's CLP(Z) uses
|
|
1125
|
+
// exactly this shape for its private `++>` Duo-DCG rules. Fold such a
|
|
1126
|
+
// custom low-strength infix operator back into the source term while
|
|
1127
|
+
// leaving the processor-defined :-, -->, and ?- dispatch below intact.
|
|
1128
|
+
const topLevelOperator = this.operatorTokenName();
|
|
1129
|
+
const topLevelInfo = topLevelOperator == null ? null : this.infixOperators.get(topLevelOperator);
|
|
1130
|
+
if (topLevelInfo && topLevelInfo.precedence < 3 &&
|
|
1131
|
+
![':-', '-->', '?-'].includes(topLevelOperator)) {
|
|
1132
|
+
this.advance();
|
|
1133
|
+
const right = this.parseTerm(
|
|
1134
|
+
topLevelInfo.associativity === 'right' ? topLevelInfo.precedence : topLevelInfo.precedence + 1,
|
|
1135
|
+
true,
|
|
1136
|
+
true,
|
|
1137
|
+
false,
|
|
1138
|
+
);
|
|
1139
|
+
head = compound(topLevelOperator, [head, right]);
|
|
1140
|
+
if (topLevelInfo.associativity === 'none') {
|
|
1141
|
+
const nextOperator = this.operatorTokenName();
|
|
1142
|
+
if (this.infixOperators.get(nextOperator)?.precedence === topLevelInfo.precedence) {
|
|
1143
|
+
throw new Error(`parse line ${this.token.line}: non-associative operator ${topLevelOperator} requires parentheses`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1122
1148
|
// Parentheses and other ordinary term syntax may hide the surface ?-
|
|
1123
1149
|
// token from the program-level dispatch above. Once the complete head
|
|
1124
1150
|
// term has been parsed, recognize the same ?-/1 or ?-/2 structure here.
|
package/src/program.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
eyePrologInteropLibraryModules,
|
|
22
22
|
} from './standard-library.js';
|
|
23
23
|
import { expandDcgRuleClause } from './dcg.js';
|
|
24
|
+
import { expandClauseGoals, expandSourceClause } from './source-expansion.js';
|
|
24
25
|
import {
|
|
25
26
|
CompactBinaryClause, clauseBodyLength, clauseHasCut, compactHeadArgName, compactHeadArgType, modulePredicateKey,
|
|
26
27
|
indexCompactOne, indexOne, isCompactBinaryClause, makeArgumentIndex, rebuildGroupIndexes, termContainsCut, termHasNoVariables,
|
|
@@ -1069,18 +1070,56 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1069
1070
|
builder.addClauses(batch);
|
|
1070
1071
|
batch.length = 0;
|
|
1071
1072
|
};
|
|
1072
|
-
|
|
1073
|
+
|
|
1074
|
+
const processClause = (inputClause) => {
|
|
1075
|
+
let clause = inputClause;
|
|
1073
1076
|
if (clause?.kind === 'quad') {
|
|
1074
1077
|
flush();
|
|
1075
1078
|
clause.module = context.module;
|
|
1076
1079
|
builder.addClauses([clause]);
|
|
1077
1080
|
return;
|
|
1078
1081
|
}
|
|
1082
|
+
|
|
1083
|
+
// User source expansion is a normal-profile compile-time facility. Hooks
|
|
1084
|
+
// execute against the clauses that have already been prepared, matching
|
|
1085
|
+
// Prolog's left-to-right source loading model. Strict ISO Part 1 keeps its
|
|
1086
|
+
// processor-defined preparation semantics unchanged.
|
|
1087
|
+
if (!builder.program.strictIso) {
|
|
1088
|
+
const expansion = expandSourceClause(builder.program, clause, context.module, {
|
|
1089
|
+
module: context.module,
|
|
1090
|
+
filename: options.filename ?? '<input>',
|
|
1091
|
+
});
|
|
1092
|
+
if (expansion.expanded) {
|
|
1093
|
+
for (const generated of expansion.clauses) processPreparedClause(generated);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
processPreparedClause(clause);
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const processPreparedClause = (inputClause) => {
|
|
1101
|
+
let clause = inputClause;
|
|
1102
|
+
const lexicalModule = context.module;
|
|
1103
|
+
|
|
1079
1104
|
// Grammar-rule expansion belongs to ISO/IEC TS 13211-3 rather than the
|
|
1080
1105
|
// Part 1 strict-core language. In strict core mode -->/2 remains the
|
|
1081
1106
|
// ordinary predefined operator term from Table 7 and is not rewritten.
|
|
1082
|
-
const grammarClause = builder.program.strictIso ? null : expandDcgRuleClause(clause,
|
|
1107
|
+
const grammarClause = builder.program.strictIso ? null : expandDcgRuleClause(clause, lexicalModule);
|
|
1083
1108
|
if (grammarClause) clause = grammarClause;
|
|
1109
|
+
|
|
1110
|
+
// A source module qualifier on a clause head selects the procedure owner,
|
|
1111
|
+
// while unqualified calls in the body retain the lexical module of the
|
|
1112
|
+
// source text. This distinction is essential for conventional hooks such
|
|
1113
|
+
// as user:term_expansion/2 defined from library modules.
|
|
1114
|
+
clause = normalizeQualifiedClauseHead(clause, lexicalModule);
|
|
1115
|
+
|
|
1116
|
+
if (!builder.program.strictIso && !isDirectiveClause(clause)) {
|
|
1117
|
+
expandClauseGoals(builder.program, clause, lexicalModule, {
|
|
1118
|
+
module: lexicalModule,
|
|
1119
|
+
filename: options.filename ?? '<input>',
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1084
1123
|
const moduleDeclaration = moduleDirective(clause);
|
|
1085
1124
|
if (moduleDeclaration) {
|
|
1086
1125
|
flush();
|
|
@@ -1121,10 +1160,20 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1121
1160
|
clause.module ??= context.module;
|
|
1122
1161
|
clause.textUnit ??= context.textUnit;
|
|
1123
1162
|
if (!isDirectiveClause(clause)) {
|
|
1124
|
-
|
|
1163
|
+
const bodyModule = clause.lexicalModule ?? lexicalModule;
|
|
1164
|
+
for (const goal of clause.body) annotateGoalModule(goal, bodyModule);
|
|
1165
|
+
}
|
|
1166
|
+
const isExpansionHook = !isDirectiveClause(clause) &&
|
|
1167
|
+
clause.head?.arity === 2 && ['term_expansion', 'goal_expansion'].includes(clause.head?.name);
|
|
1168
|
+
if (isExpansionHook) {
|
|
1169
|
+
// A hook becomes visible to the very next source term, so it cannot
|
|
1170
|
+
// wait in the large streaming build batch.
|
|
1171
|
+
flush();
|
|
1172
|
+
builder.addClauses([clause]);
|
|
1173
|
+
} else {
|
|
1174
|
+
batch.push(clause);
|
|
1175
|
+
if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
|
|
1125
1176
|
}
|
|
1126
|
-
batch.push(clause);
|
|
1127
|
-
if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
|
|
1128
1177
|
return;
|
|
1129
1178
|
}
|
|
1130
1179
|
flush();
|
|
@@ -1152,15 +1201,32 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1152
1201
|
batch[batch.length - 1].textUnit = context.textUnit;
|
|
1153
1202
|
if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
|
|
1154
1203
|
};
|
|
1155
|
-
const parsed = tryParseClausesFastInto(source,
|
|
1204
|
+
const parsed = tryParseClausesFastInto(source, processClause, acceptBinary, options);
|
|
1156
1205
|
if (parsed) flush();
|
|
1157
1206
|
return parsed;
|
|
1158
1207
|
}
|
|
1159
|
-
parseClausesInto(source, options,
|
|
1208
|
+
parseClausesInto(source, options, processClause);
|
|
1160
1209
|
flush();
|
|
1161
1210
|
return true;
|
|
1162
1211
|
}
|
|
1163
1212
|
|
|
1213
|
+
function normalizeQualifiedClauseHead(clause, lexicalModule = 'user') {
|
|
1214
|
+
if (clause == null || clause.kind === 'quad' || isDirectiveClause(clause)) return clause;
|
|
1215
|
+
const head = clause.head;
|
|
1216
|
+
if (head?.type !== COMPOUND || head.name !== ':' || head.arity !== 2) return clause;
|
|
1217
|
+
const qualifier = head.args[0];
|
|
1218
|
+
const qualifiedHead = head.args[1];
|
|
1219
|
+
if (qualifier.type === VAR) throw new PrologError('instantiation_error');
|
|
1220
|
+
if (qualifier.type !== ATOM) throw new PrologError('type_error(atom)', qualifier);
|
|
1221
|
+
if (qualifiedHead.type !== ATOM && qualifiedHead.type !== COMPOUND) {
|
|
1222
|
+
throw new PrologError('type_error(callable)', qualifiedHead);
|
|
1223
|
+
}
|
|
1224
|
+
clause.head = qualifiedHead;
|
|
1225
|
+
clause.module = qualifier.name;
|
|
1226
|
+
clause.lexicalModule = lexicalModule;
|
|
1227
|
+
return clause;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1164
1230
|
function includeDirective(clause) {
|
|
1165
1231
|
if (isCompactBinaryClause(clause) || !isDirectiveClause(clause)) return null;
|
|
1166
1232
|
const directive = clause.head.args[0];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Small host primitives needed by Scryer-style declarative libraries.
|
|
2
|
+
//
|
|
3
|
+
// Keep these facilities generic: they are not part of the CLP(Z) engine. The
|
|
4
|
+
// backtrackable blackboard lives in Env so ordinary Prolog choice points undo
|
|
5
|
+
// writes automatically.
|
|
6
|
+
|
|
7
|
+
import { ATOM, NUMBER, STRING, deref, unify } from './term.js';
|
|
8
|
+
import { PrologError } from './errors.js';
|
|
9
|
+
|
|
10
|
+
function blackboardKey(term, env) {
|
|
11
|
+
const value = deref(term, env);
|
|
12
|
+
if (value.type === ATOM) return `a:${value.name}`;
|
|
13
|
+
if (value.type === NUMBER) return `n:${value.name}`;
|
|
14
|
+
if (value.type === STRING) return `s:${value.name}`;
|
|
15
|
+
if (value.type === 'var') throw new PrologError('instantiation_error');
|
|
16
|
+
throw new PrologError('type_error(atomic)', value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function* bbGetBuiltin({ goal, env }) {
|
|
20
|
+
const key = blackboardKey(goal.args[0], env);
|
|
21
|
+
const stored = env.getBacktrackableBlackboard(key);
|
|
22
|
+
if (stored === undefined) return;
|
|
23
|
+
const next = env.clone();
|
|
24
|
+
if (unify(goal.args[1], stored, next)) yield next;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function* bbPutBuiltin({ goal, env }) {
|
|
28
|
+
const next = env.clone();
|
|
29
|
+
const key = blackboardKey(goal.args[0], next);
|
|
30
|
+
next.putBacktrackableBlackboard(key, deref(goal.args[1], next));
|
|
31
|
+
yield next;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const scryerCompatibilityBuiltins = {
|
|
35
|
+
register(registry) {
|
|
36
|
+
registry.add('eyeprolog__bb_get', 2, bbGetBuiltin, { eyePrologLibrary: true });
|
|
37
|
+
registry.add('eyeprolog__bb_b_put', 2, bbPutBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
38
|
+
},
|
|
39
|
+
};
|
package/src/solver.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { numberValueKey, sameNumberValue } from './number-value.js';
|
|
9
9
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
10
10
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
11
|
-
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
|
|
11
|
+
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
|
|
12
12
|
import { StreamManager } from './io.js';
|
|
13
13
|
import { clpzStateConsistent } from './clpz.js';
|
|
14
14
|
import { hardHeapLimit, softHeapLimit, usedHeapSize } from './platform.js';
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// User-defined source expansion for the normal EyeProlog profile.
|
|
2
|
+
//
|
|
3
|
+
// This deliberately sits beside program preparation rather than inside the
|
|
4
|
+
// term kernel. A small, fresh Solver executes already-loaded expansion hooks
|
|
5
|
+
// against the partially built Program. The solver itself imports clause-index
|
|
6
|
+
// helpers directly, so this compiler service does not introduce a module cycle.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ATOM, COMPOUND, Env, Term, VAR, atom, compound, deref, variable, variantTerms,
|
|
10
|
+
} from './term.js';
|
|
11
|
+
import { modulePredicateKey } from './program-indexing.js';
|
|
12
|
+
import { Solver } from './solver.js';
|
|
13
|
+
import { clauseToSourceTerm, sourceTermToClauses } from './expansion-builtins.js';
|
|
14
|
+
import { PrologError } from './errors.js';
|
|
15
|
+
|
|
16
|
+
let expansionFresh = 0;
|
|
17
|
+
const MAX_EXPANSION_DEPTH = 64;
|
|
18
|
+
const MAX_EXPANSION_INFERENCES = 1_000_000;
|
|
19
|
+
|
|
20
|
+
function exactHookGroup(program, module, name) {
|
|
21
|
+
return program.groups.get(modulePredicateKey(module, name, 2)) ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hookModules(module) {
|
|
25
|
+
return module === 'user' ? ['user'] : [module, 'user'];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function inputVariableNames(term) {
|
|
29
|
+
const names = new Set();
|
|
30
|
+
const stack = [term];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
while (stack.length > 0) {
|
|
33
|
+
const current = stack.pop();
|
|
34
|
+
if (current == null || seen.has(current)) continue;
|
|
35
|
+
seen.add(current);
|
|
36
|
+
if (current.type === VAR) {
|
|
37
|
+
names.add(current.name);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (current.type === COMPOUND) {
|
|
41
|
+
for (const arg of current.args) stack.push(arg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return names;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function copyHookResult(term, env, inputNames) {
|
|
48
|
+
const rootNames = new Map();
|
|
49
|
+
for (const name of inputNames) {
|
|
50
|
+
const root = deref(variable(name), env);
|
|
51
|
+
if (root.type === VAR && !rootNames.has(root.name)) rootNames.set(root.name, name);
|
|
52
|
+
}
|
|
53
|
+
const copy = (value) => {
|
|
54
|
+
const resolved = deref(value, env);
|
|
55
|
+
if (resolved.type === VAR) return variable(rootNames.get(resolved.name) ?? resolved.name);
|
|
56
|
+
const copied = resolved.type === COMPOUND && resolved.arity === 0
|
|
57
|
+
? atom(resolved.name)
|
|
58
|
+
: new Term(resolved.type, resolved.name, resolved.args.map(copy));
|
|
59
|
+
if (resolved.module != null) copied.module = resolved.module;
|
|
60
|
+
return copied;
|
|
61
|
+
};
|
|
62
|
+
return copy(term);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function runHook(program, module, name, input, loadContext = null) {
|
|
66
|
+
for (const owner of hookModules(module)) {
|
|
67
|
+
const group = exactHookGroup(program, owner, name);
|
|
68
|
+
if (group == null || group.clauses.length === 0) continue;
|
|
69
|
+
|
|
70
|
+
const output = variable(`\u0000source-expansion:${++expansionFresh}`);
|
|
71
|
+
const goal = compound(name, [input, output]);
|
|
72
|
+
goal.module = owner;
|
|
73
|
+
const solver = new Solver(program, {
|
|
74
|
+
solutionLimit: 1,
|
|
75
|
+
maxInferences: MAX_EXPANSION_INFERENCES,
|
|
76
|
+
sourceLoadContext: loadContext,
|
|
77
|
+
});
|
|
78
|
+
const inputNames = inputVariableNames(input);
|
|
79
|
+
for (const answer of solver.solve([goal], new Env())) {
|
|
80
|
+
return copyHookResult(output, answer, inputNames);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sourceTermsAreVariants(left, right) {
|
|
87
|
+
return variantTerms(left, new Env(), right, new Env());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function expandTermRecursively(program, module, term, loadContext, depth) {
|
|
91
|
+
if (depth > MAX_EXPANSION_DEPTH) throw new PrologError('resource_error(source_expansion)');
|
|
92
|
+
const expanded = runHook(program, module, 'term_expansion', term, loadContext);
|
|
93
|
+
if (expanded == null) return sourceTermToClauses(term);
|
|
94
|
+
|
|
95
|
+
const clauses = sourceTermToClauses(expanded);
|
|
96
|
+
if (clauses.length === 1 && sourceTermsAreVariants(term, clauseToSourceTerm(clauses[0]))) return clauses;
|
|
97
|
+
const result = [];
|
|
98
|
+
for (const clause of clauses) {
|
|
99
|
+
const generatedTerm = clauseToSourceTerm(clause);
|
|
100
|
+
result.push(...expandTermRecursively(program, module, generatedTerm, loadContext, depth + 1));
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function expandSourceClause(program, clause, module = 'user', loadContext = null) {
|
|
106
|
+
const sourceTerm = clauseToSourceTerm(clause);
|
|
107
|
+
if (sourceTerm == null) return { expanded: false, clauses: [clause] };
|
|
108
|
+
if (exactHookGroup(program, module, 'term_expansion') == null &&
|
|
109
|
+
(module === 'user' || exactHookGroup(program, 'user', 'term_expansion') == null)) {
|
|
110
|
+
return { expanded: false, clauses: [clause] };
|
|
111
|
+
}
|
|
112
|
+
const first = runHook(program, module, 'term_expansion', sourceTerm, loadContext);
|
|
113
|
+
if (first == null) return { expanded: false, clauses: [clause] };
|
|
114
|
+
|
|
115
|
+
const firstClauses = sourceTermToClauses(first);
|
|
116
|
+
if (firstClauses.length === 1 && sourceTermsAreVariants(sourceTerm, clauseToSourceTerm(firstClauses[0]))) {
|
|
117
|
+
return { expanded: false, clauses: [clause] };
|
|
118
|
+
}
|
|
119
|
+
const result = [];
|
|
120
|
+
for (const generated of firstClauses) {
|
|
121
|
+
const generatedTerm = clauseToSourceTerm(generated);
|
|
122
|
+
result.push(...expandTermRecursively(program, module, generatedTerm, loadContext, 1));
|
|
123
|
+
}
|
|
124
|
+
return { expanded: true, clauses: result };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isCallable(term) {
|
|
128
|
+
return term?.type === ATOM || term?.type === COMPOUND;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function expandGoalChildren(program, goal, module, loadContext, depth) {
|
|
132
|
+
if (goal.type !== COMPOUND) return goal;
|
|
133
|
+
|
|
134
|
+
if (goal.name === ':' && goal.arity === 2) {
|
|
135
|
+
const qualifier = deref(goal.args[0], new Env());
|
|
136
|
+
if (qualifier.type === VAR) return goal;
|
|
137
|
+
if (qualifier.type !== ATOM) return goal;
|
|
138
|
+
const child = expandGoalRecursively(program, goal.args[1], qualifier.name, loadContext, depth + 1);
|
|
139
|
+
return compound(':', [goal.args[0], child]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if ([',', ';', '->'].includes(goal.name) && goal.arity === 2) {
|
|
143
|
+
return compound(goal.name, [
|
|
144
|
+
expandGoalRecursively(program, goal.args[0], module, loadContext, depth + 1),
|
|
145
|
+
expandGoalRecursively(program, goal.args[1], module, loadContext, depth + 1),
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
if (goal.name === '\\+' && goal.arity === 1) {
|
|
149
|
+
return compound('\\+', [expandGoalRecursively(program, goal.args[0], module, loadContext, depth + 1)]);
|
|
150
|
+
}
|
|
151
|
+
return goal;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function expandGoalRecursively(program, goal, module, loadContext, depth = 0) {
|
|
155
|
+
if (depth > MAX_EXPANSION_DEPTH) throw new PrologError('resource_error(source_expansion)');
|
|
156
|
+
const resolved = deref(goal, new Env());
|
|
157
|
+
if (!isCallable(resolved)) return resolved;
|
|
158
|
+
|
|
159
|
+
const expanded = runHook(program, module, 'goal_expansion', resolved, loadContext);
|
|
160
|
+
if (expanded != null) {
|
|
161
|
+
if (!isCallable(expanded)) throw new PrologError('type_error(callable)', expanded);
|
|
162
|
+
if (sourceTermsAreVariants(resolved, expanded)) return expandGoalChildren(program, resolved, module, loadContext, depth);
|
|
163
|
+
return expandGoalRecursively(program, expanded, module, loadContext, depth + 1);
|
|
164
|
+
}
|
|
165
|
+
return expandGoalChildren(program, resolved, module, loadContext, depth);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function expandClauseGoals(program, clause, lexicalModule = 'user', loadContext = null) {
|
|
169
|
+
if (!Array.isArray(clause.body) || clause.body.length === 0) return clause;
|
|
170
|
+
clause.body = clause.body.map((goal) =>
|
|
171
|
+
expandGoalRecursively(program, goal, lexicalModule, loadContext));
|
|
172
|
+
return clause;
|
|
173
|
+
}
|
package/src/standard-library.js
CHANGED
|
@@ -5,23 +5,36 @@
|
|
|
5
5
|
import { PrologError, createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
|
|
6
6
|
import { clpzBuiltins } from './clpz.js';
|
|
7
7
|
import { attsBuiltins } from './atts.js';
|
|
8
|
+
import { expansionBuiltins } from './expansion-builtins.js';
|
|
9
|
+
import { scryerCompatibilityBuiltins } from './scryer-compat.js';
|
|
8
10
|
import { registerCleanupBuiltins } from './cleanup.js';
|
|
9
11
|
import { fs, isNode, memoryStatistics } from './platform.js';
|
|
10
12
|
import { ATOM, VAR, atom, deref, numberTerm, unify } from './term.js';
|
|
11
13
|
|
|
12
14
|
const moduleFiles = Object.freeze({
|
|
13
15
|
aggregate: 'aggregate.pl',
|
|
16
|
+
arithmetic: 'arithmetic.pl',
|
|
17
|
+
assoc: 'assoc.pl',
|
|
14
18
|
atts: 'atts.pl',
|
|
19
|
+
between: 'between.pl',
|
|
15
20
|
clpz: 'clpz.pl',
|
|
16
21
|
comparison: 'comparison.pl',
|
|
17
22
|
dates: 'dates.pl',
|
|
23
|
+
dcgs: 'dcgs.pl',
|
|
24
|
+
debug: 'debug.pl',
|
|
25
|
+
error: 'error.pl',
|
|
26
|
+
format: 'format.pl',
|
|
27
|
+
freeze: 'freeze.pl',
|
|
18
28
|
iso_ext: 'iso_ext.pl',
|
|
19
29
|
lambda: 'lambda.pl',
|
|
20
30
|
lists: 'lists.pl',
|
|
31
|
+
pairs: 'pairs.pl',
|
|
21
32
|
primes: 'primes.pl',
|
|
22
33
|
prologue: 'prologue.pl',
|
|
23
34
|
random: 'random.pl',
|
|
35
|
+
si: 'si.pl',
|
|
24
36
|
strings: 'strings.pl',
|
|
37
|
+
terms: 'terms.pl',
|
|
25
38
|
uuid: 'uuid.pl',
|
|
26
39
|
});
|
|
27
40
|
|
|
@@ -188,6 +201,8 @@ export function createEyePrologRegistry() {
|
|
|
188
201
|
registerCleanupBuiltins(registry);
|
|
189
202
|
eyePrologLibraryBuiltins.register(registry);
|
|
190
203
|
attsBuiltins.register(registry);
|
|
204
|
+
expansionBuiltins.register(registry);
|
|
205
|
+
scryerCompatibilityBuiltins.register(registry);
|
|
191
206
|
clpzBuiltins.register(registry);
|
|
192
207
|
registry.eyePrologLibrary = true;
|
|
193
208
|
return registry;
|
package/src/term.js
CHANGED
|
@@ -109,6 +109,9 @@ export class Env {
|
|
|
109
109
|
this._prologAttributes = null;
|
|
110
110
|
this._attributeHookRunner = null;
|
|
111
111
|
this._pendingAttributeGoals = null;
|
|
112
|
+
// Backtrackable blackboard entries used by Scryer-compatible libraries.
|
|
113
|
+
// Values are logical terms and the map is copy-on-write across Env clones.
|
|
114
|
+
this._backtrackableBlackboard = null;
|
|
112
115
|
this._clpz = null;
|
|
113
116
|
this._occursCheckHandler = null;
|
|
114
117
|
this._localVariables = null;
|
|
@@ -127,6 +130,7 @@ export class Env {
|
|
|
127
130
|
clone._prologAttributes = this._prologAttributes;
|
|
128
131
|
clone._attributeHookRunner = this._attributeHookRunner;
|
|
129
132
|
clone._pendingAttributeGoals = this._pendingAttributeGoals;
|
|
133
|
+
clone._backtrackableBlackboard = this._backtrackableBlackboard;
|
|
130
134
|
clone._clpz = this._clpz;
|
|
131
135
|
clone._occursCheckHandler = this._occursCheckHandler;
|
|
132
136
|
clone._localVariables = this._localVariables;
|
|
@@ -148,12 +152,21 @@ export class Env {
|
|
|
148
152
|
this._variableAnnotations = other._variableAnnotations;
|
|
149
153
|
this._prologAttributes = other._prologAttributes;
|
|
150
154
|
this._pendingAttributeGoals = other._pendingAttributeGoals;
|
|
155
|
+
this._backtrackableBlackboard = other._backtrackableBlackboard;
|
|
151
156
|
this._clpz = other._clpz;
|
|
152
157
|
// Execution callbacks belong to the Solver driving this Env, not to the
|
|
153
158
|
// logical branch being adopted from an inner attribute-hook call.
|
|
154
159
|
this._localVariables = other._localVariables;
|
|
155
160
|
return this;
|
|
156
161
|
}
|
|
162
|
+
getBacktrackableBlackboard(key) {
|
|
163
|
+
return this._backtrackableBlackboard?.get(key);
|
|
164
|
+
}
|
|
165
|
+
putBacktrackableBlackboard(key, value) {
|
|
166
|
+
const next = new Map(this._backtrackableBlackboard ?? []);
|
|
167
|
+
next.set(key, value);
|
|
168
|
+
this._backtrackableBlackboard = next;
|
|
169
|
+
}
|
|
157
170
|
hasLocalVariables() {
|
|
158
171
|
return this._localVariables != null && this._localVariables.size !== 0;
|
|
159
172
|
}
|