eyeprolog 1.3.75 → 1.3.77

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.
Files changed (40) hide show
  1. package/README.md +6 -6
  2. package/examples/binomial-vandermonde.pl +1 -1
  3. package/examples/book/chapter-26/02-triple.pl +1 -1
  4. package/examples/book/chapter-28/02-triangle.pl +1 -1
  5. package/examples/book/chapter-28/04-integer_rectangle.pl +1 -1
  6. package/examples/book/chapter-29/01-counterexample_to_odd_square.pl +1 -1
  7. package/examples/book/chapter-31/03-double.pl +1 -1
  8. package/examples/book/chapter-32/04-reference_square.pl +1 -1
  9. package/examples/book/chapter-33/03-chosen_pair.pl +1 -1
  10. package/examples/book/chapter-39/01-answer.pl +1 -1
  11. package/examples/catalan-convolution.pl +1 -1
  12. package/examples/collatz-1000.pl +1 -1
  13. package/examples/continued-fraction-sqrt2.pl +1 -1
  14. package/examples/easter-computus.pl +1 -1
  15. package/examples/equality-saturation.pl +1 -1
  16. package/examples/goldbach.pl +1 -1
  17. package/examples/integer-partitions.pl +1 -1
  18. package/examples/job-shop-scheduling.pl +1 -1
  19. package/examples/matrix-chain-order.pl +1 -1
  20. package/examples/missionaries-cannibals.pl +1 -1
  21. package/examples/pell-equation.pl +1 -1
  22. package/examples/polynomial.pl +1 -1
  23. package/examples/prime-range.pl +1 -1
  24. package/examples/stirling-bell-numbers.pl +1 -1
  25. package/examples/totient-summatory.pl +1 -1
  26. package/package.json +1 -1
  27. package/src/iso.js +42 -17
  28. package/src/lib/lists.pl +15 -5
  29. package/src/lib/prologue.pl +30 -166
  30. package/src/program.js +20 -8
  31. package/src/quads.js +24 -3
  32. package/src/repl.js +10 -2
  33. package/src/solver.js +5 -5
  34. package/src/standard-library.js +5 -6
  35. package/src/term.js +4 -3
  36. package/test/fixtures/README.md +8 -0
  37. package/test/fixtures/variable_names_quad.pl +163 -0
  38. package/test/run-regression.mjs +88 -7
  39. package/test/test-support.mjs +1 -1
  40. package/the-art-of-eyeprolog.md +30 -23
