eyeprolog 1.3.67 → 1.3.68
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 +7 -5
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +12 -2
- package/src/CLPZ-MIGRATION.md +31 -11
- package/src/expansion-builtins.js +99 -0
- package/src/parser.js +26 -0
- package/src/program.js +73 -7
- package/src/solver.js +1 -1
- package/src/source-expansion.js +173 -0
- package/src/standard-library.js +2 -0
- package/test/run-regression.mjs +45 -1
- package/the-art-of-eyeprolog.md +9 -5
package/README.md
CHANGED
|
@@ -159,12 +159,14 @@ variable representatives and ordinary Prolog backtracking, and the REPL projects
|
|
|
159
159
|
module `attribute_goals//1` hooks as residual goals. See
|
|
160
160
|
[`examples/attributed-variables.pl`](examples/attributed-variables.pl).
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
The second migration stage toward reusing Markus Triska's MIT-licensed
|
|
163
163
|
[Scryer `library(clpz)`](https://github.com/mthom/scryer-prolog/blob/master/src/lib/clpz.pl)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
is now in place as well. Normal-profile loading supports user-defined
|
|
165
|
+
`term_expansion/2` and `goal_expansion/2`, clause-list generation, and
|
|
166
|
+
`expand_term/2` for processor DCG lowering, including the qualified hook shape
|
|
167
|
+
used by Scryer libraries. The current CLP(Z) implementation remains active while
|
|
168
|
+
the remaining Scryer support-library surface is brought across. The staged plan
|
|
169
|
+
and exact compatibility boundary are documented in
|
|
168
170
|
[`src/CLPZ-MIGRATION.md`](src/CLPZ-MIGRATION.md).
|
|
169
171
|
|
|
170
172
|
Recursion through negation is explicit. EyeProlog provides `tnot/1` for
|
package/package.json
CHANGED
package/src/ARCHITECTURE.md
CHANGED
|
@@ -7,8 +7,8 @@ higher-level frontends.
|
|
|
7
7
|
|
|
8
8
|
1. **Kernel representation and syntax** — `term.js`, `number-value.js`,
|
|
9
9
|
`syntax-scan.js`, `parser.js`, `write.js`, `errors.js`.
|
|
10
|
-
2. **Program preparation** — `program.js
|
|
11
|
-
`program-indexing.js`. Static recursion/Datalog/WFS classification lives in
|
|
10
|
+
2. **Program preparation** — `program.js`, `source-expansion.js`, plus
|
|
11
|
+
`program-analysis.js` and `program-indexing.js`. Static recursion/Datalog/WFS classification lives in
|
|
12
12
|
`program-analysis.js`; compact clauses and candidate indexes live in
|
|
13
13
|
`program-indexing.js`.
|
|
14
14
|
3. **Execution** — `solver.js`, `cleanup.js`, `io.js`, `datalog.js`, `wfs.js`,
|
|
@@ -48,6 +48,16 @@ needed by Scryer's `library(atts)` and `library(clpz)`. The current CLP(Z)-speci
|
|
|
48
48
|
`Env._clpz` store remains transitional until the Scryer Prolog implementation can
|
|
49
49
|
replace it without losing test or performance parity; see `src/CLPZ-MIGRATION.md`.
|
|
50
50
|
|
|
51
|
+
`source-expansion.js` is the explicit compile-time execution boundary. When an
|
|
52
|
+
already-loaded `term_expansion/2` or `goal_expansion/2` hook exists, program
|
|
53
|
+
preparation invokes it with a fresh bounded `Solver` against the partially built
|
|
54
|
+
program. `solver.js` therefore imports clause-selection primitives directly from
|
|
55
|
+
`program-indexing.js` rather than importing the `program.js` facade. This is the
|
|
56
|
+
one deliberate preparation-to-execution service edge and keeps the JavaScript
|
|
57
|
+
import graph acyclic while avoiding a second meta-interpreter solely for source
|
|
58
|
+
expansion. `expansion-builtins.js` provides the lower-level `expand_term/2` DCG
|
|
59
|
+
service without depending on `Solver`.
|
|
60
|
+
|
|
51
61
|
The JavaScript runtime stays flat directly under `src/`; the existing `src/lib/`
|
|
52
62
|
contains Prolog library sources rather than JavaScript runtime modules. The architecture
|
|
53
63
|
test rejects JavaScript import cycles under `src/`. Cleanup lifecycle hooks are
|
package/src/CLPZ-MIGRATION.md
CHANGED
|
@@ -32,20 +32,40 @@ attributes, post-binding goals, backtracking, `term_attributed_variables/2`, and
|
|
|
32
32
|
REPL projection through the conventional module `attribute_goals//1` hook.
|
|
33
33
|
`examples/attributed-variables.pl` is the runnable source-level example.
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## Stage 2: source expansion substrate
|
|
36
|
+
|
|
37
|
+
This stage is implemented. Normal-profile source loading now invokes already
|
|
38
|
+
loaded module-local and `user` `term_expansion/2` and `goal_expansion/2` hooks
|
|
39
|
+
left-to-right. A term expansion may yield one clause or a list of clauses, and
|
|
40
|
+
`expand_term/2` exposes the processor's DCG lowering so a hook can generate the
|
|
41
|
+
ordinary clauses used by Scryer's CLP(Z) parser generators. Goal expansion is
|
|
42
|
+
recursive across control constructs and preserves sharing with variables in the
|
|
43
|
+
surrounding clause.
|
|
44
|
+
|
|
45
|
+
Module-qualified hook clauses such as `user:goal_expansion/2` are also compiled
|
|
46
|
+
with the correct split between procedure ownership and lexical body context. A
|
|
47
|
+
hook defined from a library module therefore lives in `user` while its
|
|
48
|
+
unqualified helper calls still resolve in the defining library. The compiler
|
|
49
|
+
executes hooks with a fresh bounded solver against the partially built program;
|
|
50
|
+
`solver.js` imports clause-selection helpers directly from `program-indexing.js`,
|
|
51
|
+
so this compile-time service remains acyclic. Strict ISO Part 1 mode does not run
|
|
52
|
+
these extension hooks.
|
|
36
53
|
|
|
37
|
-
The
|
|
38
|
-
|
|
54
|
+
The regression corpus covers clause-list generation, variable-sharing goal
|
|
55
|
+
expansion, qualified hook lexical context, and the Scryer pattern where a
|
|
56
|
+
`term_expansion/2` hook calls `expand_term/2` to generate a DCG predicate.
|
|
57
|
+
|
|
58
|
+
## What still prevents a drop-in Scryer `clpz.pl`
|
|
39
59
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
The remaining compatibility work is the supporting Scryer library surface
|
|
61
|
+
(`assoc`, `pairs`, `between`, `dcgs`, `terms`, `error`, `si`, `freeze`,
|
|
62
|
+
`arithmetic`, `debug`, and `format`) at the interfaces expected by that source,
|
|
63
|
+
plus any small builtin gaps exposed while loading those modules. The generated
|
|
64
|
+
predicates and custom Duo DCG `++>` expansion are no longer VM/compiler
|
|
65
|
+
blockers: Scryer's own `term_expansion/2` can now perform that lowering.
|
|
46
66
|
|
|
47
|
-
These are compatibility gaps, not reasons to add more
|
|
48
|
-
`term.js` or `solver.js`.
|
|
67
|
+
These are reusable library compatibility gaps, not reasons to add more
|
|
68
|
+
CLP(Z)-specific code to `term.js` or `solver.js`.
|
|
49
69
|
|
|
50
70
|
## Migration rule
|
|
51
71
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Generic source-expansion helpers exposed to Prolog code.
|
|
2
|
+
//
|
|
3
|
+
// expand_term/2 provides the system part of source expansion that libraries
|
|
4
|
+
// such as Scryer's CLP(Z) use while generating predicates. User-defined
|
|
5
|
+
// term_expansion/2 and goal_expansion/2 are invoked by program preparation in
|
|
6
|
+
// source-expansion.js; keeping this builtin independent of Solver avoids an
|
|
7
|
+
// import cycle through the standard-library registry.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ATOM, COMPOUND, Env, atom, compound, deref, flattenConjunction, unify,
|
|
11
|
+
} from './term.js';
|
|
12
|
+
import { expandDcgRuleClause } from './dcg.js';
|
|
13
|
+
import { PrologError } from './errors.js';
|
|
14
|
+
|
|
15
|
+
function conjunctionFromGoals(goals) {
|
|
16
|
+
if (goals.length === 0) return atom('true');
|
|
17
|
+
let body = goals[goals.length - 1];
|
|
18
|
+
for (let index = goals.length - 2; index >= 0; index--) {
|
|
19
|
+
body = compound(',', [goals[index], body]);
|
|
20
|
+
}
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clauseToSourceTerm(clause) {
|
|
25
|
+
if (clause == null || clause.kind === 'quad') return null;
|
|
26
|
+
if (!Array.isArray(clause.body) || clause.body.length === 0) return clause.head;
|
|
27
|
+
return compound(':-', [clause.head, conjunctionFromGoals(clause.body)]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function sourceTermToClauses(term, env = new Env()) {
|
|
31
|
+
term = deref(term, env);
|
|
32
|
+
const list = sourceTermList(term, env);
|
|
33
|
+
if (list != null) {
|
|
34
|
+
const clauses = [];
|
|
35
|
+
for (const item of list) clauses.push(...sourceTermToClauses(item, env));
|
|
36
|
+
return clauses;
|
|
37
|
+
}
|
|
38
|
+
if (term.type === COMPOUND && term.name === ':-' && term.arity === 2) {
|
|
39
|
+
const head = deref(term.args[0], env);
|
|
40
|
+
const body = deref(term.args[1], env);
|
|
41
|
+
return [{ head, body: body.type === ATOM && body.name === 'true' ? [] : flattenConjunction(body) }];
|
|
42
|
+
}
|
|
43
|
+
if (term.type === COMPOUND && term.name === ':-' && term.arity === 1) {
|
|
44
|
+
return [{ head: term, body: [] }];
|
|
45
|
+
}
|
|
46
|
+
if (term.type === COMPOUND && term.name === '?-' && term.arity >= 1) {
|
|
47
|
+
return [{ head: term, body: [] }];
|
|
48
|
+
}
|
|
49
|
+
if (term.type !== ATOM && term.type !== COMPOUND) {
|
|
50
|
+
throw new PrologError('type_error(callable)', term);
|
|
51
|
+
}
|
|
52
|
+
return [{ head: term, body: [] }];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sourceTermList(term, env) {
|
|
56
|
+
const items = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
let cursor = deref(term, env);
|
|
59
|
+
while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
|
|
60
|
+
if (seen.has(cursor)) throw new PrologError('type_error(list)', term);
|
|
61
|
+
seen.add(cursor);
|
|
62
|
+
items.push(deref(cursor.args[0], env));
|
|
63
|
+
cursor = deref(cursor.args[1], env);
|
|
64
|
+
}
|
|
65
|
+
if (cursor.type === ATOM && cursor.name === '[]') return items;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function systemExpandTerm(term, module = 'user') {
|
|
70
|
+
const resolved = deref(term, new Env());
|
|
71
|
+
if (resolved.type === COMPOUND && resolved.name === '-->' && resolved.arity === 2) {
|
|
72
|
+
const expanded = expandDcgRuleClause({ head: resolved, body: [] }, module);
|
|
73
|
+
if (expanded == null) return resolved;
|
|
74
|
+
return clauseToSourceTerm(expanded);
|
|
75
|
+
}
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function* expandTermBuiltin({ goal, env }) {
|
|
80
|
+
const input = deref(goal.args[0], env);
|
|
81
|
+
if (input.type === 'var') throw new PrologError('instantiation_error');
|
|
82
|
+
const expanded = systemExpandTerm(input, goal.module ?? 'user');
|
|
83
|
+
const next = env.clone();
|
|
84
|
+
if (unify(goal.args[1], expanded, next)) yield next;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function* expandGoalBuiltin({ goal, env }) {
|
|
88
|
+
const input = deref(goal.args[0], env);
|
|
89
|
+
if (input.type === 'var') throw new PrologError('instantiation_error');
|
|
90
|
+
const next = env.clone();
|
|
91
|
+
if (unify(goal.args[1], input, next)) yield next;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const expansionBuiltins = {
|
|
95
|
+
register(registry) {
|
|
96
|
+
registry.add('expand_term', 2, expandTermBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
97
|
+
registry.add('expand_goal', 2, expandGoalBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
98
|
+
},
|
|
99
|
+
};
|
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];
|
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,6 +5,7 @@
|
|
|
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';
|
|
8
9
|
import { registerCleanupBuiltins } from './cleanup.js';
|
|
9
10
|
import { fs, isNode, memoryStatistics } from './platform.js';
|
|
10
11
|
import { ATOM, VAR, atom, deref, numberTerm, unify } from './term.js';
|
|
@@ -188,6 +189,7 @@ export function createEyePrologRegistry() {
|
|
|
188
189
|
registerCleanupBuiltins(registry);
|
|
189
190
|
eyePrologLibraryBuiltins.register(registry);
|
|
190
191
|
attsBuiltins.register(registry);
|
|
192
|
+
expansionBuiltins.register(registry);
|
|
191
193
|
clpzBuiltins.register(registry);
|
|
192
194
|
registry.eyePrologLibrary = true;
|
|
193
195
|
return registry;
|
package/test/run-regression.mjs
CHANGED
|
@@ -185,6 +185,50 @@ probe.
|
|
|
185
185
|
'Scryer-style operator exports and multi-attribute directives load');
|
|
186
186
|
},
|
|
187
187
|
},
|
|
188
|
+
{
|
|
189
|
+
name: 'user source expansion supports Scryer-style generated predicates and DCGs',
|
|
190
|
+
run: () => {
|
|
191
|
+
const termSource = `term_expansion(make_generated, [generated(one),generated(two)]).
|
|
192
|
+
make_generated.
|
|
193
|
+
`;
|
|
194
|
+
const generated = Program.parse(termSource, { sourceMetadata: false });
|
|
195
|
+
assertEqual(generated.findGroup('generated', 1, 'user')?.clauses.length, 2,
|
|
196
|
+
'term_expansion/2 may emit a list of clauses');
|
|
197
|
+
assertEqual(generated.findGroup('make_generated', 0, 'user'), null,
|
|
198
|
+
'the source marker is replaced by its expansion');
|
|
199
|
+
|
|
200
|
+
const dcgSource = `term_expansion(make_dcg, Clause) :-
|
|
201
|
+
expand_term((generated_nt --> [a]), Clause).
|
|
202
|
+
make_dcg.
|
|
203
|
+
check :- phrase(generated_nt, [a]).
|
|
204
|
+
`;
|
|
205
|
+
assertIncludes(run(dcgSource, { goal: 'check' }).stdout, 'check.',
|
|
206
|
+
'expand_term/2 exposes system DCG expansion to a source hook');
|
|
207
|
+
|
|
208
|
+
const duoSource = `:- op(1200, xfx, ++>).
|
|
209
|
+
term_expansion((Head ++> Body), (Head :- Body)).
|
|
210
|
+
generated_duo(ok) ++> true.
|
|
211
|
+
`;
|
|
212
|
+
assertEqual(run(duoSource, { goal: 'generated_duo(ok)' }).stats.completed_goal_lists, 1,
|
|
213
|
+
'custom Duo-DCG-style operators can be lowered by term_expansion/2');
|
|
214
|
+
|
|
215
|
+
const goalSource = `:- module(expansion_probe, [check/0]).
|
|
216
|
+
helper(ok).
|
|
217
|
+
user:goal_expansion(old(X), new(X)) :- helper(ok).
|
|
218
|
+
new(ok).
|
|
219
|
+
check :- old(X), X = ok.
|
|
220
|
+
`;
|
|
221
|
+
const goalProgram = Program.parse(goalSource, { sourceMetadata: false });
|
|
222
|
+
const check = goalProgram.findGroup('check', 0, 'expansion_probe')?.clauses[0];
|
|
223
|
+
assertEqual(check?.body[0]?.name, 'new', 'goal_expansion/2 rewrites a later source goal');
|
|
224
|
+
assertEqual(check?.body[0]?.args[0]?.name, check?.body[1]?.args[0]?.name,
|
|
225
|
+
'goal expansion preserves sharing with the surrounding clause');
|
|
226
|
+
assertEqual(goalProgram.findGroup('goal_expansion', 2, 'user')?.clauses[0]?.body[0]?.module,
|
|
227
|
+
'expansion_probe', 'qualified hook clauses retain their lexical body module');
|
|
228
|
+
assertIncludes(run(goalSource, { goal: 'expansion_probe:check' }).stdout, 'expansion_probe:check.',
|
|
229
|
+
'a user hook defined by a library module executes in its lexical module');
|
|
230
|
+
},
|
|
231
|
+
},
|
|
188
232
|
{
|
|
189
233
|
name: 'write_term variable_names/1 errors preserve the instantiated option culprit (issue #69)',
|
|
190
234
|
run: () => {
|
|
@@ -4785,7 +4829,7 @@ answer(ok) :-
|
|
|
4785
4829
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
4786
4830
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
4787
4831
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
4788
|
-
assertEqual(library.defs.size,
|
|
4832
|
+
assertEqual(library.defs.size, 173, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and private library adapters');
|
|
4789
4833
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
4790
4834
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
4791
4835
|
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6424,11 +6424,15 @@ Hamiltonian circuits, three-way comparison, and domain reflection. Its existing
|
|
|
6424
6424
|
`src/clpz.js` store is now explicitly transitional: the attributed-variable layer
|
|
6425
6425
|
is intended to support staged reuse of Markus Triska's MIT-licensed
|
|
6426
6426
|
[Scryer `clpz.pl`](https://github.com/mthom/scryer-prolog/blob/master/src/lib/clpz.pl).
|
|
6427
|
-
The
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6427
|
+
The generic compile-time expansion blocker is now closed: normal-profile source
|
|
6428
|
+
loading runs module-local and `user` `term_expansion/2` and `goal_expansion/2`
|
|
6429
|
+
hooks, accepts clause-list expansions, preserves surrounding variable sharing,
|
|
6430
|
+
and provides `expand_term/2` for processor DCG lowering. This is sufficient for
|
|
6431
|
+
Scryer's generated-predicate and Duo-DCG expansion strategy. The remaining
|
|
6432
|
+
blocker is the supporting Scryer library surface used by that source; the
|
|
6433
|
+
migration state is tracked in [`src/CLPZ-MIGRATION.md`](src/CLPZ-MIGRATION.md).
|
|
6434
|
+
Until parity is reached, the JS kernel remains the active implementation so
|
|
6435
|
+
existing CLP(Z) behavior does not regress.
|
|
6432
6436
|
|
|
6433
6437
|
Alongside its interop entries `call_nth/2`, `time/1`, and `.../2`,
|
|
6434
6438
|
`library(iso_ext)` also exports EyeProlog's extension relations `countall/2`,
|