eyeprolog 1.3.65 → 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 CHANGED
@@ -130,6 +130,43 @@ local/global variable stack. `phrase/2` likewise supplies its fixed `[]`
130
130
  remainder directly to the grammar; `phrase/3` retains its delayed final output
131
131
  unification for steadfastness.
132
132
 
133
+ ### Delayed disequality with `dif/2`
134
+
135
+ The normal EyeProlog runtime provides `dif/2` as a conforming extension;
136
+ `--iso-strict` remains limited to ISO/IEC 13211-1 and does not expose it.
137
+ Unlike `\=/2`, which only tests whether two terms are non-unifiable *now*,
138
+ `dif/2` can succeed with a residual disequality constraint and reject a later
139
+ binding that would make the terms identical. For example,
140
+ `dif(X, Y), X = Y` fails, while `dif(X-Y, 1-2), X = Y, Y = 1` succeeds once
141
+ the structural constraint becomes provably satisfied.
142
+
143
+ The implementation uses backtracking-safe annotated variables in `Env`. A
144
+ pending constraint is indexed by every currently unbound variable it mentions;
145
+ variable aliasing and later bindings reindex the annotation, entailed constraints
146
+ are removed, and violated constraints make unification fail. The interactive
147
+ top level includes pending `dif/2` goals in residual answers. See
148
+ [`examples/dif-constraints.pl`](examples/dif-constraints.pl).
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
+
133
170
  Recursion through negation is explicit. EyeProlog provides `tnot/1` for
134
171
  well-founded negation over finite, range-restricted, function-free Datalog
135
172
  components. Ordinary `\+/1` remains negation-as-failure and is not silently
@@ -258,13 +295,14 @@ be continued without rebuilding a full clause-resolution frame for every
258
295
  suffix. Open and remainder-producing uses still retain the ordinary DCG
259
296
  solutions and backtracking behavior.
260
297
 
261
- EyeProlog also adds 128 public library predicate indicators to its 129-entry ISO
262
- profile. **88 are implemented entirely as ordinary Prolog clauses** in focused
263
- modules under `src/lib/`; the remaining control predicates and finite-domain
264
- `library(clpz)` kernel use backtrackable host support.
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.
265
303
  They are ordinary Prolog modules using EyeProlog's documented module compatibility surface, loaded explicitly by purpose, such as
266
- `library(lists)`, `library(lambda)`, `library(strings)`, `library(aggregate)`, or
267
- `library(clpz)`.
304
+ `library(lists)`, `library(lambda)`, `library(strings)`, `library(aggregate)`,
305
+ `library(atts)`, or `library(clpz)`.
268
306
  Portable text
269
307
  predicates use ISO atoms or character lists. The old catch-all
270
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)].
@@ -0,0 +1,19 @@
1
+ % Delayed disequality constraints with dif/2.
2
+ %
3
+ % Unlike \=/2, dif/2 records a constraint when its arguments are still
4
+ % unifiable and rechecks that constraint after later bindings.
5
+
6
+ %% goal: allowed_pair(X, Y)
7
+ %% goal: specialization(X, Y)
8
+
9
+ allowed_pair(X, Y) :-
10
+ dif(X, Y),
11
+ X = left,
12
+ Y = right.
13
+
14
+ % The structural constraint remains pending after X=Y. Binding both to 1
15
+ % then proves X-Y and 1-2 different, so the constraint is discharged.
16
+ specialization(X, Y) :-
17
+ dif(X-Y, 1-2),
18
+ X = Y,
19
+ Y = 1.
@@ -0,0 +1,2 @@
1
+ accepts_required_value(ok).
2
+ alias_preserves_attribute(ok).
@@ -0,0 +1,2 @@
1
+ allowed_pair(left, right).
2
+ specialization(1, 1).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.65",
6
+ "version": "1.3.67",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/playground.html CHANGED
@@ -446,6 +446,7 @@
446
446
  "ancestor",
447
447
  "animal",
448
448
  "annotation",
449
+ "attributed-variables",
449
450
  "auroracare",
450
451
  "backward",
451
452
  "basic-monadic",
@@ -495,6 +496,7 @@
495
496
  "derived-backward-rule",
496
497
  "derived-rule",
497
498
  "diamond-property",
499
+ "dif-constraints",
498
500
  "dijkstra",
499
501
  "dijkstra-findall-sort",
500
502
  "dijkstra-risk-path",
@@ -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.
@@ -31,6 +31,23 @@ kernel component into the ISO registry or a frontend. In particular,
31
31
  report processor errors without importing `iso.js` and creating an
32
32
  `iso.js <-> dcg.js` cycle.
33
33
 