package/src/quads.js CHANGED
@@ -110,10 +110,15 @@ function checkAlternative(program, quad, alternative, options, context) {
110
110
  const actual = executeQuery(program, quad.query, input, maxSolutions, {
111
111
  ...options,
112
112
  detectLoops: leaves.some((leaf) => leaf.loops),
113
+ detectWaits: leaves.some((leaf) => leaf.waits),
113
114
  inputBoundary: hasInputSpec,
114
115
  inputPeek: peek,
115
116
  });
116
117
 
118
+ if (leaves.some((leaf) => leaf.waits)) {
119
+ return { ok: leaves.length === 1 && actual.inputWaitObserved };
120
+ }
121
+
117
122
  if (requiresSto) {
118
123
  // Trealla currently treats the answer part of an `sto`-annotated leaf as
119
124
  // implementation-dependent and skips it. EyeProlog can strengthen that
@@ -199,6 +204,7 @@ function describeLeaf(term) {
199
204
  false: false,
200
205
  truth: false,
201
206
  loops: false,
207
+ waits: false,
202
208
  error: null,
203
209
  input: null,
204
210
  peek: null,
@@ -213,7 +219,8 @@ function describeLeaf(term) {
213
219
  else if (item.name === '...' || item.name === 'ad_infinitum') leaf.more = true;
214
220
  else if (item.name === 'sto') leaf.sto = true;
215
221
  else if (item.name === 'loops') leaf.loops = true;
216
- else if (item.name === 'waits' || item.name === 'other_answer_sequence') {
222
+ else if (item.name === 'waits') leaf.waits = true;
223
+ else if (item.name === 'other_answer_sequence') {
217
224
  leaf.unsupported ??= item;
218
225
  } else if (item.name === 'false') leaf.false = true;
219
226
  else if (item.name === 'true') leaf.truth = true;
@@ -246,7 +253,8 @@ function describeLeaf(term) {
246
253
  if (isErrorDescription(item)) leaf.error = item;
247
254
  else leaf.malformed ??= item;
248
255
  }
249
- leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.loops || leaf.error != null || leaf.output != null;
256
+ leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.loops || leaf.waits ||
257
+ leaf.error != null || leaf.output != null;
250
258
  if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
251
259
  if ([leaf.false, leaf.truth, leaf.error != null].filter(Boolean).length > 1) leaf.malformed ??= term;
252
260
  return leaf;
@@ -254,6 +262,7 @@ function describeLeaf(term) {
254
262
 
255
263
  function executeQuery(program, query, input, maxSolutions, options) {
256
264
  let pendingOutput = '';
265
+ let inputWaitObserved = false;
257
266
  const boundedInput = options.inputBoundary
258
267
  ? `${input}${options.inputPeek ?? ''}${INVALID_UTF8_SENTINEL}`
259
268
  : input;
@@ -274,13 +283,24 @@ function executeQuery(program, query, input, maxSolutions, options) {
274
283
  write: (text) => { pendingOutput += String(text); },
275
284
  },
276
285
  });
286
+ if (options.detectWaits) {
287
+ const inputStream = solver.io.resolve('user_input');
288
+ const observeWait = () => {
289
+ inputWaitObserved = true;
290
+ return null;
291
+ };
292
+ inputStream.interactiveReadTerm = observeWait;
293
+ inputStream.interactiveReadUnit = observeWait;
294
+ }
277
295
  if (options.inputBoundary) {
278
296
  // Quads need a boundary that is neither EOF nor a valid character. This
279
297
  // mirrors Trealla's sentinel technique: inputs/1 says exactly what was
280
298
  // consumed, while peeks/1 supplies one additional character that must
281
299
  // remain unread. If the query asks for anything beyond that boundary it
282
300
  // sees representation_error(character), not an artificial EOF.
283
- solver.io.resolve('user_input').strictUtf8 = true;
301
+ const inputStream = solver.io.resolve('user_input');
302
+ inputStream.strictUtf8 = true;
303
+ inputStream.syntheticInputBoundary = input.length;
284
304
  }
285
305
  // Undefined predicates are test failures rather than silent negative
286
306
  // answers unless the source explicitly selected another unknown policy.
@@ -338,6 +358,7 @@ function executeQuery(program, query, input, maxSolutions, options) {
338
358
  inputRemainder,
339
359
  complete,
340
360
  loopObserved,
361
+ inputWaitObserved,
341
362
  stoObserved: solver.occursCheckObserved,
342
363
  nstoObserved: complete && !solver.occursCheckObserved && !loopObserved &&
343
364
  !bounded && !resourceInterrupted,
package/src/repl.js CHANGED
@@ -722,6 +722,12 @@ function formatAnswer(engine, state, variables, env) {
722
722
  : equality.specifier === 'xfy' ? equality.priority : equality.priority - 1;
723
723
  let generated = 0;
724
724
 
725
+ // Meta-predicate wrappers can make a query variable alias an internal fresh
726
+ // variable. Keep residual constraints tied to the query's visible name.
727
+ for (const variable of variables) {
728
+ const root = engine.deref(variable, env);
729
+ if (root.type === 'var' && !names.has(root.name)) names.set(root.name, variable.name);
730
+ }
725
731
  for (const variable of variables) collectUnboundVariables(engine, variable, env, names, () => `_${letterName(generated++)}`);
726
732
  for (const variable of variables) {
727
733
  const value = engine.deref(variable, env);
@@ -797,7 +803,7 @@ function letterName(index) {
797
803
  }
798
804
 
799
805
  function formatError(engine, state, error) {
800
- if (error?.name === 'PrologError') {
806
+ if (error?.name === 'PrologError' || (error?.name === 'ThrownTerm' && error.term != null)) {
801
807
  const env = new engine.Env();
802
808
  const variableNames = new Map();
803
809
  let generated = 0;
@@ -805,7 +811,9 @@ function formatError(engine, state, error) {
805
811
  // Reuse the same conversion as catch/3 so uncaught errors at the top
806
812
  // level cannot lose the implementation-defined context or misplace a
807
813
  // culprit as the second argument of error/2.
808
- const term = formalErrorTerm(error);
814
+ const term = error.name === 'ThrownTerm'
815
+ ? engine.compound('throw', [error.term])
816
+ : formalErrorTerm(error);
809
817
  collectUnboundVariables(engine, term, env, variableNames, () => `_${letterName(generated++)}`);
810
818
  return `${engine.formatTermForWrite(term, env, {
811
819
  quoted: true,
package/src/solver.js CHANGED
@@ -1428,12 +1428,12 @@ function pushWfsAnswerFrames(stack, model, group, goal, rest, env, depth, active
1428
1428
 
1429
1429
  function bundledBetweenIterator(solver, group, goal, env) {
1430
1430
  if (solver.registry.eyePrologLibrary !== true ||
1431
- group.module !== 'prologue' || group.name !== 'between' || group.arity !== 3 ||
1431
+ group.module !== 'between' || group.name !== 'between' || group.arity !== 3 ||
1432
1432
  group.bundledLibrary !== true || group.clauses.length !== 1) {
1433
1433
  return null;
1434
1434
  }
1435
1435
 
1436
- // The portable Prologue definition is intentionally kept as the semantic
1436
+ // The portable library(between) definition is kept as the semantic
1437
1437
  // source of between/3. Its recursive helper, however, carries the output
1438
1438
  // variable through one fresh clause instance per integer. With persistent
1439
1439
  // environments that builds a growing variable-alias chain, so dereferencing
@@ -1475,7 +1475,7 @@ function* bundledBetweenSolutions(solver, goal, env) {
1475
1475
 
1476
1476
  function bundledMemberIterator(solver, group, goal, env) {
1477
1477
  if (solver.registry.eyePrologLibrary !== true ||
1478
- !['lists', 'prologue'].includes(group.module) || group.name !== 'member' || group.arity !== 2 ||
1478
+ group.module !== 'lists' || group.name !== 'member' || group.arity !== 2 ||
1479
1479
  group.bundledLibrary !== true || group.clauses.length !== 2) {
1480
1480
  return null;
1481
1481
  }
@@ -1588,7 +1588,7 @@ function* bundledEllipsisSolutions(solver, output, input, env) {
1588
1588
 
1589
1589
  function bundledLengthIterator(solver, group, goal, env) {
1590
1590
  if (solver.registry.eyePrologLibrary !== true ||
1591
- !['lists', 'prologue'].includes(group.module) || group.name !== 'length' || group.arity !== 2 ||
1591
+ group.module !== 'lists' || group.name !== 'length' || group.arity !== 2 ||
1592
1592
  group.bundledLibrary !== true || group.clauses.length !== 2) {
1593
1593
  return null;
1594
1594
  }
@@ -1900,7 +1900,7 @@ function findallLengthFusion(solver, goal, rest, env) {
1900
1900
  if (lengthGoal.type !== COMPOUND || lengthGoal.name !== 'length' || lengthGoal.arity !== 2) return null;
1901
1901
  const lengthGroup = solver.program.findGroup('length', 2, lengthGoal.module ?? 'user');
1902
1902
  if (!lengthGroup || lengthGroup.bundledLibrary !== true ||
1903
- !['lists', 'prologue'].includes(lengthGroup.module)) return null;
1903
+ lengthGroup.module !== 'lists') return null;
1904
1904
  const lengthList = derefForLocal(lengthGoal.args[0], env);
1905
1905
  const countArg = derefForLocal(lengthGoal.args[1], env);
1906
1906
  if (lengthList.type !== 'var' || lengthList.name !== bag.name || countArg.type !== 'var') return null;
@@ -141,11 +141,10 @@ export const eyePrologInteropAutoload = Object.freeze({
141
141
  // unchanged while their implementations remain outside the ISO core.
142
142
  'time/1': 'iso_ext',
143
143
  '.../2': 'iso_ext',
144
- // Trealla and Scryer expose between/3 without an EyeProlog-style
145
- // library(prologue) dependency. EyeProlog keeps its implementation in the
146
- // Prologue module but autoloads it so portable source need not name that
147
- // implementation-specific library.
148
- 'between/3': 'prologue',
144
+ // Scryer provides library(between), while Trealla exposes the relation as a
145
+ // system predicate. Keep EyeProlog's canonical portable owner in the
146
+ // specialized module; library(prologue) re-exports it for compatibility.
147
+ 'between/3': 'between',
149
148
  });
150
149
 
151
150
  export const eyePrologInteropLibraryIndicators = Object.freeze(
@@ -155,7 +154,7 @@ export const eyePrologInteropLibraryIndicators = Object.freeze(
155
154
  // Libraries whose *name* is part of the current interop profile. A program
156
155
  // may freely use_module/1 with these common module names; predicates in those
157
156
  // modules outside eyePrologInteropLibraryIndicators are still diagnosed when used.
158
- export const eyePrologInteropLibraryModules = Object.freeze(['lists', 'iso_ext', 'lambda', 'atts']);
157
+ export const eyePrologInteropLibraryModules = Object.freeze(['lists', 'iso_ext', 'lambda', 'atts', 'freeze']);
159
158
 
160
159
  function* tabledNegationBuiltin({ solver, goal, env }) {
161
160
  yield* solver.solveTabledNegation(goal.args[0], env);
package/src/term.js CHANGED
@@ -650,6 +650,7 @@ export function unify(left, right, env, options = {}) {
650
650
  // unification: a variable cannot be bound to a term containing itself.
651
651
  // Bindings are written into the supplied Env.
652
652
  const occursCheckHandler = options.occursCheck === 'fail' ? null : env?._occursCheckHandler;
653
+ const runAttributeHooks = options.skipAttributeHooks !== true;
653
654
  // Callers may provide a proof that selected variables cannot occur in the
654
655
  // term they are about to receive. Source-level first-use analysis and a few
655
656
  // construction fast paths share this internal proof; ordinary unification
@@ -663,7 +664,7 @@ export function unify(left, right, env, options = {}) {
663
664
 
664
665
  if (a.type === VAR && b.type === VAR && a.name === b.name) continue;
665
666
  if (a.type === VAR && b.type === VAR) {
666
- if (env?._prologAttributes != null) {
667
+ if (runAttributeHooks && env?._prologAttributes != null) {
667
668
  if (!env.preparePrologAttributeUnification(a, b)) return false;
668
669
  a = deref(a, env);
669
670
  b = deref(b, env);
@@ -694,7 +695,7 @@ export function unify(left, right, env, options = {}) {
694
695
  occursCheckHandler?.(a, b, env);
695
696
  return false;
696
697
  }
697
- if (env?._prologAttributes != null && env.hasPrologAttributes(a.name)) {
698
+ if (runAttributeHooks && env?._prologAttributes != null && env.hasPrologAttributes(a.name)) {
698
699
  if (!env.preparePrologAttributeUnification(a, b)) return false;
699
700
  a = deref(a, env);
700
701
  b = deref(b, env);
@@ -712,7 +713,7 @@ export function unify(left, right, env, options = {}) {
712
713
  occursCheckHandler?.(b, a, env);
713
714
  return false;
714
715
  }
715
- if (env?._prologAttributes != null && env.hasPrologAttributes(b.name)) {
716
+ if (runAttributeHooks && env?._prologAttributes != null && env.hasPrologAttributes(b.name)) {
716
717
  if (!env.preparePrologAttributeUnification(b, a)) return false;
717
718
  b = deref(b, env);
718
719
  a = deref(a, env);
@@ -9,6 +9,14 @@ Retrieved on 2026-08-11. It is vendored so the regression suite exercises all
9
9
  58 quads without depending on network access or availability of the source
10
10
  server.
11
11
 
12
+ `variable_names_quad.pl` is an unmodified snapshot of the 75 machine-readable
13
+ quads for the ISO read and write option `variable_names/1`:
14
+
15
+ <https://www.complang.tuwien.ac.at/ulrich/iso-prolog/variable_names_quad.pl>
16
+
17
+ Retrieved on 2026-08-25. It is vendored so all input, output, waiting, and
18
+ error cases remain release-gated.
19
+
12
20
  `prologue_quad.pl` is an unmodified snapshot of the 33 machine-readable quads
13
21
  for the predicates proposed by the Prolog Prologue working draft:
14
22
 
@@ -0,0 +1,163 @@
1
+ 1 ?- write_term(T,[quoted(true),variable_names([N=T])]).
2
+ instantiation_error.
3
+ 2 ?- N = 'X', /**/write_term(T,[quoted(true),variable_names([N=T])]).
4
+ outputs("X"), N = 'X'.
5
+ outputs("X"), N = ... . % Same, ignoring 'X'
6
+ 3 ?- N = T, /**/write_term(T,[quoted(true),variable_names([N=T])]).
7
+ instantiation_error.
8
+ 4 ?- N = '_', /**/write_term(T,[quoted(true),variable_names([N=T])]).
9
+ outputs("_"), N = ... .
10
+ 65 ?- N = '_/*.*/', /**/write_term(T,[quoted(true),variable_names([N=T])]).
11
+ outputs("_/*.*/"), N = ... .
12
+ 5 ?- N = x, /**/write_term(T,[quoted(true),variable_names([N=T])]).
13
+ outputs("x"), N = ... .
14
+ 6 ?- N = 'x+y', /**/write_term(T,[quoted(true),variable_names([N=T])]).
15
+ outputs("x+y"), N = ... .
16
+ 50 ?- N = '))', /**/write_term(T,[quoted(true),variable_names([N=T])]).
17
+ outputs("))"), N = ... .
18
+ 7 ?- N = 7, /**/write_term(T,[quoted(true),variable_names([N=T])]).
19
+ domain_error(write_option, variable_names(...)).
20
+ 8 ?- N = 1+2, /**/write_term(T,[quoted(true),variable_names([N=T])]).
21
+ domain_error(write_option, variable_names(...)).
22
+ 9 ?- N = '$VAR'(9), /**/write_term(T,[quoted(true),variable_names([N=T])]).
23
+ domain_error(write_option, variable_names(...)).
24
+ 10 ?- T = a, /**/write_term(T,[quoted(true),variable_names([N=T])]).
25
+ instantiation_error.
26
+ 11 ?- T = a, N = 'Any', /**/write_term(T,[quoted(true),variable_names([N=T])]).
27
+ outputs("a"), T = ..., N = ... .
28
+ 12 ?- T = '$VAR'(9), N = '_', /**/write_term(T,[quoted(true),variable_names([N=T])]).
29
+ outputs("'$VAR'(9)"), T = ..., N = ... .
30
+ 74 ?- T = f(_), N = 'Bad', /**/write_term(T,[quoted(true),variable_names([N=T])]).
31
+ outputs(("f(_",...,")")), T = ..., N = ... .
32
+ 28 ?- freeze(T,throw(g(T))), N = 'X', /**/write_term(T,[quoted(true),variable_names([N=T])]), false.
33
+ outputs("X"), false.
34
+ 13 ?- write_term(T,[quoted(true),variable_names(['X'=X,'Y'=Y,'Z'=Z])]).
35
+ outputs(("_",...)).
36
+ 14 ?- T=(X,Y,Z), /**/write_term(T,[quoted(true),variable_names(['X'=X,'Y'=Y,'Z'=Z])]).
37
+ outputs("X,Y,Z"), T = ... .
38
+ 15 ?- Z=Y, T=(X,Y,Z), /**/write_term(T,[quoted(true),variable_names(['X'=X,'Y'=Y,'Z'=Z])]).
39
+ outputs("X,Y,Y"), Z = Y, T = ... .
40
+ 16 ?- Z=Y, Y=X, T=(X,Y,Z), /**/write_term(T,[quoted(true),variable_names(['X'=X,'Y'=Y,'Z'=Z])]).
41
+ outputs("X,X,X"), Z = X, Y = X, T = ... .
42
+ 17 ?- T=(Y,Z), /**/write_term(T,[quoted(true),variable_names(['X'=X,'Y'=Y,'Z'=Z])]).
43
+ outputs("Y,Z"), T = ... .
44
+ 18 ?- T=(Z,Y), /**/write_term(T,[quoted(true),variable_names(['X'=X,'Y'=Y,'Z'=Z])]).
45
+ outputs("Z,Y"), T = ... .
46
+ 19 ?- write_term(T,[quoted(true),variable_names(['Z'=Z,'Y'=Y,'X'=X])]).
47
+ outputs(("_",...)).
48
+ 20 ?- T=(X,Y,Z), /**/write_term(T,[quoted(true),variable_names(['Z'=Z,'Y'=Y,'X'=X])]).
49
+ outputs("X,Y,Z"), T = ... .
50
+ 21 ?- Z=Y, T=(X,Y,Z), /**/write_term(T,[quoted(true),variable_names(['Z'=Z,'Y'=Y,'X'=X])]).
51
+ outputs("X,Z,Z"), Z = Y, T = ... .
52
+ 22 ?- Z=Y, Y=X, T=(X,Y,Z), /**/write_term(T,[quoted(true),variable_names(['Z'=Z,'Y'=Y,'X'=X])]).
53
+ outputs("Z,Z,Z"), Z = X, Y = X, T = ... .
54
+ 23 ?- T=(Y,Z), /**/write_term(T,[quoted(true),variable_names(['Z'=Z,'Y'=Y,'X'=X])]).
55
+ outputs("Y,Z"), T = ... .
56
+ 24 ?- T=(Z,Y), /**/write_term(T,[quoted(true),variable_names(['Z'=Z,'Y'=Y,'X'=X])]).
57
+ outputs("Z,Y"), T = ... .
58
+ 25 ?- write_term(T,[quoted(true),variable_names(['X'=Z,'X'=Y,'X'=X])]).
59
+ outputs(("_",...)).
60
+ 26 ?- T=(X,Y,Z), /**/write_term(T,[quoted(true),variable_names(['X'=Z,'X'=Y,'X'=X])]).
61
+ outputs("X,X,X"), T = ... .
62
+ 27 ?- T=(1,2,3), T=(X,Y,Z), /**/write_term(T,[quoted(true),variable_names(['X'=Z,'X'=Y,'X'=X])]).
63
+ outputs("1,2,3"), T = ..., X = 1, Y = 2, Z = 3.
64
+ 32 ?- read_term(T,[variable_names(VN_list)]),VN_list=[_=1,_=2,_=3],writeq(VN_list).
65
+ waits.
66
+ 29 ?- /**/read_term(T,[variable_names(VN_list)]),VN_list=[_=1,_=2,_=3],writeq(VN_list).
67
+ inputs("B+C+A+B+C+A."), peeks("\n"),
68
+ outputs("['B'=1,'C'=2,'A'=3]"),
69
+ T = 1+2+3+1+2+3,
70
+ VN_list=['B'=1,'C'=2,'A'=3].
71
+ 30 ?- write_term(T,[variable_names(VN_list)]).
72
+ instantiation_error.
73
+ 31 ?- VN_list = 1, /**/write_term(T,[variable_names(VN_list)]).
74
+ domain_error(write_option, variable_names(1)).
75
+ 33 ?- VN_list = [[]], /**/write_term(T,[variable_names(VN_list)]).
76
+ domain_error(write_option, variable_names([[]])).
77
+ 34 ?- VN_list = non_list, /**/write_term(T,[variable_names(VN_list)]).
78
+ domain_error(write_option, variable_names(non_list)).
79
+ 35 ?- VN_list = [T='T'|non_list], /**/write_term(T,[variable_names(VN_list)]).
80
+ domain_error(write_option, variable_names([_='T'|non_list]))
81
+ | instantiation_error.
82
+ 52 ?- VN_list = ['T'=T|_], /**/write_term(T,[variable_names(VN_list)]).
83
+ instantiation_error.
84
+ 51 ?- VN_list = ['T'=T|non_list], /**/write_term(T,[variable_names(VN_list)]).
85
+ domain_error(write_option, variable_names(...)).
86
+ 36 ?- VN_list = [T-'T'], /**/write_term(T,[variable_names(VN_list)]).
87
+ domain_error(write_option, variable_names(...)).
88
+ 63 ?- VN_list = [_,a], /**/write_term(T,[variable_names(VN_list)]).
89
+ domain_error(write_option, variable_names(...))
90
+ | instantiation_error.
91
+ 64 ?- VN_list = [a,_], /**/write_term(T,[variable_names(VN_list)]).
92
+ domain_error(write_option, variable_names(...))
93
+ | instantiation_error.
94
+ 66 ?- VN_list = [a|_], /**/write_term(T,[variable_names(VN_list)]).
95
+ domain_error(write_option, variable_names(...))
96
+ | instantiation_error.
97
+ 67 ?- VN_list = [i=i,7=i], /**/write_term(T,[variable_names(VN_list)]).
98
+ domain_error(write_option,variable_names([i=i,7=i])).
99
+ 68 ?- VN_list = [_,_], /**/write_term(T,[variable_names(VN_list)]).
100
+ instantiation_error.
101
+ 43 ?- write_term(-X^2,[variable_names(['X'=X])]).
102
+ outputs("- (X^2)").
103
+ 44 ?- X=1, /**/write_term(-X^2,[variable_names(['X'=X])]).
104
+ outputs("- (1^2)"), X = ... .
105
+ 37 ?- open(f,write,_,[O]).
106
+ instantiation_error.
107
+ 38 ?- O = 1, /**/open(f,write,_,[O]).
108
+ domain_error(stream_option,1).
109
+ 56 ?- O = typex(_), /**/open(f,write,_,[O]).
110
+ domain_error(stream_option,typex(_)).
111
+ 57 ?- O = typex(1), /**/open(f,write,_,[O]).
112
+ domain_error(stream_option,typex(1)).
113
+ 62 ?- O = typex(s(_)), /**/open(f,write,_,[O]).
114
+ domain_error(stream_option,typex(s(_))).
115
+ 39 ?- O = type(text), /**/open(f,write,_,[O]).
116
+ O = ... .
117
+ 40 ?- O = type(1), /**/open(f,write,_,[O]).
118
+ domain_error(stream_option,type(1)).
119
+ 41 ?- O = type(_), /**/open(f,write,_,[O]).
120
+ instantiation_error.
121
+ 60 ?- O = alias(_), /**/open(f,write,_,[O]).
122
+ instantiation_error.
123
+ 42 ?- O = type(nontype), /**/open(f,write,_,[O]).
124
+ domain_error(stream_option,type(nontype)).
125
+ 61 ?- O = alias(1), /**/open(f,write,_,[O]).
126
+ domain_error(stream_option,alias(1)).
127
+ 45 ?- read_term(T,[variable_names(VN_list)]).
128
+ waits.
129
+ 46 ?- /**/read_term(T,[variable_names(VN_list)]).
130
+ inputs("a."), peeks("\n"),
131
+ T = a, VN_list = [].
132
+ 47 ?- VN_list = 42, /**/read_term(T,[variable_names(VN_list)]).
133
+ waits.
134
+ 48 ?- VN_list = 42, /**/read_term(T,[variable_names(VN_list)]).
135
+ inputs("a."), peeks("\n"),
136
+ false.
137
+ 49 ?- VN_list = 42, /**/read_term(T,[variable_names(VN_list)]).
138
+ inputs("a b."), peeks("\n"),
139
+ syntax_error(...).
140
+ 53 ?- write_term(S,[quoted(true),variable_names([N=T])]).
141
+ instantiation_error.
142
+ 54 ?- S=1+T,N='/*r*/V',/**/write_term(S,[quoted(true),variable_names([N=T])]).
143
+ outputs("1+/*r*/V"), S = ..., N = ...
144
+ | outputs("1+ /*r*/V"), S = ..., N = ... .
145
+ 55 ?- S=1+T,N=' /*r*/V',/**/write_term(S,[quoted(true),variable_names([N=T])]).
146
+ outputs("1+ /*r*/V"), S = ..., N = ... .
147
+ 58 ?- S=1+T,N=(+),/**/write_term(S,[quoted(true),variable_names([N=T])]).
148
+ outputs("1++"), S = ..., N = ...
149
+ | outputs("1+ +"), S = ..., N = ... .
150
+ 59 ?- S=T+1,N=(+),/**/write_term(S,[quoted(true),variable_names([N=T])]).
151
+ outputs("++1"), S = ..., N = ...
152
+ | outputs("+ +1"), S = ..., N = ... .
153
+ 73 ?- S=(1 is T),N='X',/**/write_term(S,[quoted(true),variable_names([N=T])]).
154
+ outputs("1 is X"), S = ..., N = ... .
155
+ 69 ?- read_term(T,[singletons(1)]).
156
+ waits.
157
+ 70 ?- /**/read_term(T,[singletons(1)]).
158
+ inputs("a."), peeks("\n"),
159
+ false.
160
+ 71 ?- write_term(T,[variable_names(['Bad'=T]),variable_names(['Good'=T])]).
161
+ outputs("Good").
162
+ 72 ?- read_term(T,[singletons([])]).
163
+ waits.
@@ -127,6 +127,17 @@ function regressionCases() {
127
127
  'scalar-fact fast matching still validates annotated variables');
128
128
  assertEqual(run('p(b).', { goal: 'dif(X,a),p(X)' }).stats.completed_goal_lists, 1,
129
129
  'scalar-fact fast matching can discharge an annotated disequality');
130
+
131
+ const freezeProgram = Program.parse(':- use_module(library(freeze), [freeze/2]).');
132
+ const freezeSolver = new Solver(freezeProgram);
133
+ const freezeDifGoal = parseGoalText('freeze(X,a),dif(X,c)');
134
+ const freezeDifAnswer = freezeSolver.solve([freezeDifGoal], new Env(), 0).next();
135
+ assertEqual(freezeDifAnswer.done, false, 'freeze/2 and dif/2 answer');
136
+ const freezeResiduals = freezeSolver.attributeResidualGoals(
137
+ freezeDifGoal.args[0].args[0], freezeDifAnswer.value,
138
+ );
139
+ assertEqual(freezeResiduals.length, 1,
140
+ 'freeze/2 residual projection terminates alongside dif/2 (issue #73)');
130
141
  },
131
142
  },
132
143
  {
@@ -190,6 +201,7 @@ check :- call_residue_vars(X in 1..3, Vs), Vs = [X].
190
201
 
191
202
  const residualFile = path.join(tmp, `atts-residual-${++tmpCounter}.pl`);
192
203
  fs.writeFileSync(residualFile, `:- use_module(library(atts)).
204
+ :- use_module(library(freeze), [freeze/2]).
193
205
  :- attribute required/1.
194
206
  attach(X,V) :- put_atts(X, required(V)).
195
207
  verify_attributes(Var, Other, Goals) :-
@@ -199,9 +211,13 @@ verify_attributes(Var, Other, Goals) :-
199
211
  same(X,X).
200
212
  attribute_goals(X) --> { get_atts(X, required(V)) }, [required(X,V)].
201
213
  `);
202
- const repl = runCli([], { input: `[${sourceAtom(residualFile)}].\nattach(X,a).\nhalt.\n` });
214
+ const repl = runCli([], {
215
+ input: `[${sourceAtom(residualFile)}].\nattach(X,a).\nfreeze(Y,a),dif(Y,c).\nhalt.\n`,
216
+ });
203
217
  assertEqual(repl.status, 0, 'attribute residual REPL status');
204
218
  assertIncludes(repl.stdout, 'required(X, a).', 'attribute_goals//1 residual projection');
219
+ assertIncludes(repl.stdout, 'dif(Y, c), freeze:freeze(Y, a).',
220
+ 'freeze/2 and dif/2 residuals share the query variable (issue #73)');
205
221
 
206
222
  const scryerShape = Program.parse(`:- module(scryer_shape, [op(700, xfx, #=), probe/0]).
207
223
  :- op(700, xfx, #=).
@@ -1549,6 +1565,18 @@ c4 ?- call((!;1)).
1549
1565
  assertEqual(result.stderr, '', 'quad stderr');
1550
1566
  },
1551
1567
  },
1568
+ {
1569
+ name: 'CLI passes the complete authoritative variable_names quad corpus (issue #69)',
1570
+ run: () => {
1571
+ const filename = path.join(testRoot, 'fixtures', 'variable_names_quad.pl');
1572
+ const source = fs.readFileSync(filename, 'utf8');
1573
+ assertEqual(Program.parse(source).quads.length, 74, 'vendored query-record total');
1574
+ const result = runCli(['-q', filename], { cwd: tmp });
1575
+ assertEqual(result.status, 0, 'quad exit status');
1576
+ assertEqual(result.stdout, 'quads: 75 run, 75 passed, 0 failed.\n', 'quad report');
1577
+ assertEqual(result.stderr, '', 'quad stderr');
1578
+ },
1579
+ },
1552
1580
  {
1553
1581
  name: 'REPL advances anonymous Prologue length checks through I = 28',
1554
1582
  run: () => {
@@ -1809,6 +1837,15 @@ c4 ?- call((!;1)).
1809
1837
  assertEqual(result.stderr, '', 'stderr');
1810
1838
  },
1811
1839
  },
1840
+ {
1841
+ name: 'REPL displays uncaught user exceptions as throw terms (issue #75)',
1842
+ run: () => {
1843
+ const result = runCli([], { input: 'throw(stopped).\nhalt.\n' });
1844
+ assertEqual(result.status, 0, 'exit status');
1845
+ assertEqual(result.stdout, '?- throw(stopped).\n?- ', 'stdout');
1846
+ assertEqual(result.stderr, '', 'stderr');
1847
+ },
1848
+ },
1812
1849
  {
1813
1850
  name: 'REPL freshens variables displayed in uncaught ISO errors',
1814
1851
  run: () => {
@@ -2814,6 +2851,22 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
2814
2851
  assertEqual(result.stderr, '', 'stderr');
2815
2852
  },
2816
2853
  },
2854
+ {
2855
+ name: 'REPL combines canonical libraries with the Prologue facade',
2856
+ run: () => {
2857
+ const result = runCli([], {
2858
+ input:
2859
+ 'use_module(library(freeze)).\n' +
2860
+ 'use_module(library(lists)).\n' +
2861
+ 'use_module(library(iso_ext)).\n' +
2862
+ 'use_module(library(prologue)).\n' +
2863
+ 'halt.\n',
2864
+ });
2865
+ assertEqual(result.status, 0, 'exit status');
2866
+ assertEqual(result.stdout, '?- true.\n?- true.\n?- true.\n?- true.\n?- ', 'stdout');
2867
+ assertEqual(result.stderr, '', 'stderr');
2868
+ },
2869
+ },
2817
2870
  {
2818
2871
  name: 'REPL halt status is returned by the CLI',
2819
2872
  run: () => {
@@ -4550,7 +4603,7 @@ answer(ok) :-
4550
4603
  const program = Program.parse('answer(X) :- member(X, [a]), between(1, 1, _).\n');
4551
4604
  assertEqual(program.autoloadedPredicates.length, 2, 'autoloaded predicate count');
4552
4605
  assertEqual(program.autoloadedPredicates.map((entry) => `${entry.indicator}:${entry.library}`).sort().join(','),
4553
- 'between/3:prologue,member/2:lists', 'autoload mapping');
4606
+ 'between/3:between,member/2:lists', 'autoload mapping');
4554
4607
  assertEqual(program.interopPortabilityWarnings.length, 0, 'autoloaded portable calls are warning-free');
4555
4608
  },
4556
4609
  },
@@ -5034,9 +5087,9 @@ answer(ok) :-
5034
5087
  assertEqual(registeredNativeEyePrologLibraryNames().length, 49, 'public native EyeProlog builtin count');
5035
5088
  assertEqual(eyePrologPortableLibraryIndicators.length, 87, 'portable Prolog library count');
5036
5089
  assertEqual(eyePrologInteropLibraryIndicators.length, 30, 'cross-implementation interop profile count');
5037
- assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda,atts', 'common explicit library module profile');
5090
+ assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda,atts,freeze', 'common explicit library module profile');
5038
5091
  assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
5039
- assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
5092
+ assertEqual(eyePrologInteropAutoload['between/3'], 'between', 'between/3 canonical internal autoload');
5040
5093
  assertEqual(eyePrologInteropAutoload['call_nth/2'], 'iso_ext', 'call_nth/2 canonical interop autoload');
5041
5094
  assertEqual(eyePrologInteropAutoload['call_residue_vars/2'], 'atts', 'call_residue_vars/2 canonical interop autoload');
5042
5095
  assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
@@ -5097,13 +5150,41 @@ answer(ok) :-
5097
5150
  assertEqual(program.findGroup('random', 3)?.module, 'random', 'random/3 is imported from library(random)');
5098
5151
  },
5099
5152
  },
5153
+ {
5154
+ name: 'Prologue facade re-exports canonical library predicates without conflicts',
5155
+ run: () => {
5156
+ const sources = [
5157
+ ':- use_module(library(freeze)).\n:- use_module(library(lists)).\n:- use_module(library(iso_ext)).\n:- use_module(library(prologue)).',
5158
+ ':- use_module(library(prologue)).\n:- use_module(library(freeze)).\n:- use_module(library(lists)).\n:- use_module(library(iso_ext)).',
5159
+ ];
5160
+ for (const source of sources) {
5161
+ const program = Program.parse(source);
5162
+ assertEqual(program.findGroup('freeze', 2)?.module, 'freeze', 'freeze/2 canonical owner');
5163
+ assertEqual(program.findGroup('member', 2)?.module, 'lists', 'member/2 canonical owner');
5164
+ assertEqual(program.findGroup('call_nth', 2)?.module, 'iso_ext', 'call_nth/2 canonical owner');
5165
+ assertEqual(program.findGroup('between', 3)?.module, 'between', 'between/3 canonical owner');
5166
+ }
5167
+
5168
+ const conflicting = new Program();
5169
+ conflicting.defineModule('first', [{ name: 'shared', arity: 1 }]);
5170
+ conflicting.defineModule('second', [{ name: 'shared', arity: 1 }]);
5171
+ conflicting.importModule('user', 'first');
5172
+ let conflict = null;
5173
+ try {
5174
+ conflicting.importModule('user', 'second');
5175
+ } catch (error) {
5176
+ conflict = error;
5177
+ }
5178
+ assertEqual(conflict?.formal, 'permission_error(import, procedure)', 'distinct implementations still conflict');
5179
+ },
5180
+ },
5100
5181
  {
5101
5182
  name: 'aligned bundled libraries have no overlapping full-module exports',
5102
5183
  run: () => {
5103
5184
  const owners = new Map();
5104
5185
  for (const [moduleName, entry] of standardLibrarySources) {
5105
- // library(prologue) is the documented legacy compatibility umbrella;
5106
- // aligned libraries must remain pairwise collision-free so full
5186
+ // library(prologue) is a facade whose nominal exports are re-exports;
5187
+ // canonical libraries must remain pairwise collision-free so full
5107
5188
  // use_module/1 imports can be combined safely.
5108
5189
  if (moduleName === 'prologue') continue;
5109
5190
  const parsed = Program.parse(entry.source);
@@ -6525,7 +6606,7 @@ child.on('close', (status, signal) => {
6525
6606
 
6526
6607
  function runCli(args, options = {}) {
6527
6608
  return spawnSync(process.execPath, [bin, ...args], {
6528
- cwd: packageRoot,
6609
+ cwd: options.cwd ?? packageRoot,
6529
6610
  encoding: 'utf8',
6530
6611
  env: options.env ? { ...process.env, ...options.env } : process.env,
6531
6612
  input: options.input ?? undefined,
@@ -10,7 +10,7 @@ const standardModulePrelude = `:- use_module(library(aggregate)).
10
10
  :- use_module(library(iso_ext)).
11
11
  :- use_module(library(lists)).
12
12
  :- use_module(library(primes)).
13
- :- use_module(library(prologue), [between/3]).
13
+ :- use_module(library(between), [between/3]).
14
14
  :- use_module(library(random)).
15
15
  :- use_module(library(strings)).
16
16
  :- use_module(library(uuid)).