eyeprolog 1.5.59 → 1.5.60

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.59",
6
+ "version": "1.5.60",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -3219,6 +3219,11 @@ function* catchSolutions({ solver, goal, env }, state) {
3219
3219
  if (child != null) solver.absorbStatsFrom(child);
3220
3220
  }
3221
3221
  }
3222
+ // library(error) call_with_error_context/2, as a primitive rather than a
3223
+ // Prolog catch/3 wrapper (issue #98). Context elements are assembled only while
3224
+ // an error unwinds, so the success path pays for neither a clause resolution
3225
+ // nor a Prolog-level catch frame. The element is copied as it is added, so
3226
+ // variables in it cannot alias bindings that the unwinding undoes.
3222
3227
  function* throwBuiltin({ goal, env }) {
3223
3228
  const ball = deref(goal.args[0], env);
3224
3229
  if (ball.type === VAR) throw new PrologError('instantiation_error');
package/src/lib/error.pl CHANGED
@@ -17,56 +17,62 @@
17
17
 
18
18
  :- meta_predicate(call_with_error_context(0, +)).
19
19
 
20
+ % The context is supplied once here rather than handed over manually at every
21
+ % raise site: the raise sites throw with [] and this wrapper prepends its
22
+ % element, so contexts stay proper lists and compose with any enclosing
23
+ % call_with_error_context/2 (issue #98).
20
24
  must_be(Type, Term) :-
21
- ( var(Type) -> instantiation_error(must_be/2)
22
- ; error__must_be(Type, Term)
23
- ).
25
+ call_with_error_context(
26
+ ( var(Type) -> instantiation_error
27
+ ; error__must_be(Type, Term)
28
+ ),
29
+ must_be/2).
24
30
 
25
31
  error__must_be(integer, Term) :- !,
26
- ( var(Term) -> instantiation_error(must_be/2)
32
+ ( var(Term) -> instantiation_error
27
33
  ; integer(Term) -> true
28
- ; type_error(integer, Term, must_be/2)
34
+ ; type_error(integer, Term)
29
35
  ).
30
36
  error__must_be(atom, Term) :- !,
31
- ( var(Term) -> instantiation_error(must_be/2)
37
+ ( var(Term) -> instantiation_error
32
38
  ; atom(Term) -> true
33
- ; type_error(atom, Term, must_be/2)
39
+ ; type_error(atom, Term)
34
40
  ).
35
41
  error__must_be(number, Term) :- !,
36
- ( var(Term) -> instantiation_error(must_be/2)
42
+ ( var(Term) -> instantiation_error
37
43
  ; number(Term) -> true
38
- ; type_error(number, Term, must_be/2)
44
+ ; type_error(number, Term)
39
45
  ).
40
46
  error__must_be(var, Term) :- !,
41
47
  ( var(Term) -> true
42
- ; throw(error(uninstantiation_error(Term), must_be/2))
48
+ ; throw(error(uninstantiation_error(Term), []))
43
49
  ).
44
50
  error__must_be(ground, Term) :- !,
45
- ( ground(Term) -> true ; instantiation_error(must_be/2) ).
51
+ ( ground(Term) -> true ; instantiation_error ).
46
52
  error__must_be(acyclic, Term) :- !,
47
- ( acyclic_term(Term) -> true ; type_error(acyclic_term, Term, must_be/2) ).
53
+ ( acyclic_term(Term) -> true ; type_error(acyclic_term, Term) ).
48
54
  error__must_be(list, Term) :- !,
49
55
  error__proper_list(Term).
50
56
  error__must_be(list(Type), Term) :- !,
51
57
  error__proper_list_of(Term, Type).
52
58
  error__must_be(pair, Term) :- !,
53
- ( var(Term) -> instantiation_error(must_be/2)
59
+ ( var(Term) -> instantiation_error
54
60
  ; Term = _-_ -> true
55
- ; type_error(pair, Term, must_be/2)
61
+ ; type_error(pair, Term)
56
62
  ).
57
63
  error__must_be(not_less_than_zero, Term) :- !,
58
64
  must_be(integer, Term),
59
- ( Term >= 0 -> true ; domain_error(not_less_than_zero, Term, must_be/2) ).
65
+ ( Term >= 0 -> true ; domain_error(not_less_than_zero, Term) ).
60
66
  error__must_be(Type, Term) :-
61
- ( var(Term) -> instantiation_error(must_be/2)
62
- ; type_error(Type, Term, must_be/2)
67
+ ( var(Term) -> instantiation_error
68
+ ; type_error(Type, Term)
63
69
  ).
64
70
 
65
71
  error__proper_list([]) :- !.
66
72
  error__proper_list([_|Tail]) :- !, error__proper_list(Tail).
67
73
  error__proper_list(Term) :-
68
- ( var(Term) -> instantiation_error(must_be/2)
69
- ; type_error(list, Term, must_be/2)
74
+ ( var(Term) -> instantiation_error
75
+ ; type_error(list, Term)
70
76
  ).
71
77
 
72
78
  error__proper_list_of([], _) :- !.
@@ -74,8 +80,8 @@ error__proper_list_of([Head|Tail], Type) :- !,
74
80
  must_be(Type, Head),
75
81
  error__proper_list_of(Tail, Type).
76
82
  error__proper_list_of(Term, _) :-
77
- ( var(Term) -> instantiation_error(must_be/2)
78
- ; type_error(list, Term, must_be/2)
83
+ ( var(Term) -> instantiation_error
84
+ ; type_error(list, Term)
79
85
  ).
80
86
 
81
87
  can_be(Type, Term) :-
@@ -122,9 +128,9 @@ resource_error(Resource) :- throw(error(resource_error(Resource), [])).
122
128
  resource_error(Resource, Context) :- throw(error(resource_error(Resource), Context)).
123
129
 
124
130
  % Context elements are assembled only when an error actually propagates, so
125
- % the success path costs nothing. The added element is copied so that
126
- % variables in it are not shared with the goal's bindings, which would
127
- % otherwise be undone as the error unwinds.
131
+ % the prepend costs nothing on success. The enclosing catch/3 frame is not
132
+ % free, though: see issue #98 for measurements and the primitive design that
133
+ % would remove it.
128
134
  call_with_error_context(Goal, Pair) :-
129
135
  catch(Goal,
130
136
  error(Error, Context),
package/src/lib/random.pl CHANGED
@@ -10,7 +10,7 @@
10
10
  :- module(random, [maybe/0, maybe/1, maybe/2, random/1, random/3, random_integer/3, set_random/1]).
11
11
 
12
12
  :- use_module(library(iso_ext), [bb_get/2, bb_put/2]).
13
- :- use_module(library(error), [instantiation_error/1, type_error/3]).
13
+ :- use_module(library(error), [call_with_error_context/2, instantiation_error/0, type_error/2]).
14
14
 
15
15
  maybe :-
16
16
  random_integer(0, 2, 0).
@@ -28,15 +28,10 @@ maybe(K, N) :-
28
28
  random(Value) :-
29
29
  eyeprolog__random_value(Value).
30
30
 
31
+ % Context declared once per predicate instead of at each raise site, so the
32
+ % raise sites throw with [] and contexts stay proper composable lists.
31
33
  random_integer(Lower, Upper, R) :-
32
- ( var(Lower) -> instantiation_error(random_integer/3)
33
- ; var(Upper) -> instantiation_error(random_integer/3)
34
- ; integer(Lower) -> true
35
- ; type_error(integer, Lower, random_integer/3)
36
- ),
37
- ( integer(Upper) -> true
38
- ; type_error(integer, Upper, random_integer/3)
39
- ),
34
+ call_with_error_context(random__check_integer_range(Lower, Upper), random_integer/3),
40
35
  Lower < Upper,
41
36
  random__current_seed(Seed0),
42
37
  random(Seed0, _, Seed),
@@ -44,15 +39,28 @@ random_integer(Lower, Upper, R) :-
44
39
  R is Lower + Seed mod (Upper - Lower).
45
40
 
46
41
  set_random(Seed) :-
47
- ( var(Seed) -> instantiation_error(set_random/1)
42
+ call_with_error_context(random__set_seed(Seed), set_random/1).
43
+
44
+ random__check_integer_range(Lower, Upper) :-
45
+ ( var(Lower) -> instantiation_error
46
+ ; var(Upper) -> instantiation_error
47
+ ; integer(Lower) -> true
48
+ ; type_error(integer, Lower)
49
+ ),
50
+ ( integer(Upper) -> true
51
+ ; type_error(integer, Upper)
52
+ ).
53
+
54
+ random__set_seed(Seed) :-
55
+ ( var(Seed) -> instantiation_error
48
56
  ; Seed = seed(S) ->
49
- ( var(S) -> instantiation_error(set_random/1)
57
+ ( var(S) -> instantiation_error
50
58
  ; integer(S) ->
51
59
  random__random_normalize_seed(S, Normalized),
52
60
  bb_put('$random_seed', Normalized)
53
- ; type_error(integer, S, set_random/1)
61
+ ; type_error(integer, S)
54
62
  )
55
- ; type_error(random_state, Seed, set_random/1)
63
+ ; type_error(random_state, Seed)
56
64
  ).
57
65
 
58
66
  random__current_seed(Seed) :- bb_get('$random_seed', Seed), !.
package/src/repl.js CHANGED
@@ -861,8 +861,16 @@ function formatError(engine, state, error) {
861
861
  // Reuse the same conversion as catch/3 so uncaught errors at the top
862
862
  // level cannot lose the implementation-defined context or misplace a
863
863
  // culprit as the second argument of error/2.
864
+ // A thrown ball that is already an error/2 envelope is the same thing a
865
+ // built-in raises, so display it the same way. Wrapping only that case in
866
+ // throw/1 made one error print two different ways depending on whether the
867
+ // engine or Prolog code raised it. Other balls keep the wrapper, because
868
+ // throw(foo) has no error/2 envelope to show.
869
+ const thrownBall = error.name === 'ThrownTerm' ? engine.deref(error.term, env) : null;
870
+ const thrownIsErrorEnvelope = thrownBall != null
871
+ && thrownBall.type === 'compound' && thrownBall.name === 'error' && thrownBall.arity === 2;
864
872
  const term = error.name === 'ThrownTerm'
865
- ? engine.compound('throw', [error.term])
873
+ ? (thrownIsErrorEnvelope ? thrownBall : engine.compound('throw', [error.term]))
866
874
  : formalErrorTerm(error);
867
875
  collectUnboundVariables(engine, term, env, variableNames, () => `_${letterName(generated++)}`);
868
876
  return `${engine.formatTermForWrite(term, env, {
@@ -4476,6 +4476,40 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
4476
4476
  assertIncludes(result.stdout, 'answer([outer - 1, atom_length / 2])', 'composed context');
4477
4477
  },
4478
4478
  },
4479
+ {
4480
+ // Issue #98: library predicates declare their context once with
4481
+ // call_with_error_context/2 rather than handing it over manually at each
4482
+ // raise site. Manual handover produced a bare, non-list context, so it
4483
+ // composed into an improper list.
4484
+ name: 'library predicates yield composable contexts, not manual ones',
4485
+ run: () => {
4486
+ const input = ':- use_module(library(error)).\n:- use_module(library(random)).\n%% goal: answer(X)\n' +
4487
+ 'answer(C) :- catch(must_be(integer, a), error(_,C), true).\n';
4488
+ const result = runCli(['-'], { input });
4489
+ assertEqual(result.status, 0, `exit status; stderr=${result.stderr}`);
4490
+ assertIncludes(result.stdout, 'answer([must_be / 2])', 'must_be context is a list');
4491
+
4492
+ const composed = ':- use_module(library(error)).\n%% goal: answer(X)\n' +
4493
+ 'answer(C) :- catch(call_with_error_context(must_be(integer, a), outer-1), error(_,C), true).\n';
4494
+ const nested = runCli(['-'], { input: composed });
4495
+ assertEqual(nested.status, 0, `nested exit status; stderr=${nested.stderr}`);
4496
+ assertIncludes(nested.stdout, 'answer([outer - 1, must_be / 2])', 'composes as a proper list');
4497
+ },
4498
+ },
4499
+ {
4500
+ // An error/2 ball raised by Prolog throw/1 is the same thing a built-in
4501
+ // raises, so the top level must display it the same way. Only balls
4502
+ // without an error/2 envelope keep the throw/1 wrapper.
4503
+ name: 'uncaught error/2 balls display without a throw/1 wrapper',
4504
+ run: () => {
4505
+ const repl = runCli([], {
4506
+ input: 'must_be(integer, a).\nthrow(foo).\nhalt.\n',
4507
+ });
4508
+ assertIncludes(repl.stdout, 'error(type_error(integer, a), [must_be/2])', 'thrown ISO error');
4509
+ assertNotIncludes(repl.stdout, 'throw(error(type_error', 'no throw/1 wrapper on error/2');
4510
+ assertIncludes(repl.stdout, 'throw(foo)', 'non-error ball keeps the wrapper');
4511
+ },
4512
+ },
4479
4513
  {
4480
4514
  // Nesting prepends outermost-first and keeps the raising predicate last.
4481
4515
  name: 'nested call_with_error_context/2 accumulates outermost first',