34
+ `term.js` also owns the generic annotated-variable storage carried by `Env`.
35
+ Annotations are persistent across `Env.clone()` and therefore backtrack with the
36
+ substitution. Language services may attach immutable constraint descriptors,
37
+ but the unconstrained unification hot path only performs a null check; descriptor
38
+ validation and reindexing run only in environments that actually contain
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`.
50
+
34
51
  The JavaScript runtime stays flat directly under `src/`; the existing `src/lib/`
35
52
  contains Prolog library sources rather than JavaScript runtime modules. The architecture
36
53
  test rejects JavaScript import cycles under `src/`. Cleanup lifecycle hooks are
@@ -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/iso.js CHANGED
@@ -167,6 +167,7 @@ export const eyePrologLibraryBuiltins = {
167
167
  registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
168
168
  registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
169
169
  registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
170
+ registry.add('dif', 2, difBuiltin, { deterministic: true, eyePrologLibrary: true });
170
171
  registry.add('time', 1, timeBuiltin, { eyePrologLibrary: true });
171
172
  registry.add('eyeprolog__time', 1, timeBuiltin, { eyePrologLibrary: true });
172
173
  },
@@ -189,6 +190,43 @@ function* nonUnification({ goal, env }) {
189
190
  if (!unify(goal.args[0], goal.args[1], env.clone())) yield env;
190
191
  }
191
192
 
193
+ function difConstraint(left, right) {
194
+ return Object.freeze({
195
+ kind: 'dif',
196
+ left,
197
+ right,
198
+ variables(env) {
199
+ return new Set([...termVariableNames(left, env), ...termVariableNames(right, env)]);
200
+ },
201
+ status(env) {
202
+ if (identical(left, right, env)) return 'violated';
203
+ const probe = env.clone();
204
+ return unify(left, right, probe, { skipVariableConstraints: true }) ? 'pending' : 'entailed';
205
+ },
206
+ residualGoal() {
207
+ return compound('dif', [left, right]);
208
+ },
209
+ });
210
+ }
211
+
212
+ function* difBuiltin({ goal, env }) {
213
+ const left = goal.args[0];
214
+ const right = goal.args[1];
215
+ if (identical(left, right, env)) return;
216
+
217
+ // In EyeProlog's finite-tree model, terms that cannot unify are already
218
+ // provably different and need no residual constraint. Otherwise attach a
219
+ // disequality constraint to every currently unbound variable it mentions.
220
+ const probe = env.clone();
221
+ if (!unify(left, right, probe, { skipVariableConstraints: true })) {
222
+ yield env;
223
+ return;
224
+ }
225
+ const next = env.clone();
226
+ next.addVariableConstraint(difConstraint(left, right));
227
+ yield next;
228
+ }
229
+
192
230
  function termVariableNames(term, env, names = new Set(), seen = new Set()) {
193
231
  term = deref(term, env);
194
232
  if (term.type === VAR) {
@@ -1646,7 +1684,7 @@ function writeOptionBoolean(value, env, option) {
1646
1684
  value = deref(value, env);
1647
1685
  if (value.type === VAR) throw new PrologError('instantiation_error');
1648
1686
  if (value.type !== ATOM || !['true', 'false'].includes(value.name)) {
1649
- throw new PrologError('domain_error(write_option)', option);
1687
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1650
1688
  }
1651
1689
  return value.name === 'true';
1652
1690
  }
@@ -1657,19 +1695,20 @@ function writeVariableNames(value, env, option) {
1657
1695
  const items = properListItems(value, env);
1658
1696
  if (items == null) {
1659
1697
  if (isPartialList(value, env)) throw new PrologError('instantiation_error');
1660
- throw new PrologError('domain_error(write_option)', option);
1698
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1661
1699
  }
1662
1700
  const names = new Map();
1663
1701
  for (const item of items) {
1664
1702
  const pair = deref(item, env);
1703
+ if (pair.type === VAR) throw new PrologError('instantiation_error');
1665
1704
  if (pair.type !== COMPOUND || pair.name !== '=' || pair.arity !== 2) {
1666
- throw new PrologError('domain_error(write_option)', option);
1705
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1667
1706
  }
1668
1707
  const name = deref(pair.args[0], env);
1669
1708
  const target = deref(pair.args[1], env);
1670
1709
  if (name.type === VAR) throw new PrologError('instantiation_error');
1671
1710
  if (name.type !== ATOM) {
1672
- throw new PrologError('domain_error(write_option)', option);
1711
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1673
1712
  }
1674
1713
  // Corrigendum 3 permits any term on the right. Only variables can name a
1675
1714
  // variable being written; retain the leftmost applicable entry.
@@ -1682,7 +1721,7 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
1682
1721
  const result = defaultTermWriteOptions(mode);
1683
1722
  for (const option of options) {
1684
1723
  if (option.type !== COMPOUND || option.arity !== 1) {
1685
- throw new PrologError('domain_error(write_option)', option);
1724
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1686
1725
  }
1687
1726
  if (option.name === 'quoted') result.quoted = writeOptionBoolean(option.args[0], env, option);
1688
1727
  else if (option.name === 'ignore_ops') result.ignoreOps = writeOptionBoolean(option.args[0], env, option);
@@ -1691,7 +1730,7 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
1691
1730
  else if (option.name === 'double_quotes' && !solver?.isoStrict) {
1692
1731
  result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
1693
1732
  } else {
1694
- throw new PrologError('domain_error(write_option)', option);
1733
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1695
1734
  }
1696
1735
  }
1697
1736
  return result;
@@ -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 || designation.args[0].name !== 'clpz') return;
376
- for (const [priority, specifier, name] of CLPZ_OPERATOR_DEFINITIONS) {
377
- this.defineOperator(priority, specifier, name);
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 directive = this.parseTerm();
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;