eyeprolog 1.3.66 → 1.3.67
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 +27 -6
- package/examples/attributed-variables.pl +40 -0
- package/examples/output/attributed-variables.pl +2 -0
- package/package.json +1 -1
- package/playground.html +1 -0
- package/src/ARCHITECTURE.md +12 -2
- package/src/CLPZ-MIGRATION.md +61 -0
- package/src/atts.js +224 -0
- package/src/lib/atts.pl +20 -0
- package/src/parser.js +21 -6
- package/src/program.js +14 -1
- package/src/repl.js +18 -0
- package/src/solver.js +80 -2
- package/src/standard-library.js +5 -1
- package/src/term.js +197 -0
- package/test/run-regression.mjs +66 -7
- package/the-art-of-eyeprolog.md +26 -10
package/README.md
CHANGED
|
@@ -147,6 +147,26 @@ are removed, and violated constraints make unification fail. The interactive
|
|
|
147
147
|
top level includes pending `dif/2` goals in residual answers. See
|
|
148
148
|
[`examples/dif-constraints.pl`](examples/dif-constraints.pl).
|
|
149
149
|
|
|
150
|
+
### Attributed variables and the CLP(Z) migration
|
|
151
|
+
|
|
152
|
+
`library(atts)` now exposes Scryer-style attributed-variable operations on the
|
|
153
|
+
same persistent `Env` machinery introduced for `dif/2`: `put_atts/2`,
|
|
154
|
+
`get_atts/2`, `put_attr/3`, `get_attr/3`, `del_attr/2`, and
|
|
155
|
+
`term_attributed_variables/2`. `:- attribute ...` declarations are accepted,
|
|
156
|
+
`verify_attributes/3` is invoked before an attributed binding is committed, and
|
|
157
|
+
the hook's returned goals run after that binding. Attribute state follows
|
|
158
|
+
variable representatives and ordinary Prolog backtracking, and the REPL projects
|
|
159
|
+
module `attribute_goals//1` hooks as residual goals. See
|
|
160
|
+
[`examples/attributed-variables.pl`](examples/attributed-variables.pl).
|
|
161
|
+
|
|
162
|
+
This is the first migration stage toward reusing Markus Triska's MIT-licensed
|
|
163
|
+
[Scryer `library(clpz)`](https://github.com/mthom/scryer-prolog/blob/master/src/lib/clpz.pl)
|
|
164
|
+
instead of maintaining a separate constraint solver in `src/clpz.js`. The
|
|
165
|
+
current CLP(Z) implementation remains active while compatibility gaps such as
|
|
166
|
+
Scryer's compile-time term/goal expansion and supporting libraries are closed.
|
|
167
|
+
The staged plan and exact compatibility boundary are documented in
|
|
168
|
+
[`src/CLPZ-MIGRATION.md`](src/CLPZ-MIGRATION.md).
|
|
169
|
+
|
|
150
170
|
Recursion through negation is explicit. EyeProlog provides `tnot/1` for
|
|
151
171
|
well-founded negation over finite, range-restricted, function-free Datalog
|
|
152
172
|
components. Ordinary `\+/1` remains negation-as-failure and is not silently
|
|
@@ -275,13 +295,14 @@ be continued without rebuilding a full clause-resolution frame for every
|
|
|
275
295
|
suffix. Open and remainder-producing uses still retain the ordinary DCG
|
|
276
296
|
solutions and backtracking behavior.
|
|
277
297
|
|
|
278
|
-
EyeProlog also adds
|
|
279
|
-
profile. **
|
|
280
|
-
modules under `src/lib/`; the remaining control predicates
|
|
281
|
-
`library(clpz)` kernel use
|
|
298
|
+
EyeProlog also adds 135 public library predicate indicators to its 129-entry ISO
|
|
299
|
+
profile. **89 are implemented entirely as ordinary Prolog clauses** in focused
|
|
300
|
+
modules under `src/lib/`; the remaining control predicates, attributed-variable
|
|
301
|
+
primitives, and the transitional finite-domain `library(clpz)` kernel use
|
|
302
|
+
backtrackable host support.
|
|
282
303
|
They are ordinary Prolog modules using EyeProlog's documented module compatibility surface, loaded explicitly by purpose, such as
|
|
283
|
-
`library(lists)`, `library(lambda)`, `library(strings)`, `library(aggregate)`,
|
|
284
|
-
`library(clpz)`.
|
|
304
|
+
`library(lists)`, `library(lambda)`, `library(strings)`, `library(aggregate)`,
|
|
305
|
+
`library(atts)`, or `library(clpz)`.
|
|
285
306
|
Portable text
|
|
286
307
|
predicates use ISO atoms or character lists. The old catch-all
|
|
287
308
|
`library(eyeprolog)` module is no longer needed.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
% Scryer-style attributed variables on EyeProlog's annotated-variable runtime.
|
|
2
|
+
%
|
|
3
|
+
% The attribute declaration is intentionally the same shape used by
|
|
4
|
+
% library(atts) clients such as Scryer's CLP(Z). verify_attributes/3 runs before
|
|
5
|
+
% an attributed variable is bound; goals it returns run after the binding.
|
|
6
|
+
|
|
7
|
+
:- use_module(library(atts)).
|
|
8
|
+
:- attribute required/1.
|
|
9
|
+
|
|
10
|
+
%% goal: accepts_required_value(ok)
|
|
11
|
+
%% goal: alias_preserves_attribute(ok)
|
|
12
|
+
|
|
13
|
+
attach_required(Variable, Value) :-
|
|
14
|
+
put_atts(Variable, required(Value)).
|
|
15
|
+
|
|
16
|
+
verify_attributes(Variable, Other, Goals) :-
|
|
17
|
+
( get_atts(Variable, required(Value)) ->
|
|
18
|
+
( var(Other) ->
|
|
19
|
+
put_atts(Other, required(Value)),
|
|
20
|
+
Goals = []
|
|
21
|
+
; Goals = [required_value(Other, Value)] )
|
|
22
|
+
; Goals = [] ).
|
|
23
|
+
|
|
24
|
+
required_value(Value, Value).
|
|
25
|
+
|
|
26
|
+
accepts_required_value(ok) :-
|
|
27
|
+
attach_required(X, 7),
|
|
28
|
+
X = 7.
|
|
29
|
+
|
|
30
|
+
alias_preserves_attribute(ok) :-
|
|
31
|
+
attach_required(X, ready),
|
|
32
|
+
X = Y,
|
|
33
|
+
get_atts(Y, required(ready)),
|
|
34
|
+
Y = ready.
|
|
35
|
+
|
|
36
|
+
% The REPL uses the conventional attribute_goals//1 hook to project remaining
|
|
37
|
+
% attributes as residual goals.
|
|
38
|
+
attribute_goals(X) -->
|
|
39
|
+
{ get_atts(X, required(Value)) },
|
|
40
|
+
[required(X, Value)].
|
package/package.json
CHANGED
package/playground.html
CHANGED
package/src/ARCHITECTURE.md
CHANGED
|
@@ -15,7 +15,7 @@ higher-level frontends.
|
|
|
15
15
|
`clpz.js`. `cleanup.js` owns lifecycle-aware disposal of protected builtin
|
|
16
16
|
iterators and registers the normal-profile cleanup controls without making
|
|
17
17
|
`solver.js` depend back on the language registry.
|
|
18
|
-
4. **Language services** — `iso.js`, `iso-arithmetic.js`, `dcg.js`,
|
|
18
|
+
4. **Language services** — `iso.js`, `iso-arithmetic.js`, `dcg.js`, `atts.js`,
|
|
19
19
|
`standard-library.js`, and `src/lib/`.
|
|
20
20
|
5. **Frontends/tools** — `execute.js`, `repl.js`, `cli.js`, `quads.js`,
|
|
21
21
|
`explain.js`, and the playground worker.
|
|
@@ -36,7 +36,17 @@ Annotations are persistent across `Env.clone()` and therefore backtrack with the
|
|
|
36
36
|
substitution. Language services may attach immutable constraint descriptors,
|
|
37
37
|
but the unconstrained unification hot path only performs a null check; descriptor
|
|
38
38
|
validation and reindexing run only in environments that actually contain
|
|
39
|
-
annotations. `dif/2` is the first user of this mechanism.
|
|
39
|
+
annotations. `dif/2` is the first descriptor-based user of this mechanism.
|
|
40
|
+
|
|
41
|
+
`atts.js` layers Prolog-visible attributed variables over that same persistent
|
|
42
|
+
environment. Per-module attributes follow the current variable representative.
|
|
43
|
+
When an attributed variable is about to be bound, the solver runs that module's
|
|
44
|
+
`verify_attributes/3` against the still-unbound representative; only a successful
|
|
45
|
+
hook permits the binding, and any goals returned in the third argument are queued
|
|
46
|
+
immediately after it. This pre-bind/post-bind split is the compatibility boundary
|
|
47
|
+
needed by Scryer's `library(atts)` and `library(clpz)`. The current CLP(Z)-specific
|
|
48
|
+
`Env._clpz` store remains transitional until the Scryer Prolog implementation can
|
|
49
|
+
replace it without losing test or performance parity; see `src/CLPZ-MIGRATION.md`.
|
|
40
50
|
|
|
41
51
|
The JavaScript runtime stays flat directly under `src/`; the existing `src/lib/`
|
|
42
52
|
contains Prolog library sources rather than JavaScript runtime modules. The architecture
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# CLP(Z) migration toward Scryer's Prolog implementation
|
|
2
|
+
|
|
3
|
+
EyeProlog's current `library(clpz)` has a Prolog API in `src/lib/clpz.pl` and a
|
|
4
|
+
CLP(Z)-specific propagation store in `src/clpz.js`. The long-term target is to
|
|
5
|
+
reuse Markus Triska's MIT-licensed [`clpz.pl` from Scryer
|
|
6
|
+
Prolog](https://github.com/mthom/scryer-prolog/blob/master/src/lib/clpz.pl) and
|
|
7
|
+
keep the JavaScript runtime responsible for generic Prolog VM facilities rather
|
|
8
|
+
than a second finite-domain solver.
|
|
9
|
+
|
|
10
|
+
## Stage 1: attributed-variable substrate
|
|
11
|
+
|
|
12
|
+
This stage is implemented. `Env` carries persistent per-module Prolog
|
|
13
|
+
attributes in addition to the generic constraint descriptors introduced for
|
|
14
|
+
`dif/2`. `src/atts.js` and `library(atts)` expose the operations required by the
|
|
15
|
+
Scryer style: `put_atts/2`, `get_atts/2`, `put_attr/3`, `get_attr/3`,
|
|
16
|
+
`del_attr/2`, and `term_attributed_variables/2`. The parser accepts the
|
|
17
|
+
`attribute` declaration operator, Scryer-style comma-separated
|
|
18
|
+
`:- attribute a/1, b/0.` directives, and `op/3` entries in module export lists.
|
|
19
|
+
|
|
20
|
+
The important unification protocol is also implemented. Before an attributed
|
|
21
|
+
variable is bound to a nonvariable, or before two variables carrying attributes
|
|
22
|
+
for the same owner module are aliased, the solver invokes the owner's
|
|
23
|
+
`verify_attributes/3`. A failed hook rejects the unification. Goals returned in
|
|
24
|
+
the third argument are queued only after the binding succeeds. Aliasing an
|
|
25
|
+
attributed variable with a plain variable transfers the persistent attribute map
|
|
26
|
+
to the surviving representative, which is equivalent to Scryer's CLP(Z)
|
|
27
|
+
`verify_attributes/3` transfer branch and avoids a recursive hook call while
|
|
28
|
+
entering the hook itself.
|
|
29
|
+
|
|
30
|
+
The substrate is covered by regressions for lookup, alias transfer, conflicting
|
|
31
|
+
attributes, post-binding goals, backtracking, `term_attributed_variables/2`, and
|
|
32
|
+
REPL projection through the conventional module `attribute_goals//1` hook.
|
|
33
|
+
`examples/attributed-variables.pl` is the runnable source-level example.
|
|
34
|
+
|
|
35
|
+
## What still prevents a drop-in Scryer `clpz.pl`
|
|
36
|
+
|
|
37
|
+
The upstream solver is not only an attributed-variable client. It also relies on
|
|
38
|
+
compile-time facilities that EyeProlog does not yet provide generically:
|
|
39
|
+
|
|
40
|
+
- user-defined `term_expansion/2` and `goal_expansion/2` during module loading;
|
|
41
|
+
- the generated predicates at the end of Scryer's `clpz.pl` and its custom Duo
|
|
42
|
+
DCG `++>` expansion;
|
|
43
|
+
- the supporting Scryer library surface (`assoc`, `pairs`, `between`, `dcgs`,
|
|
44
|
+
`terms`, `error`, `si`, `freeze`, `arithmetic`, `debug`, and `format`) at the
|
|
45
|
+
interfaces expected by that source;
|
|
46
|
+
|
|
47
|
+
These are compatibility gaps, not reasons to add more CLP(Z)-specific code to
|
|
48
|
+
`term.js` or `solver.js`.
|
|
49
|
+
|
|
50
|
+
## Migration rule
|
|
51
|
+
|
|
52
|
+
`src/clpz.js` remains the active implementation until an upstream-based Prolog
|
|
53
|
+
solver passes the existing EyeProlog CLP(Z) corpus and the selected Scryer CLP(Z)
|
|
54
|
+
regressions. During the transition it is the behavioral reference, not the
|
|
55
|
+
architecture to extend. New generic VM work should go into attributed variables,
|
|
56
|
+
term/goal expansion, or reusable library support where possible.
|
|
57
|
+
|
|
58
|
+
Once the upstream source loads and reaches parity, remove the corresponding
|
|
59
|
+
`eyeprolog__clpz_*` adapters and the `_clpz` store in one or more reviewable
|
|
60
|
+
steps. Keep the upstream copyright and MIT license notice intact and keep local
|
|
61
|
+
patches small enough that future Scryer updates can be diffed mechanically.
|
package/src/atts.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Generic attributed-variable bridge for Prolog libraries.
|
|
2
|
+
//
|
|
3
|
+
// The storage is owned by Env so attributes backtrack with substitutions. This
|
|
4
|
+
// module exposes the small atts interface used by Scryer libraries. Attribute
|
|
5
|
+
// verification itself is scheduled by term.js and executed by Solver before a
|
|
6
|
+
// binding is committed; goals returned by verify_attributes/3 run immediately
|
|
7
|
+
// after the binding.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ATOM, COMPOUND, VAR, atom, compound, deref, listFromItems,
|
|
11
|
+
properListItems, unify, variable,
|
|
12
|
+
} from './term.js';
|
|
13
|
+
import { PrologError } from './errors.js';
|
|
14
|
+
|
|
15
|
+
function callerModule(goal) {
|
|
16
|
+
return goal.module ?? 'user';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function attributeSignature(term) {
|
|
20
|
+
if (term.type === ATOM) return { name: term.name, arity: 0 };
|
|
21
|
+
if (term.type === COMPOUND) return { name: term.name, arity: term.arity };
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizedAttribute(term, env) {
|
|
26
|
+
let resolved = deref(term, env);
|
|
27
|
+
let mode = 'put';
|
|
28
|
+
if (resolved.type === COMPOUND && resolved.arity === 1 && (resolved.name === '+' || resolved.name === '-')) {
|
|
29
|
+
mode = resolved.name === '-' ? 'delete' : 'put';
|
|
30
|
+
resolved = deref(resolved.args[0], env);
|
|
31
|
+
}
|
|
32
|
+
if (resolved.type === VAR) throw new PrologError('instantiation_error');
|
|
33
|
+
const signature = attributeSignature(resolved);
|
|
34
|
+
if (signature == null) throw new PrologError('type_error(callable)', resolved);
|
|
35
|
+
return { mode, attribute: resolved, ...signature };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function putOne(next, variableTerm, owner, attributeTerm) {
|
|
39
|
+
const variableValue = deref(variableTerm, next);
|
|
40
|
+
if (variableValue.type !== VAR) throw new PrologError('uninstantiation_error', variableValue);
|
|
41
|
+
const normalized = normalizedAttribute(attributeTerm, next);
|
|
42
|
+
if (normalized.mode === 'delete') {
|
|
43
|
+
next.deletePrologAttribute(variableValue.name, owner, normalized.name, normalized.arity);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
next.putPrologAttribute(variableValue.name, owner, normalized.attribute);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function* putAttsBuiltin({ goal, env }) {
|
|
50
|
+
const next = env.clone();
|
|
51
|
+
const owner = callerModule(goal);
|
|
52
|
+
const variableTerm = deref(goal.args[0], next);
|
|
53
|
+
if (variableTerm.type !== VAR) throw new PrologError('uninstantiation_error', variableTerm);
|
|
54
|
+
const list = properListItems(goal.args[1], next);
|
|
55
|
+
if (list != null) {
|
|
56
|
+
for (const attribute of list) putOne(next, variableTerm, owner, attribute);
|
|
57
|
+
} else {
|
|
58
|
+
putOne(next, variableTerm, owner, goal.args[1]);
|
|
59
|
+
}
|
|
60
|
+
yield next;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function* getAttsBuiltin({ goal, env }) {
|
|
64
|
+
const variableTerm = deref(goal.args[0], env);
|
|
65
|
+
if (variableTerm.type !== VAR) throw new PrologError('uninstantiation_error', variableTerm);
|
|
66
|
+
const owner = callerModule(goal);
|
|
67
|
+
const pattern = deref(goal.args[1], env);
|
|
68
|
+
const attributes = env.prologAttributes(variableTerm.name, owner);
|
|
69
|
+
if (pattern.type === VAR) {
|
|
70
|
+
if (attributes.length === 0) return;
|
|
71
|
+
const next = env.clone();
|
|
72
|
+
if (unify(goal.args[1], listFromItems(attributes), next)) yield next;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let mode = 'present';
|
|
77
|
+
let inner = pattern;
|
|
78
|
+
if (pattern.type === COMPOUND && pattern.arity === 1 && (pattern.name === '+' || pattern.name === '-')) {
|
|
79
|
+
mode = pattern.name === '-' ? 'absent' : 'present';
|
|
80
|
+
inner = deref(pattern.args[0], env);
|
|
81
|
+
}
|
|
82
|
+
if (inner.type === VAR) throw new PrologError('instantiation_error');
|
|
83
|
+
const signature = attributeSignature(inner);
|
|
84
|
+
if (signature == null) throw new PrologError('type_error(callable)', inner);
|
|
85
|
+
const stored = env.getPrologAttribute(variableTerm.name, owner, signature.name, signature.arity);
|
|
86
|
+
if (mode === 'absent') {
|
|
87
|
+
if (stored == null) yield env.clone();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (stored == null) return;
|
|
91
|
+
const next = env.clone();
|
|
92
|
+
if (unify(inner, stored, next, { skipVariableConstraints: true })) yield next;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function attributeName(term, env) {
|
|
96
|
+
const resolved = deref(term, env);
|
|
97
|
+
if (resolved.type === VAR) throw new PrologError('instantiation_error');
|
|
98
|
+
if (resolved.type !== ATOM) throw new PrologError('type_error(atom)', resolved);
|
|
99
|
+
return resolved.name;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function* putAttrBuiltin({ goal, env }) {
|
|
103
|
+
const next = env.clone();
|
|
104
|
+
const owner = callerModule(goal);
|
|
105
|
+
const variableTerm = deref(goal.args[0], next);
|
|
106
|
+
if (variableTerm.type !== VAR) throw new PrologError('uninstantiation_error', variableTerm);
|
|
107
|
+
const name = attributeName(goal.args[1], next);
|
|
108
|
+
next.putPrologAttribute(variableTerm.name, owner, compound(name, [goal.args[2]]));
|
|
109
|
+
yield next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function* getAttrBuiltin({ goal, env }) {
|
|
113
|
+
const variableTerm = deref(goal.args[0], env);
|
|
114
|
+
if (variableTerm.type !== VAR) return;
|
|
115
|
+
const owner = callerModule(goal);
|
|
116
|
+
const name = attributeName(goal.args[1], env);
|
|
117
|
+
const stored = env.getPrologAttribute(variableTerm.name, owner, name, 1);
|
|
118
|
+
if (stored == null) return;
|
|
119
|
+
const next = env.clone();
|
|
120
|
+
if (unify(goal.args[2], stored.args[0], next, { skipVariableConstraints: true })) yield next;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function* delAttrBuiltin({ goal, env }) {
|
|
124
|
+
const next = env.clone();
|
|
125
|
+
const variableTerm = deref(goal.args[0], next);
|
|
126
|
+
if (variableTerm.type !== VAR) { yield next; return; }
|
|
127
|
+
const owner = callerModule(goal);
|
|
128
|
+
const name = attributeName(goal.args[1], next);
|
|
129
|
+
next.deletePrologAttribute(variableTerm.name, owner, name, null);
|
|
130
|
+
yield next;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function collectTermVariables(term, env) {
|
|
134
|
+
const result = [];
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
const stack = [term];
|
|
137
|
+
while (stack.length) {
|
|
138
|
+
const current = deref(stack.pop(), env);
|
|
139
|
+
if (current.type === VAR) {
|
|
140
|
+
if (!seen.has(current.name)) {
|
|
141
|
+
seen.add(current.name);
|
|
142
|
+
result.push(current);
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (current.type === COMPOUND) {
|
|
147
|
+
for (let i = current.arity - 1; i >= 0; i--) stack.push(current.args[i]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function* termAttributedVariablesBuiltin({ goal, env }) {
|
|
154
|
+
const variables = collectTermVariables(goal.args[0], env).filter((term) => env.hasPrologAttributes(term.name));
|
|
155
|
+
const next = env.clone();
|
|
156
|
+
if (unify(goal.args[1], listFromItems(variables), next)) yield next;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function* getAttrListBuiltin({ goal, env }) {
|
|
160
|
+
const variableTerm = deref(goal.args[0], env);
|
|
161
|
+
if (variableTerm.type !== VAR) return;
|
|
162
|
+
const entries = env.prologAttributes(variableTerm.name);
|
|
163
|
+
if (entries.length === 0) return;
|
|
164
|
+
const encoded = entries.map(({ module, attribute }) => compound(':', [atom(module), attribute]));
|
|
165
|
+
const next = env.clone();
|
|
166
|
+
if (unify(goal.args[1], listFromItems(encoded), next)) yield next;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function* getFromAttrListBuiltin({ goal, env }) {
|
|
170
|
+
const variableTerm = deref(goal.args[0], env);
|
|
171
|
+
if (variableTerm.type !== VAR) return;
|
|
172
|
+
const moduleTerm = deref(goal.args[1], env);
|
|
173
|
+
if (moduleTerm.type === VAR) throw new PrologError('instantiation_error');
|
|
174
|
+
if (moduleTerm.type !== ATOM) throw new PrologError('type_error(atom)', moduleTerm);
|
|
175
|
+
const pattern = deref(goal.args[2], env);
|
|
176
|
+
if (pattern.type === VAR) throw new PrologError('instantiation_error');
|
|
177
|
+
const signature = attributeSignature(pattern);
|
|
178
|
+
if (signature == null) throw new PrologError('type_error(callable)', pattern);
|
|
179
|
+
const stored = env.getPrologAttribute(variableTerm.name, moduleTerm.name, signature.name, signature.arity);
|
|
180
|
+
if (stored == null) return;
|
|
181
|
+
const next = env.clone();
|
|
182
|
+
if (unify(pattern, stored, next, { skipVariableConstraints: true })) yield next;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function* putToAttrListBuiltin({ goal, env }) {
|
|
186
|
+
const next = env.clone();
|
|
187
|
+
const variableTerm = deref(goal.args[0], next);
|
|
188
|
+
if (variableTerm.type !== VAR) throw new PrologError('uninstantiation_error', variableTerm);
|
|
189
|
+
const moduleTerm = deref(goal.args[1], next);
|
|
190
|
+
if (moduleTerm.type === VAR) throw new PrologError('instantiation_error');
|
|
191
|
+
if (moduleTerm.type !== ATOM) throw new PrologError('type_error(atom)', moduleTerm);
|
|
192
|
+
const normalized = normalizedAttribute(goal.args[2], next);
|
|
193
|
+
next.putPrologAttribute(variableTerm.name, moduleTerm.name, normalized.attribute);
|
|
194
|
+
yield next;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function* delFromAttrListBuiltin({ goal, env }) {
|
|
198
|
+
const next = env.clone();
|
|
199
|
+
const variableTerm = deref(goal.args[0], next);
|
|
200
|
+
if (variableTerm.type !== VAR) { yield next; return; }
|
|
201
|
+
const moduleTerm = deref(goal.args[1], next);
|
|
202
|
+
if (moduleTerm.type === VAR) throw new PrologError('instantiation_error');
|
|
203
|
+
if (moduleTerm.type !== ATOM) throw new PrologError('type_error(atom)', moduleTerm);
|
|
204
|
+
const normalized = normalizedAttribute(goal.args[2], next);
|
|
205
|
+
next.deletePrologAttribute(variableTerm.name, moduleTerm.name, normalized.name, normalized.arity);
|
|
206
|
+
yield next;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const attsBuiltins = {
|
|
210
|
+
register(registry) {
|
|
211
|
+
registry.add('put_atts', 2, putAttsBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
212
|
+
registry.add('get_atts', 2, getAttsBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
213
|
+
// Scryer's CLP(Z) sources use these through goal expansion. EyeProlog
|
|
214
|
+
// provides them directly so the source does not depend on goal_expansion/2.
|
|
215
|
+
registry.add('put_attr', 3, putAttrBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
216
|
+
registry.add('get_attr', 3, getAttrBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
217
|
+
registry.add('del_attr', 2, delAttrBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
218
|
+
registry.add('$term_attributed_variables', 2, termAttributedVariablesBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
219
|
+
registry.add('$get_attr_list', 2, getAttrListBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
220
|
+
registry.add('$get_from_attr_list', 3, getFromAttrListBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
221
|
+
registry.add('$put_to_attr_list', 3, putToAttrListBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
222
|
+
registry.add('$del_from_attr_list', 3, delFromAttrListBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
223
|
+
},
|
|
224
|
+
};
|
package/src/lib/atts.pl
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* Attributed variables compatibility layer.
|
|
2
|
+
|
|
3
|
+
The public surface follows Scryer Prolog library(atts). EyeProlog keeps the
|
|
4
|
+
actual attribute store and verify_attributes/3 scheduling in the generic
|
|
5
|
+
annotated-variable runtime, so libraries can use put_atts/2 and get_atts/2
|
|
6
|
+
without code-generation term expansion.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
:- module(atts, [
|
|
10
|
+
put_atts/2,
|
|
11
|
+
get_atts/2,
|
|
12
|
+
put_attr/3,
|
|
13
|
+
get_attr/3,
|
|
14
|
+
del_attr/2,
|
|
15
|
+
term_attributed_variables/2
|
|
16
|
+
]).
|
|
17
|
+
:- op(1199, fx, attribute).
|
|
18
|
+
|
|
19
|
+
term_attributed_variables(Term, Variables) :-
|
|
20
|
+
'$term_attributed_variables'(Term, Variables).
|
package/src/parser.js
CHANGED
|
@@ -372,9 +372,15 @@ class Parser {
|
|
|
372
372
|
if (directive.type !== COMPOUND || directive.name !== 'use_module' || ![1, 2].includes(directive.arity)) return;
|
|
373
373
|
const designation = directive.args[0];
|
|
374
374
|
if (designation?.type !== COMPOUND || designation.name !== 'library' || designation.arity !== 1 ||
|
|
375
|
-
designation.args[0]?.type !== ATOM
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
designation.args[0]?.type !== ATOM) return;
|
|
376
|
+
if (designation.args[0].name === 'clpz') {
|
|
377
|
+
for (const [priority, specifier, name] of CLPZ_OPERATOR_DEFINITIONS) {
|
|
378
|
+
this.defineOperator(priority, specifier, name);
|
|
379
|
+
}
|
|
380
|
+
} else if (designation.args[0].name === 'atts') {
|
|
381
|
+
// Scryer library(atts) exports this declaration operator. EyeProlog
|
|
382
|
+
// handles the directive directly instead of relying on term expansion.
|
|
383
|
+
this.defineOperator(1199, 'fx', 'attribute');
|
|
378
384
|
}
|
|
379
385
|
}
|
|
380
386
|
operatorTokenName(token = this.token) {
|
|
@@ -1020,13 +1026,22 @@ class Parser {
|
|
|
1020
1026
|
}
|
|
1021
1027
|
if (this.token.type === TOK.IF) {
|
|
1022
1028
|
this.advance();
|
|
1023
|
-
const
|
|
1029
|
+
const attributeSequence = !this.strictIso && this.operatorTokenName() === 'attribute';
|
|
1030
|
+
let directive = this.parseTerm(0, attributeSequence);
|
|
1031
|
+
// Scryer's library(atts) convention permits declarations such as
|
|
1032
|
+
// `:- attribute a/1, b/0.` without parentheses. At program level the
|
|
1033
|
+
// comma would otherwise sit outside the prefix term; fold it back into
|
|
1034
|
+
// the single attribute/1 directive before directive classification.
|
|
1035
|
+
if (attributeSequence && directive.type === COMPOUND && directive.name === ',' && directive.arity === 2 &&
|
|
1036
|
+
directive.args[0].type === COMPOUND && directive.args[0].name === 'attribute' && directive.args[0].arity === 1) {
|
|
1037
|
+
directive = compound('attribute', [compound(',', [directive.args[0].args[0], directive.args[1]])]);
|
|
1038
|
+
}
|
|
1024
1039
|
const coreDirective = directive.type === 'compound' && (
|
|
1025
1040
|
(['dynamic', 'multifile', 'discontiguous', 'initialization', 'include', 'ensure_loaded'].includes(directive.name) && directive.arity === 1) ||
|
|
1026
1041
|
(['char_conversion', 'set_prolog_flag'].includes(directive.name) && directive.arity === 2)
|
|
1027
1042
|
);
|
|
1028
1043
|
const extensionDirective = directive.type === 'compound' && (
|
|
1029
|
-
(['use_module', 'meta_predicate'].includes(directive.name) && directive.arity === 1) ||
|
|
1044
|
+
(['use_module', 'meta_predicate', 'attribute'].includes(directive.name) && directive.arity === 1) ||
|
|
1030
1045
|
(['module', 'use_module'].includes(directive.name) && directive.arity === 2)
|
|
1031
1046
|
);
|
|
1032
1047
|
if (this.strictIso && extensionDirective) {
|
|
@@ -1044,7 +1059,7 @@ class Parser {
|
|
|
1044
1059
|
// Module loading declarations describe the compilation unit rather
|
|
1045
1060
|
// than an executable source clause, so they do not shift proof clause
|
|
1046
1061
|
// numbers in the importing file.
|
|
1047
|
-
if (!['module', 'use_module', 'meta_predicate'].includes(directive.name)) clauseNumber++;
|
|
1062
|
+
if (!['module', 'use_module', 'meta_predicate', 'attribute'].includes(directive.name)) clauseNumber++;
|
|
1048
1063
|
if (this.sourceMetadata) clause.source = { filename: this.filename, line, clause: clauseNumber };
|
|
1049
1064
|
accept(clause);
|
|
1050
1065
|
continue;
|
package/src/program.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Program representation and clause indexing.
|
|
2
2
|
// Indexes are deliberately conservative: they speed up common scalar arguments but never replace unification as the final check.
|
|
3
|
-
import { ATOM, COMPOUND, STRING, VAR, Env, atom, compound, deref, flattenConjunction, isScalar, numberTerm, properListItems, termToString, variable } from './term.js';
|
|
3
|
+
import { ATOM, COMPOUND, NUMBER, STRING, VAR, Env, atom, compound, deref, flattenConjunction, isScalar, numberTerm, properListItems, termToString, variable } from './term.js';
|
|
4
4
|
import { formatTermForWrite } from './write.js';
|
|
5
5
|
import {
|
|
6
6
|
ISO_OPERATOR_DEFINITIONS,
|
|
@@ -1195,6 +1195,19 @@ function moduleExportIndicators(term) {
|
|
|
1195
1195
|
if (items == null) return null;
|
|
1196
1196
|
const indicators = [];
|
|
1197
1197
|
for (const item of items) {
|
|
1198
|
+
// Scryer-style modules may export operators alongside predicate
|
|
1199
|
+
// indicators, for example op(700, xfx, #=) from library(clpz).
|
|
1200
|
+
// Operator visibility while parsing bundled libraries is handled by the
|
|
1201
|
+
// parser's imported-library operator table; module import resolution only
|
|
1202
|
+
// needs procedure indicators here.
|
|
1203
|
+
if (item.type === COMPOUND && item.name === 'op' && item.arity === 3) {
|
|
1204
|
+
const [priority, specifier, names] = item.args;
|
|
1205
|
+
const operatorNames = names.type === ATOM ? [names] : properListItems(names, new Env());
|
|
1206
|
+
if (priority.type !== NUMBER || !/^\d+$/.test(priority.name) || Number(priority.name) > 1200 ||
|
|
1207
|
+
specifier.type !== ATOM || !['fx', 'fy', 'xf', 'yf', 'xfx', 'xfy', 'yfx'].includes(specifier.name) ||
|
|
1208
|
+
operatorNames == null || operatorNames.some((name) => name.type !== ATOM)) return null;
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1198
1211
|
if (item.type !== COMPOUND || !['/', '//'].includes(item.name) || item.arity !== 2) return null;
|
|
1199
1212
|
const indicator = predicateIndicator(item.args[0], item.args[1]);
|
|
1200
1213
|
if (!indicator) return null;
|
package/src/repl.js
CHANGED
|
@@ -753,6 +753,24 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
753
753
|
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
754
754
|
}));
|
|
755
755
|
}
|
|
756
|
+
const projectedAttributeRoots = new Set();
|
|
757
|
+
for (const variable of variables) {
|
|
758
|
+
const root = engine.deref(variable, env);
|
|
759
|
+
if (root.type !== 'var' || projectedAttributeRoots.has(root.name) || !env.hasPrologAttributes?.(root.name)) continue;
|
|
760
|
+
projectedAttributeRoots.add(root.name);
|
|
761
|
+
names.set(root.name, variable.name);
|
|
762
|
+
for (const residual of state.solver.attributeResidualGoals(root, env)) {
|
|
763
|
+
collectUnboundVariables(engine, residual, env, names, () => `_${letterName(generated++)}`);
|
|
764
|
+
bindings.push(engine.formatTermForWrite(residual, env, {
|
|
765
|
+
quoted: true,
|
|
766
|
+
operators,
|
|
767
|
+
variableNames: names,
|
|
768
|
+
operatorAtomsAsArgs: true,
|
|
769
|
+
dottedGraphicAtoms: true,
|
|
770
|
+
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
771
|
+
}));
|
|
772
|
+
}
|
|
773
|
+
}
|
|
756
774
|
return bindings.length === 0 ? 'true' : bindings.join(', ');
|
|
757
775
|
}
|
|
758
776
|
|
package/src/solver.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
ATOM, COMPOUND, NUMBER, STRING, VAR, Env, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
|
|
5
5
|
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
6
|
-
numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variable, variantTerms,
|
|
6
|
+
numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
8
8
|
import { numberValueKey, sameNumberValue } from './number-value.js';
|
|
9
9
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
@@ -32,6 +32,23 @@ function qualifyTerm(term, module) {
|
|
|
32
32
|
return term;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function renameVariableInTerm(term, fromName, toName) {
|
|
36
|
+
if (fromName === toName) return term;
|
|
37
|
+
const stack = [term];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
while (stack.length) {
|
|
40
|
+
const current = stack.pop();
|
|
41
|
+
if (current == null || seen.has(current)) continue;
|
|
42
|
+
seen.add(current);
|
|
43
|
+
if (current.type === VAR) {
|
|
44
|
+
if (current.name === fromName) current.name = toName;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (current.type === COMPOUND) for (const arg of current.args) stack.push(arg);
|
|
48
|
+
}
|
|
49
|
+
return term;
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
export function nextFreshId() {
|
|
36
53
|
return ++freshCounter;
|
|
37
54
|
}
|
|
@@ -120,6 +137,8 @@ export class Solver {
|
|
|
120
137
|
raiseOccursCheckError(left, right, env);
|
|
121
138
|
}
|
|
122
139
|
};
|
|
140
|
+
this.attributeHookRunner = (module, attributed, other, env) =>
|
|
141
|
+
this.runAttributeHook(module, attributed, other, env);
|
|
123
142
|
this.charConversions = options.charConversions ?? new Map();
|
|
124
143
|
if (!options.prologFlags) {
|
|
125
144
|
if (['chars', 'codes', 'atom'].includes(program.doubleQuotes)) {
|
|
@@ -303,6 +322,54 @@ export class Solver {
|
|
|
303
322
|
}
|
|
304
323
|
}
|
|
305
324
|
|
|
325
|
+
runAttributeHook(module, attributed, other, env) {
|
|
326
|
+
if (this.program.findGroup('verify_attributes', 3, module) == null) return true;
|
|
327
|
+
const goalsVariable = variable(`\u0000attributeGoals${nextFreshId()}`);
|
|
328
|
+
const hook = compound('verify_attributes', [attributed, other, goalsVariable]);
|
|
329
|
+
qualifyTerm(hook, module);
|
|
330
|
+
const child = this.cloneForInnerGoal(1);
|
|
331
|
+
const iterator = child.solve([hook], env.clone(), 0);
|
|
332
|
+
const result = iterator.next();
|
|
333
|
+
try { iterator.return?.(); } catch (_) { /* best-effort iterator cleanup */ }
|
|
334
|
+
this.absorbStatsFrom(child);
|
|
335
|
+
if (result.done) return false;
|
|
336
|
+
env.adopt(result.value);
|
|
337
|
+
env.setOccursCheckHandler(this.occursCheckHandler);
|
|
338
|
+
env.setAttributeHookRunner(this.attributeHookRunner);
|
|
339
|
+
const goals = properListItems(goalsVariable, env);
|
|
340
|
+
if (goals == null) throw new PrologError('type_error(list)', deref(goalsVariable, env));
|
|
341
|
+
for (const goal of goals) qualifyTerm(goal, module);
|
|
342
|
+
env.enqueueAttributeGoals(goals);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
attributeResidualGoals(variableTerm, env) {
|
|
347
|
+
const root = deref(variableTerm, env);
|
|
348
|
+
if (root.type !== VAR || !env.hasPrologAttributes(root.name)) return [];
|
|
349
|
+
const residuals = [];
|
|
350
|
+
for (const module of env.prologAttributeModules(root.name)) {
|
|
351
|
+
if (this.program.findGroup('attribute_goals', 3, module) == null) continue;
|
|
352
|
+
const goalsVariable = variable(`\u0000attributeResidual${nextFreshId()}`);
|
|
353
|
+
const projection = compound('attribute_goals', [root, goalsVariable, emptyList()]);
|
|
354
|
+
qualifyTerm(projection, module);
|
|
355
|
+
const child = this.cloneForInnerGoal(1);
|
|
356
|
+
const iterator = child.solve([projection], env.clone(), 0);
|
|
357
|
+
const result = iterator.next();
|
|
358
|
+
try { iterator.return?.(); } catch (_) { /* best-effort iterator cleanup */ }
|
|
359
|
+
this.absorbStatsFrom(child);
|
|
360
|
+
if (result.done) continue;
|
|
361
|
+
const goals = properListItems(goalsVariable, result.value);
|
|
362
|
+
if (goals == null) throw new PrologError('type_error(list)', deref(goalsVariable, result.value));
|
|
363
|
+
const projectedRoot = deref(root, result.value);
|
|
364
|
+
for (const goal of goals) {
|
|
365
|
+
const residual = copyResolved(goal, result.value);
|
|
366
|
+
if (projectedRoot.type === VAR) renameVariableInTerm(residual, projectedRoot.name, root.name);
|
|
367
|
+
residuals.push(residual);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return residuals;
|
|
371
|
+
}
|
|
372
|
+
|
|
306
373
|
runInitializations() {
|
|
307
374
|
if (this.program._initializationsExecuted) return;
|
|
308
375
|
for (const goal of this.program.initializations ?? []) {
|
|
@@ -321,6 +388,7 @@ export class Solver {
|
|
|
321
388
|
*solve(goals, env = new Env(), depth = 0) {
|
|
322
389
|
if (!Array.isArray(goals)) goals = [goals];
|
|
323
390
|
env.setOccursCheckHandler(this.occursCheckHandler);
|
|
391
|
+
env.setAttributeHookRunner(this.attributeHookRunner);
|
|
324
392
|
if (this.isoStrict) rejectStrictIsoStringTerms(goals, env);
|
|
325
393
|
|
|
326
394
|
const writeVariableState = this.writeVariableState;
|
|
@@ -386,6 +454,7 @@ export class Solver {
|
|
|
386
454
|
goals = frame.goals;
|
|
387
455
|
env = frame.env;
|
|
388
456
|
env.setOccursCheckHandler(this.occursCheckHandler);
|
|
457
|
+
env.setAttributeHookRunner(this.attributeHookRunner);
|
|
389
458
|
depth = frame.depth;
|
|
390
459
|
let active = frame.active;
|
|
391
460
|
|
|
@@ -408,6 +477,9 @@ export class Solver {
|
|
|
408
477
|
}
|
|
409
478
|
if (this.solutionsSeen >= this.solutionLimit) break;
|
|
410
479
|
|
|
480
|
+
const pendingAttributeGoals = env.takePendingAttributeGoals?.() ?? [];
|
|
481
|
+
if (pendingAttributeGoals.length > 0) goals = [...pendingAttributeGoals, ...goals];
|
|
482
|
+
|
|
411
483
|
const readyDelays = env.takeReadyDelays();
|
|
412
484
|
if (readyDelays.length > 0) {
|
|
413
485
|
const awakened = readyDelays.map(({ goal, module }) => {
|
|
@@ -1971,7 +2043,7 @@ function fastCountBranch(branch, env) {
|
|
|
1971
2043
|
|
|
1972
2044
|
function fastCountPureGoal(solver, goal, env) {
|
|
1973
2045
|
if (solver.isoStrict || solver.solutionLimit !== Infinity || solver.maxInferences !== Infinity ||
|
|
1974
|
-
env._clpz != null || env._variableConstraints != null || (env._delays != null && env._delays.size !== 0)) return null;
|
|
2046
|
+
env._clpz != null || env._variableConstraints != null || env._prologAttributes != null || (env._delays != null && env._delays.size !== 0)) return null;
|
|
1975
2047
|
const budget = { remaining: 8192 };
|
|
1976
2048
|
const branches = expandPureCountGoal(solver, goal, goal.module ?? 'user', new Set(), budget);
|
|
1977
2049
|
if (branches == null) return null;
|
|
@@ -1981,6 +2053,7 @@ function fastCountPureGoal(solver, goal, env) {
|
|
|
1981
2053
|
}
|
|
1982
2054
|
|
|
1983
2055
|
function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
2056
|
+
if (env._prologAttributes != null) return false;
|
|
1984
2057
|
// Consecutive scalar-fact lookups are common in data-heavy joins. Short
|
|
1985
2058
|
// joins are fastest when their continuation frames are materialized locally;
|
|
1986
2059
|
// wide joins can explode combinatorially, so they are streamed lazily.
|
|
@@ -2164,6 +2237,11 @@ function matchScalarFactLocal(goal, head, env, names, values) {
|
|
|
2164
2237
|
}
|
|
2165
2238
|
|
|
2166
2239
|
function matchScalarFact(goal, head, env) {
|
|
2240
|
+
if (env._prologAttributes != null) {
|
|
2241
|
+
const next = env.clone();
|
|
2242
|
+
if (!unify(goal, head, next)) return null;
|
|
2243
|
+
return next;
|
|
2244
|
+
}
|
|
2167
2245
|
// A scalar ground fact has no variables to freshen and no compound structure
|
|
2168
2246
|
// to traverse. Match the goal arguments directly and clone only after the
|
|
2169
2247
|
// candidate has succeeded.
|
package/src/standard-library.js
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
// source-level interop autoloader declared below.
|
|
5
5
|
import { PrologError, createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
|
|
6
6
|
import { clpzBuiltins } from './clpz.js';
|
|
7
|
+
import { attsBuiltins } from './atts.js';
|
|
7
8
|
import { registerCleanupBuiltins } from './cleanup.js';
|
|
8
9
|
import { fs, isNode, memoryStatistics } from './platform.js';
|
|
9
10
|
import { ATOM, VAR, atom, deref, numberTerm, unify } from './term.js';
|
|
10
11
|
|
|
11
12
|
const moduleFiles = Object.freeze({
|
|
12
13
|
aggregate: 'aggregate.pl',
|
|
14
|
+
atts: 'atts.pl',
|
|
13
15
|
clpz: 'clpz.pl',
|
|
14
16
|
comparison: 'comparison.pl',
|
|
15
17
|
dates: 'dates.pl',
|
|
@@ -49,6 +51,7 @@ function libraryUrl(filename) {
|
|
|
49
51
|
|
|
50
52
|
export const eyePrologNativeLibraryIndicators = Object.freeze([
|
|
51
53
|
'call_nth/2', 'freeze/2', 'dif/2', 'countall/2', 'time/1',
|
|
54
|
+
'put_atts/2', 'get_atts/2', 'put_attr/3', 'get_attr/3', 'del_attr/2', 'term_attributed_variables/2',
|
|
52
55
|
'#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
|
|
53
56
|
'#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
|
|
54
57
|
'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
|
|
@@ -137,7 +140,7 @@ export const eyePrologInteropLibraryIndicators = Object.freeze(
|
|
|
137
140
|
// Libraries whose *name* is part of the current interop profile. A program
|
|
138
141
|
// may freely use_module/1 with these common module names; predicates in those
|
|
139
142
|
// modules outside eyePrologInteropLibraryIndicators are still diagnosed when used.
|
|
140
|
-
export const eyePrologInteropLibraryModules = Object.freeze(['lists', 'iso_ext', 'lambda']);
|
|
143
|
+
export const eyePrologInteropLibraryModules = Object.freeze(['lists', 'iso_ext', 'lambda', 'atts']);
|
|
141
144
|
|
|
142
145
|
function* tabledNegationBuiltin({ solver, goal, env }) {
|
|
143
146
|
yield* solver.solveTabledNegation(goal.args[0], env);
|
|
@@ -184,6 +187,7 @@ export function createEyePrologRegistry() {
|
|
|
184
187
|
registry.add('tnot', 1, tabledNegationBuiltin, { deterministic: true });
|
|
185
188
|
registerCleanupBuiltins(registry);
|
|
186
189
|
eyePrologLibraryBuiltins.register(registry);
|
|
190
|
+
attsBuiltins.register(registry);
|
|
187
191
|
clpzBuiltins.register(registry);
|
|
188
192
|
registry.eyePrologLibrary = true;
|
|
189
193
|
return registry;
|
package/src/term.js
CHANGED
|
@@ -102,6 +102,13 @@ export class Env {
|
|
|
102
102
|
// is copied only when a branch adds, removes, or reindexes a constraint.
|
|
103
103
|
this._variableConstraints = null;
|
|
104
104
|
this._variableAnnotations = null;
|
|
105
|
+
// Prolog-visible attributed variables. The outer map is indexed by the
|
|
106
|
+
// current unbound representative name; each representative owns module-
|
|
107
|
+
// scoped attribute terms keyed by functor/arity. Maps are copy-on-write so
|
|
108
|
+
// Env.clone() keeps ordinary Prolog backtracking constant-time.
|
|
109
|
+
this._prologAttributes = null;
|
|
110
|
+
this._attributeHookRunner = null;
|
|
111
|
+
this._pendingAttributeGoals = null;
|
|
105
112
|
this._clpz = null;
|
|
106
113
|
this._occursCheckHandler = null;
|
|
107
114
|
this._localVariables = null;
|
|
@@ -117,6 +124,9 @@ export class Env {
|
|
|
117
124
|
clone._delays = this._delays;
|
|
118
125
|
clone._variableConstraints = this._variableConstraints;
|
|
119
126
|
clone._variableAnnotations = this._variableAnnotations;
|
|
127
|
+
clone._prologAttributes = this._prologAttributes;
|
|
128
|
+
clone._attributeHookRunner = this._attributeHookRunner;
|
|
129
|
+
clone._pendingAttributeGoals = this._pendingAttributeGoals;
|
|
120
130
|
clone._clpz = this._clpz;
|
|
121
131
|
clone._occursCheckHandler = this._occursCheckHandler;
|
|
122
132
|
clone._localVariables = this._localVariables;
|
|
@@ -126,6 +136,24 @@ export class Env {
|
|
|
126
136
|
this._occursCheckHandler = typeof handler === 'function' ? handler : null;
|
|
127
137
|
return this;
|
|
128
138
|
}
|
|
139
|
+
setAttributeHookRunner(handler) {
|
|
140
|
+
this._attributeHookRunner = typeof handler === 'function' ? handler : null;
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
adopt(other) {
|
|
144
|
+
if (!(other instanceof Env)) throw new TypeError('Env.adopt expects Env');
|
|
145
|
+
this._state = other._state;
|
|
146
|
+
this._delays = other._delays;
|
|
147
|
+
this._variableConstraints = other._variableConstraints;
|
|
148
|
+
this._variableAnnotations = other._variableAnnotations;
|
|
149
|
+
this._prologAttributes = other._prologAttributes;
|
|
150
|
+
this._pendingAttributeGoals = other._pendingAttributeGoals;
|
|
151
|
+
this._clpz = other._clpz;
|
|
152
|
+
// Execution callbacks belong to the Solver driving this Env, not to the
|
|
153
|
+
// logical branch being adopted from an inner attribute-hook call.
|
|
154
|
+
this._localVariables = other._localVariables;
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
129
157
|
hasLocalVariables() {
|
|
130
158
|
return this._localVariables != null && this._localVariables.size !== 0;
|
|
131
159
|
}
|
|
@@ -243,6 +271,150 @@ export class Env {
|
|
|
243
271
|
if (ready.length > 0) this._delays = remaining;
|
|
244
272
|
return ready;
|
|
245
273
|
}
|
|
274
|
+
_attributeRootName(name) {
|
|
275
|
+
const root = deref(variable(name), this);
|
|
276
|
+
return root.type === VAR ? root.name : null;
|
|
277
|
+
}
|
|
278
|
+
_attributeModulesForRoot(name) {
|
|
279
|
+
const root = this._attributeRootName(name);
|
|
280
|
+
return root == null ? null : (this._prologAttributes?.get(root) ?? null);
|
|
281
|
+
}
|
|
282
|
+
prologAttributes(name, module = null) {
|
|
283
|
+
const modules = this._attributeModulesForRoot(name);
|
|
284
|
+
if (modules == null) return [];
|
|
285
|
+
if (module != null) return [...(modules.get(module)?.values() ?? [])];
|
|
286
|
+
const result = [];
|
|
287
|
+
for (const [owner, attrs] of modules) {
|
|
288
|
+
for (const attribute of attrs.values()) result.push({ module: owner, attribute });
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
prologAttributeModules(name) {
|
|
293
|
+
const modules = this._attributeModulesForRoot(name);
|
|
294
|
+
return modules == null ? [] : [...modules.keys()];
|
|
295
|
+
}
|
|
296
|
+
getPrologAttribute(name, module, functor, arity) {
|
|
297
|
+
const modules = this._attributeModulesForRoot(name);
|
|
298
|
+
return modules?.get(module)?.get(`${functor}/${arity}`) ?? null;
|
|
299
|
+
}
|
|
300
|
+
putPrologAttribute(name, module, attribute) {
|
|
301
|
+
const root = this._attributeRootName(name);
|
|
302
|
+
if (root == null) return false;
|
|
303
|
+
const signature = `${attribute.name}/${attribute.arity}`;
|
|
304
|
+
const outer = new Map(this._prologAttributes ?? []);
|
|
305
|
+
const modules = new Map(outer.get(root) ?? []);
|
|
306
|
+
const attrs = new Map(modules.get(module) ?? []);
|
|
307
|
+
attrs.set(signature, attribute);
|
|
308
|
+
modules.set(module, attrs);
|
|
309
|
+
outer.set(root, modules);
|
|
310
|
+
this._prologAttributes = outer;
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
deletePrologAttribute(name, module, functor, arity = null) {
|
|
314
|
+
const root = this._attributeRootName(name);
|
|
315
|
+
if (root == null) return false;
|
|
316
|
+
const existingModules = this._prologAttributes?.get(root);
|
|
317
|
+
const existingAttrs = existingModules?.get(module);
|
|
318
|
+
if (existingAttrs == null) return false;
|
|
319
|
+
const attrs = new Map(existingAttrs);
|
|
320
|
+
let deleted = false;
|
|
321
|
+
if (arity == null) {
|
|
322
|
+
for (const key of [...attrs.keys()]) {
|
|
323
|
+
if (key.startsWith(`${functor}/`)) { attrs.delete(key); deleted = true; }
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
deleted = attrs.delete(`${functor}/${arity}`);
|
|
327
|
+
}
|
|
328
|
+
if (!deleted) return false;
|
|
329
|
+
const modules = new Map(existingModules);
|
|
330
|
+
if (attrs.size === 0) modules.delete(module); else modules.set(module, attrs);
|
|
331
|
+
const outer = new Map(this._prologAttributes);
|
|
332
|
+
if (modules.size === 0) outer.delete(root); else outer.set(root, modules);
|
|
333
|
+
this._prologAttributes = outer.size === 0 ? null : outer;
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
hasPrologAttributes(name) {
|
|
337
|
+
const modules = this._attributeModulesForRoot(name);
|
|
338
|
+
if (modules == null) return false;
|
|
339
|
+
for (const attrs of modules.values()) if (attrs.size !== 0) return true;
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
attributedVariableNames() {
|
|
343
|
+
if (this._prologAttributes == null) return [];
|
|
344
|
+
const names = [];
|
|
345
|
+
const seen = new Set();
|
|
346
|
+
for (const name of this._prologAttributes.keys()) {
|
|
347
|
+
const root = deref(variable(name), this);
|
|
348
|
+
if (root.type !== VAR || seen.has(root.name)) continue;
|
|
349
|
+
if (!this.hasPrologAttributes(root.name)) continue;
|
|
350
|
+
seen.add(root.name);
|
|
351
|
+
names.push(root.name);
|
|
352
|
+
}
|
|
353
|
+
return names;
|
|
354
|
+
}
|
|
355
|
+
enqueueAttributeGoals(goals) {
|
|
356
|
+
if (goals == null || goals.length === 0) return;
|
|
357
|
+
this._pendingAttributeGoals = [...(this._pendingAttributeGoals ?? []), ...goals];
|
|
358
|
+
}
|
|
359
|
+
takePendingAttributeGoals() {
|
|
360
|
+
if (this._pendingAttributeGoals == null || this._pendingAttributeGoals.length === 0) return [];
|
|
361
|
+
const goals = this._pendingAttributeGoals;
|
|
362
|
+
this._pendingAttributeGoals = null;
|
|
363
|
+
return goals;
|
|
364
|
+
}
|
|
365
|
+
_ownerModules(name) {
|
|
366
|
+
const modules = this._attributeModulesForRoot(name);
|
|
367
|
+
return modules == null ? [] : [...modules.keys()];
|
|
368
|
+
}
|
|
369
|
+
_targetHasOwnerModule(name, module) {
|
|
370
|
+
return this._attributeModulesForRoot(name)?.has(module) === true;
|
|
371
|
+
}
|
|
372
|
+
preparePrologAttributeUnification(variableTerm, otherTerm) {
|
|
373
|
+
if (this._prologAttributes == null || variableTerm?.type !== VAR) return true;
|
|
374
|
+
const sourceRoot = deref(variableTerm, this);
|
|
375
|
+
if (sourceRoot.type !== VAR) return true;
|
|
376
|
+
const modules = this._ownerModules(sourceRoot.name);
|
|
377
|
+
if (modules.length === 0) return true;
|
|
378
|
+
const other = deref(otherTerm, this);
|
|
379
|
+
if (other.type === VAR) {
|
|
380
|
+
if (other.name === sourceRoot.name) return true;
|
|
381
|
+
// Aliasing an attributed variable with a plain variable is just a
|
|
382
|
+
// representative change. No user-level hook is needed; final aliasing
|
|
383
|
+
// moves the attributes to the surviving representative.
|
|
384
|
+
const hooks = modules.filter((module) => this._targetHasOwnerModule(other.name, module));
|
|
385
|
+
for (const module of hooks) {
|
|
386
|
+
if (this._attributeHookRunner && !this._attributeHookRunner(module, sourceRoot, other, this)) return false;
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
for (const module of modules) {
|
|
391
|
+
if (this._attributeHookRunner && !this._attributeHookRunner(module, sourceRoot, other, this)) return false;
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
finalizePrologAttributeAlias(sourceName, targetName) {
|
|
396
|
+
if (this._prologAttributes == null || sourceName === targetName) return;
|
|
397
|
+
const source = this._prologAttributes.get(sourceName);
|
|
398
|
+
if (source == null) return;
|
|
399
|
+
const outer = new Map(this._prologAttributes);
|
|
400
|
+
const target = new Map(outer.get(targetName) ?? []);
|
|
401
|
+
for (const [module, sourceAttrs] of source) {
|
|
402
|
+
const attrs = new Map(target.get(module) ?? []);
|
|
403
|
+
for (const [signature, attribute] of sourceAttrs) {
|
|
404
|
+
if (!attrs.has(signature)) attrs.set(signature, attribute);
|
|
405
|
+
}
|
|
406
|
+
target.set(module, attrs);
|
|
407
|
+
}
|
|
408
|
+
outer.delete(sourceName);
|
|
409
|
+
outer.set(targetName, target);
|
|
410
|
+
this._prologAttributes = outer;
|
|
411
|
+
}
|
|
412
|
+
dropPrologAttributes(name) {
|
|
413
|
+
if (this._prologAttributes == null) return;
|
|
414
|
+
const outer = new Map(this._prologAttributes);
|
|
415
|
+
outer.delete(name);
|
|
416
|
+
this._prologAttributes = outer.size === 0 ? null : outer;
|
|
417
|
+
}
|
|
246
418
|
addVariableConstraint(constraint) {
|
|
247
419
|
if (constraint == null || typeof constraint.variables !== 'function' ||
|
|
248
420
|
typeof constraint.status !== 'function') {
|
|
@@ -374,6 +546,15 @@ export function unify(left, right, env, options = {}) {
|
|
|
374
546
|
|
|
375
547
|
if (a.type === VAR && b.type === VAR && a.name === b.name) continue;
|
|
376
548
|
if (a.type === VAR && b.type === VAR) {
|
|
549
|
+
if (env?._prologAttributes != null) {
|
|
550
|
+
if (!env.preparePrologAttributeUnification(a, b)) return false;
|
|
551
|
+
a = deref(a, env);
|
|
552
|
+
b = deref(b, env);
|
|
553
|
+
if (a.type !== VAR || b.type !== VAR || a.name === b.name) {
|
|
554
|
+
stack.push([a, b]);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
377
558
|
// For a compiler-generated DCG state handed directly to another
|
|
378
559
|
// nonterminal, keep the local caller variable as representative. Ordinary
|
|
379
560
|
// aliases retain the established direction and observable conventions.
|
|
@@ -381,9 +562,11 @@ export function unify(left, right, env, options = {}) {
|
|
|
381
562
|
a.name.startsWith('\u0000dcg') && b.name.startsWith('\u0000dcg');
|
|
382
563
|
if (aLocalDcg) {
|
|
383
564
|
markCompactVariableBound(b);
|
|
565
|
+
env?.finalizePrologAttributeAlias?.(b.name, a.name);
|
|
384
566
|
env.bind(b.name, a);
|
|
385
567
|
} else {
|
|
386
568
|
markCompactVariableBound(a);
|
|
569
|
+
env?.finalizePrologAttributeAlias?.(a.name, b.name);
|
|
387
570
|
env.bind(a.name, b);
|
|
388
571
|
}
|
|
389
572
|
continue;
|
|
@@ -394,7 +577,14 @@ export function unify(left, right, env, options = {}) {
|
|
|
394
577
|
occursCheckHandler?.(a, b, env);
|
|
395
578
|
return false;
|
|
396
579
|
}
|
|
580
|
+
if (env?._prologAttributes != null && env.hasPrologAttributes(a.name)) {
|
|
581
|
+
if (!env.preparePrologAttributeUnification(a, b)) return false;
|
|
582
|
+
a = deref(a, env);
|
|
583
|
+
b = deref(b, env);
|
|
584
|
+
if (a.type !== VAR) { stack.push([a, b]); continue; }
|
|
585
|
+
}
|
|
397
586
|
markCompactVariableBound(a);
|
|
587
|
+
env?.dropPrologAttributes?.(a.name);
|
|
398
588
|
env.bind(a.name, b);
|
|
399
589
|
if (aLocal) env.forgetLocalVariable(a.name);
|
|
400
590
|
continue;
|
|
@@ -405,7 +595,14 @@ export function unify(left, right, env, options = {}) {
|
|
|
405
595
|
occursCheckHandler?.(b, a, env);
|
|
406
596
|
return false;
|
|
407
597
|
}
|
|
598
|
+
if (env?._prologAttributes != null && env.hasPrologAttributes(b.name)) {
|
|
599
|
+
if (!env.preparePrologAttributeUnification(b, a)) return false;
|
|
600
|
+
b = deref(b, env);
|
|
601
|
+
a = deref(a, env);
|
|
602
|
+
if (b.type !== VAR) { stack.push([a, b]); continue; }
|
|
603
|
+
}
|
|
408
604
|
markCompactVariableBound(b);
|
|
605
|
+
env?.dropPrologAttributes?.(b.name);
|
|
409
606
|
env.bind(b.name, a);
|
|
410
607
|
if (bLocal) env.forgetLocalVariable(b.name);
|
|
411
608
|
continue;
|
package/test/run-regression.mjs
CHANGED
|
@@ -129,6 +129,62 @@ function regressionCases() {
|
|
|
129
129
|
'scalar-fact fast matching can discharge an annotated disequality');
|
|
130
130
|
},
|
|
131
131
|
},
|
|
132
|
+
{
|
|
133
|
+
name: 'library(atts) provides Scryer-style attributed-variable hooks',
|
|
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]).
|
|
136
|
+
:- use_module(library(atts)).
|
|
137
|
+
:- attribute mark/1.
|
|
138
|
+
attach(X, V) :- put_atts(X, mark(V)).
|
|
139
|
+
verify_attributes(Var, Other, Goals) :-
|
|
140
|
+
( get_atts(Var, mark(V)) ->
|
|
141
|
+
( var(Other) ->
|
|
142
|
+
( get_atts(Other, mark(W)) -> V = W ; put_atts(Other, mark(V)) ),
|
|
143
|
+
Goals = []
|
|
144
|
+
; Goals = [same(Other, V)] )
|
|
145
|
+
; Goals = [] ).
|
|
146
|
+
same(X, Y) :- X = Y.
|
|
147
|
+
check_get :- attach(X, a), get_atts(X, mark(a)), X = a.
|
|
148
|
+
check_alias :- attach(X, a), X = Y, get_atts(Y, mark(a)), Y = a.
|
|
149
|
+
check_ok :- attach(X, 3), X = 3.
|
|
150
|
+
check_bad :- attach(X, 3), X = 4.
|
|
151
|
+
check_conflict :- attach(X, a), attach(Y, b), X = Y.
|
|
152
|
+
check_backtrack :- (attach(X, a), fail ; true), \\+ get_atts(X, mark(_)).
|
|
153
|
+
check_term_vars :- attach(X, a), term_attributed_variables(pair(X,Y), [X]), var(Y), X = a.
|
|
154
|
+
`;
|
|
155
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_get' }).stdout, 'attr_probe:check_get.', 'get_atts/2');
|
|
156
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_alias' }).stdout, 'attr_probe:check_alias.', 'attribute transfer on aliasing');
|
|
157
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_ok' }).stdout, 'attr_probe:check_ok.', 'post-binding hook goals');
|
|
158
|
+
assertEqual(run(source, { goal: 'attr_probe:check_bad' }).stdout, '', 'post-binding hook can reject a binding');
|
|
159
|
+
assertEqual(run(source, { goal: 'attr_probe:check_conflict' }).stdout, '', 'same-module attributes are verified before aliasing');
|
|
160
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_backtrack' }).stdout, 'attr_probe:check_backtrack.', 'attributes backtrack with Env branches');
|
|
161
|
+
assertIncludes(run(source, { goal: 'attr_probe:check_term_vars' }).stdout, 'attr_probe:check_term_vars.', 'term_attributed_variables/2');
|
|
162
|
+
|
|
163
|
+
const residualFile = path.join(tmp, `atts-residual-${++tmpCounter}.pl`);
|
|
164
|
+
fs.writeFileSync(residualFile, `:- use_module(library(atts)).
|
|
165
|
+
:- attribute required/1.
|
|
166
|
+
attach(X,V) :- put_atts(X, required(V)).
|
|
167
|
+
verify_attributes(Var, Other, Goals) :-
|
|
168
|
+
( get_atts(Var, required(V)) ->
|
|
169
|
+
( var(Other) -> put_atts(Other, required(V)), Goals=[] ; Goals=[same(Other,V)] )
|
|
170
|
+
; Goals=[] ).
|
|
171
|
+
same(X,X).
|
|
172
|
+
attribute_goals(X) --> { get_atts(X, required(V)) }, [required(X,V)].
|
|
173
|
+
`);
|
|
174
|
+
const repl = runCli([], { input: `[${sourceAtom(residualFile)}].\nattach(X,a).\nhalt.\n` });
|
|
175
|
+
assertEqual(repl.status, 0, 'attribute residual REPL status');
|
|
176
|
+
assertIncludes(repl.stdout, 'required(X, a).', 'attribute_goals//1 residual projection');
|
|
177
|
+
|
|
178
|
+
const scryerShape = Program.parse(`:- module(scryer_shape, [op(700, xfx, #=), probe/0]).
|
|
179
|
+
:- op(700, xfx, #=).
|
|
180
|
+
:- use_module(library(atts)).
|
|
181
|
+
:- attribute first/1, second/0.
|
|
182
|
+
probe.
|
|
183
|
+
`);
|
|
184
|
+
assertEqual(scryerShape.findGroup('probe', 0, 'scryer_shape')?.module, 'scryer_shape',
|
|
185
|
+
'Scryer-style operator exports and multi-attribute directives load');
|
|
186
|
+
},
|
|
187
|
+
},
|
|
132
188
|
{
|
|
133
189
|
name: 'write_term variable_names/1 errors preserve the instantiated option culprit (issue #69)',
|
|
134
190
|
run: () => {
|
|
@@ -3626,8 +3682,8 @@ function documentationSyncCases() {
|
|
|
3626
3682
|
'<a href="https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog">\n <img src="book-assets/title-page.svg" alt="Read The Art of EyeProlog"',
|
|
3627
3683
|
'README cover links to the book',
|
|
3628
3684
|
);
|
|
3629
|
-
for (const filename of ['src/iso.js', 'src/dcg.js', 'src/standard-library.js',
|
|
3630
|
-
'src/lib/aggregate.pl', 'src/lib/comparison.pl', 'src/lib/dates.pl',
|
|
3685
|
+
for (const filename of ['src/iso.js', 'src/dcg.js', 'src/atts.js', 'src/standard-library.js',
|
|
3686
|
+
'src/CLPZ-MIGRATION.md', 'src/lib/aggregate.pl', 'src/lib/atts.pl', 'src/lib/comparison.pl', 'src/lib/dates.pl',
|
|
3631
3687
|
'src/lib/iso_ext.pl', 'src/lib/lists.pl', 'src/lib/primes.pl', 'src/lib/prologue.pl',
|
|
3632
3688
|
'src/lib/random.pl', 'src/lib/strings.pl', 'src/lib/uuid.pl',
|
|
3633
3689
|
'src/playground-worker.js']) {
|
|
@@ -4729,7 +4785,7 @@ answer(ok) :-
|
|
|
4729
4785
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
4730
4786
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
4731
4787
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
4732
|
-
assertEqual(library.defs.size,
|
|
4788
|
+
assertEqual(library.defs.size, 171, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and private library adapters');
|
|
4733
4789
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
4734
4790
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
4735
4791
|
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
|
@@ -4746,19 +4802,19 @@ answer(ok) :-
|
|
|
4746
4802
|
assertEqual(Boolean(library.get('call_cleanup', 2)), true, 'call_cleanup/2 is an EyeProlog cleanup control');
|
|
4747
4803
|
assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
|
|
4748
4804
|
assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
|
|
4749
|
-
assertEqual(registeredNativeEyePrologLibraryNames().length,
|
|
4805
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 48, 'public native EyeProlog builtin count');
|
|
4750
4806
|
assertEqual(eyePrologPortableLibraryIndicators.length, 87, 'portable Prolog library count');
|
|
4751
4807
|
assertEqual(eyePrologInteropLibraryIndicators.length, 29, 'cross-implementation interop profile count');
|
|
4752
|
-
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda', 'common explicit library module profile');
|
|
4808
|
+
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda,atts', 'common explicit library module profile');
|
|
4753
4809
|
assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
|
|
4754
4810
|
assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
|
|
4755
4811
|
assertEqual(eyePrologInteropAutoload['call_nth/2'], 'iso_ext', 'call_nth/2 canonical interop autoload');
|
|
4756
4812
|
assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
|
|
4757
4813
|
assertEqual(eyePrologInteropAutoload['.../2'], 'iso_ext', '.../2 canonical interop autoload');
|
|
4758
4814
|
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
4759
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
4815
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 48, 'native host library count');
|
|
4760
4816
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 3).join(','), 'call_nth/2,freeze/2,dif/2', 'control and constraint predicates requiring host support');
|
|
4761
|
-
assertEqual(eyePrologLibraryIndicators.length,
|
|
4817
|
+
assertEqual(eyePrologLibraryIndicators.length, 135, 'complete EyeProlog library surface');
|
|
4762
4818
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
4763
4819
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
4764
4820
|
assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
|
|
@@ -4769,6 +4825,9 @@ answer(ok) :-
|
|
|
4769
4825
|
assertEqual(Boolean(library.get('eyeprolog__time', 1)), true, 'private time adapter is registered for EyeProlog');
|
|
4770
4826
|
assertEqual(Boolean(library.get('eyeprolog__clpz_labeling', 2)), true, 'private CLP(Z) labeling adapter is registered');
|
|
4771
4827
|
assertEqual(Boolean(library.get('eyeprolog__clpz_global_cardinality', 3)), true, 'private CLP(Z) cardinality adapter is registered');
|
|
4828
|
+
assertEqual(registry.get('put_atts', 2), null, 'put_atts/2 is absent from the ISO registry');
|
|
4829
|
+
assertEqual(Boolean(library.get('put_atts', 2)), true, 'put_atts/2 is registered for attributed-variable libraries');
|
|
4830
|
+
assertEqual(Boolean(library.get('$put_to_attr_list', 3)), true, 'private attribute-list adapter is registered');
|
|
4772
4831
|
assertEqual(library.get('between', 3), null, 'between/3 remains portable Prolog');
|
|
4773
4832
|
assertEqual(library.get('smallest_divisor_from', 3), null, 'smallest_divisor_from/3 remains portable Prolog');
|
|
4774
4833
|
assertEqual(library.get('random', 3), null, 'random/3 remains portable Prolog');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6211,18 +6211,18 @@ so side effects occur in Prolog execution order.
|
|
|
6211
6211
|
|
|
6212
6212
|
### The EyeProlog library
|
|
6213
6213
|
|
|
6214
|
-
EyeProlog exposes **
|
|
6215
|
-
indicators in its isolated ISO profile. **
|
|
6214
|
+
EyeProlog exposes **135 library predicate indicators** in addition to the 129
|
|
6215
|
+
indicators in its isolated ISO profile. **89 are defined entirely as ordinary
|
|
6216
6216
|
Prolog clauses** in focused modules under `src/lib/`. The remaining public
|
|
6217
6217
|
relations use small private host adapters where control, constraints, or host
|
|
6218
6218
|
observability cannot be expressed by ordinary clauses alone; `time/1` is one
|
|
6219
6219
|
such relation. The resulting normal EyeProlog language surface is therefore
|
|
6220
|
-
**
|
|
6220
|
+
**264 public predicate indicators**. Most library relations remain module source
|
|
6221
6221
|
clauses over private adapters. `time/1` is additionally registered directly in
|
|
6222
6222
|
the normal EyeProlog runtime so Trealla-style timing works at the interactive
|
|
6223
6223
|
top level without an import; it is absent from the strict ISO registry.
|
|
6224
6224
|
|
|
6225
|
-
The sources are `src/lib/aggregate.pl`, `src/lib/clpz.pl`, `src/lib/comparison.pl`,
|
|
6225
|
+
The sources are `src/lib/aggregate.pl`, `src/lib/atts.pl`, `src/lib/clpz.pl`, `src/lib/comparison.pl`,
|
|
6226
6226
|
`src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lambda.pl`,
|
|
6227
6227
|
`src/lib/lists.pl`, `src/lib/primes.pl`, `src/lib/prologue.pl`,
|
|
6228
6228
|
`src/lib/random.pl`, `src/lib/strings.pl`, and `src/lib/uuid.pl`. Each declares a same-named module
|
|
@@ -6253,6 +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
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` |
|
|
6257
6258
|
| `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
|
|
6258
6259
|
| `library(dates)` | `difference/3` |
|
|
@@ -6402,17 +6403,32 @@ The catalog above is authoritative for the complete EyeProlog library surface.
|
|
|
6402
6403
|
The following notes describe useful parts of that surface without extending the
|
|
6403
6404
|
cross-engine claims made above.
|
|
6404
6405
|
|
|
6406
|
+
`library(atts)` is the Prolog-facing attributed-variable layer over the persistent
|
|
6407
|
+
annotated-variable machinery in `src/term.js`, with its small host bridge in
|
|
6408
|
+
`src/atts.js`. It provides the attribute operations used by Scryer libraries,
|
|
6409
|
+
accepts `:- attribute ...` declarations, invokes module-local
|
|
6410
|
+
`verify_attributes/3` before an attributed binding is committed, and schedules
|
|
6411
|
+
the returned goals immediately after the binding. Attribute maps are copied only
|
|
6412
|
+
when changed and therefore backtrack with `Env` branches; the interactive top
|
|
6413
|
+
level projects module `attribute_goals//1` hooks as residual goals. The checked
|
|
6414
|
+
[`examples/attributed-variables.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/attributed-variables.pl)
|
|
6415
|
+
demonstrates binding verification and attribute transfer across aliases.
|
|
6416
|
+
|
|
6405
6417
|
`library(clpz)` follows the familiar Trealla and Scryer convention for
|
|
6406
6418
|
constraint logic programming over integers. EyeProlog currently provides finite
|
|
6407
6419
|
interval and union domains, arithmetic and reified constraints, backtrackable
|
|
6408
6420
|
labeling with `ff`, `up`, and `down`, global distinctness, linear sums and scalar
|
|
6409
6421
|
products, chains, elements, value counting, extensional tuple tables,
|
|
6410
6422
|
lexicographic chains, serialized schedules, global cardinality with costs,
|
|
6411
|
-
Hamiltonian circuits, three-way comparison, and domain reflection.
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6423
|
+
Hamiltonian circuits, three-way comparison, and domain reflection. Its existing
|
|
6424
|
+
`src/clpz.js` store is now explicitly transitional: the attributed-variable layer
|
|
6425
|
+
is intended to support staged reuse of Markus Triska's MIT-licensed
|
|
6426
|
+
[Scryer `clpz.pl`](https://github.com/mthom/scryer-prolog/blob/master/src/lib/clpz.pl).
|
|
6427
|
+
The remaining blockers are compile-time term/goal expansion and the supporting
|
|
6428
|
+
library surface used by that source; the migration state is tracked in
|
|
6429
|
+
[`src/CLPZ-MIGRATION.md`](src/CLPZ-MIGRATION.md). Until parity is reached, the JS
|
|
6430
|
+
kernel remains the active implementation so existing CLP(Z) behavior does not
|
|
6431
|
+
regress.
|
|
6416
6432
|
|
|
6417
6433
|
Alongside its interop entries `call_nth/2`, `time/1`, and `.../2`,
|
|
6418
6434
|
`library(iso_ext)` also exports EyeProlog's extension relations `countall/2`,
|
|
@@ -7131,7 +7147,7 @@ Review questions:
|
|
|
7131
7147
|
</figure>
|
|
7132
7148
|
|
|
7133
7149
|
The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
|
|
7134
|
-
top-level directory contains **
|
|
7150
|
+
top-level directory contains **213 self-contained runnable programs**. Every
|
|
7135
7151
|
source program has an exact answer file under
|
|
7136
7152
|
[examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
|
|
7137
7153
|
explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic tables below link every top-level program and open the program
|