eyeprolog 1.5.57 → 1.5.59

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.57",
6
+ "version": "1.5.59",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/errors.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Keep these independent of the ISO builtin registry so syntax, DCG, program,
3
3
  // and solver layers can report Prolog errors without importing the whole ISO
4
4
  // implementation (and without creating semantic-layer import cycles).
5
- import { termToString } from './term.js';
5
+ import { atom, compound, emptyList, numberTerm, termToString } from './term.js';
6
6
 
7
7
  export class PrologError extends Error {
8
8
  constructor(formal, culprit = null) {
@@ -21,3 +21,34 @@ export class HaltSignal extends Error {
21
21
  this.code = code;
22
22
  }
23
23
  }
24
+
25
+ // ISO 7.12.2 leaves the second argument of error/2 implementation defined.
26
+ // Reporting which built-in raised the error is far more useful than a constant,
27
+ // so built-in call sites attach the predicate indicator when the raising code
28
+ // did not supply a context of its own. The indicator term is built once per
29
+ // registry entry and reused, so the error path allocates nothing extra.
30
+ function builtinErrorContext(def, goal) {
31
+ let context = def._errorContextTerm;
32
+ if (context === undefined) {
33
+ // A one-element list, not a bare indicator: error contexts are lists so
34
+ // that library(error)'s call_with_error_context/2 can prepend its own
35
+ // elements and still yield a proper list (issue #98).
36
+ context = compound('.', [
37
+ compound('/', [atom(goal.name), numberTerm(goal.arity)]),
38
+ emptyList(),
39
+ ]);
40
+ def._errorContextTerm = context;
41
+ }
42
+ return context;
43
+ }
44
+
45
+ // Shared, pre-built error instances are thrown many times from different
46
+ // built-ins, so tagging one with a context would leak the first thrower's
47
+ // indicator into every later report. They keep the default context, which also
48
+ // preserves the ground-error-term cache in formalErrorTerm.
49
+ export function attachBuiltinErrorContext(error, def, goal) {
50
+ if (!(error instanceof PrologError)) return error;
51
+ if (error.contextTerm != null || error._sharedInstance === true) return error;
52
+ error.contextTerm = builtinErrorContext(def, goal);
53
+ return error;
54
+ }
package/src/iso.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  let isoFresh = 0;
23
23
 
24
24
  export { PrologError, HaltSignal } from './errors.js';
25
- import { PrologError, HaltSignal } from './errors.js';
25
+ import { HaltSignal, PrologError, attachBuiltinErrorContext } from './errors.js';
26
26
 
27
27
  class ThrownTerm extends Error {
28
28
  constructor(term) {
@@ -2452,6 +2452,7 @@ function numberListText(list, env, kind, valueIsBound, solver = null) {
2452
2452
  }
2453
2453
 
2454
2454
  const numberSyntaxError = new PrologError('syntax_error(number)');
2455
+ numberSyntaxError._sharedInstance = true;
2455
2456
 
2456
2457
  function numberListBuiltin(kind) {
2457
2458
  return function* ({ solver, goal, env }) {
@@ -3090,7 +3091,11 @@ function* phraseSolutions({ solver, goal, env }, state) {
3090
3091
  solver.trimInnerTableScope('phrase');
3091
3092
  }
3092
3093
  }
3093
- const defaultErrorContext = atom('eyeprolog');
3094
+ // ISO 7.12.2 leaves this implementation defined. EyeProlog uses a list of
3095
+ // context elements so that contexts compose: the raising built-in contributes
3096
+ // its predicate indicator and each enclosing call_with_error_context/2
3097
+ // prepends its own element, always yielding a proper list (issue #98).
3098
+ const defaultErrorContext = emptyList();
3094
3099
 
3095
3100
  function parseFormalErrorTerm(text) {
3096
3101
  const open = text.indexOf('(');
@@ -3161,8 +3166,17 @@ function* catchSolutions({ solver, goal, env }, state) {
3161
3166
  // isolated in a child solver. Running it directly avoids constructing a
3162
3167
  // complete Solver for hot caught failures such as number_chars/2 syntax
3163
3168
  // probes, while the cloned environment keeps catch/3's rollback boundary.
3164
- const iterator = direct.handler({ solver, goal: invoked, env: env.clone() });
3165
- const result = iterator.next();
3169
+ let iterator;
3170
+ let result;
3171
+ try {
3172
+ iterator = direct.handler({ solver, goal: invoked, env: env.clone() });
3173
+ result = iterator.next();
3174
+ } catch (caught) {
3175
+ // This fast path bypasses the solver's builtin frame, so it has to
3176
+ // attach the raising predicate's indicator itself (see
3177
+ // attachBuiltinErrorContext in solver.js).
3178
+ throw attachBuiltinErrorContext(caught, direct, invoked);
3179
+ }
3166
3180
  if (result.done) solver.stats.deterministic_builtin_failures++;
3167
3181
  else {
3168
3182
  solver.stats.deterministic_builtin_successes++;
package/src/lib/error.pl CHANGED
@@ -121,5 +121,12 @@ representation_error(Flag) :- throw(error(representation_error(Flag), [])).
121
121
  resource_error(Resource) :- throw(error(resource_error(Resource), [])).
122
122
  resource_error(Resource, Context) :- throw(error(resource_error(Resource), Context)).
123
123
 
124
+ % 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.
124
128
  call_with_error_context(Goal, Pair) :-
125
- catch(Goal, error(Error, Context), throw(error(Error, [Pair|Context]))).
129
+ catch(Goal,
130
+ error(Error, Context),
131
+ ( copy_term(Pair, Element),
132
+ throw(error(Error, [Element|Context])) )).
package/src/repl.js CHANGED
@@ -867,6 +867,10 @@ function formatError(engine, state, error) {
867
867
  collectUnboundVariables(engine, term, env, variableNames, () => `_${letterName(generated++)}`);
868
868
  return `${engine.formatTermForWrite(term, env, {
869
869
  quoted: true,
870
+ // Match the spacing successful answers use: operator layout only where
871
+ // ISO 6.3.4/6.4 needs it to keep the text readable back as the same
872
+ // term, so contexts print as [outer-1, atom_length/2].
873
+ minimalOperatorSpacing: true,
870
874
  operators: [...state.program.operators.values()],
871
875
  variableNames,
872
876
  doubleBar: !state.strictIso,
package/src/solver.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
7
7
  } from './term.js';
8
8
  import { numberValueKey, sameNumberValue } from './number-value.js';
9
+ import { attachBuiltinErrorContext } from './errors.js';
9
10
  import { PrologError, getStrictIsoRegistry } from './iso.js';
10
11
  import { getEyePrologRegistry } from './standard-library.js';
11
12
  import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
@@ -757,8 +758,14 @@ export class Solver {
757
758
  if (builtinReady) {
758
759
  const deterministic = def.deterministic ||
759
760
  def.deterministicWhen?.({ solver: this, goal, env }) === true;
760
- const iterator = def.handler({ solver: this, goal, env });
761
- const firstResult = iterator.next();
761
+ let iterator;
762
+ let firstResult;
763
+ try {
764
+ iterator = def.handler({ solver: this, goal, env });
765
+ firstResult = iterator.next();
766
+ } catch (caught) {
767
+ throw attachBuiltinErrorContext(caught, def, goal);
768
+ }
762
769
  if (deterministic) {
763
770
  if (!firstResult.done) this.stats.deterministic_builtin_successes++;
764
771
  else this.stats.deterministic_builtin_failures++;
@@ -67,7 +67,7 @@ Status values are:
67
67
  | 7.11.2.2 | Effect when `debug=on` | The flag is accepted and stored; it does not change goal semantics or enable a debugger. | **defined** — `src/solver.js`; no semantic branch depends on `debug`. |
68
68
  | 7.11.2.3 | Default `max_arity` | `unbounded`: EyeProlog imposes no fixed semantic ceiling on compound-term arity. Practical host allocation exhaustion is a resource condition. This flag is distinct from any potential implementation-specific procedure-arity limit; EyeProlog currently declares no separate finite procedure limit. | **defined** — `src/iso-limits.js`, `src/solver.js`, `src/parser.js`, `src/iso.js`; strict regression coverage. |
69
69
  | 7.11.2.5 | Default `double_quotes` | `chars`. | **defined** — `src/solver.js`, parser flag state. |
70
- | 7.12.1 | Second argument of `error/2` | The default context term is the atom `eyeprolog`. A few implementation-specific diagnostics may deliberately supply a more specific context term. | **defined** — `formalErrorTerm()` in `src/iso.js`. |
70
+ | 7.12.1 | Second argument of `error/2` | A list of context elements, innermost last. A built-in contributes its own predicate indicator, so `atom_length(1.0, _)` raises `error(type_error(atom, 1.0), [atom_length/2])`. Errors raised outside a built-in frame, and errors thrown from shared pre-built instances reused by several built-ins, use `[]`. Each enclosing `call_with_error_context/2` prepends a copy of its own element, so contexts compose into proper lists. | **defined** — `attachBuiltinErrorContext()` in `src/errors.js`, `formalErrorTerm()` in `src/iso.js`, `call_with_error_context/2` in `src/lib/error.pl`. |
71
71
  | 7.12.2(f) | Implementation-defined representation limits | Character and character-code operations are limited to Unicode scalar values; surrogates and values above U+10FFFF are representation errors. Arity/integer values are modeled as unbounded but may hit host/resource limits. Float input overflow uses the implementation-specific `max_float`/`min_float` representation names documented by the STC-oriented tests. | **defined** — parser/ISO numeric and character guards. |
72
72
  | 8.17.1 | Implementation-defined flag value ranges | Strict mode exposes only Part 1 core flags and their standard value sets. Normal mode additionally exposes EyeProlog's `occurs_check` and `default_procedure_access` flags. With `bounded=false`, `max_integer` and `min_integer` have no current or selectable value and their `current_prolog_flag/2` queries fail. Valid alternative values of fixed standard flags are distinguished from invalid values so `set_prolog_flag/2` reports permission versus domain errors as prescribed. | **defined** — strict registry/flag filtering in `src/solver.js`; strict flag tests. |
73
73
  | 8.17.3 | Other effects of `halt/0` | Terminates EyeProlog execution and returns host/process status `0`; it produces no Prolog solution. | **defined** — `HaltSignal`, `haltBuiltin()`, CLI/runner handling. |
@@ -1,3 +1,3 @@
1
- answer(7, red, caught(eyeprolog), first, caught(eyeprolog)).
2
- answer(7, red, caught(eyeprolog), second, caught(eyeprolog)).
1
+ answer(7, red, caught([atom_length / 2]), first, caught([])).
2
+ answer(7, red, caught([atom_length / 2]), second, caught([])).
3
3
  flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true), pair(default_procedure_access, private)]).
@@ -1475,7 +1475,7 @@ c4 ?- call((!;1)).
1475
1475
  });
1476
1476
  assertEqual(result.status, 0, 'exit status');
1477
1477
  assertEqual(result.stdout,
1478
- '?- error(type_error(callable, (write(3), 3)), eyeprolog).\n' +
1478
+ '?- error(type_error(callable, (write(3),3)), []).\n' +
1479
1479
  '?- ',
1480
1480
  'fully instantiated call/1 culprit');
1481
1481
  assertEqual(result.stderr, '', 'stderr');
@@ -1891,7 +1891,7 @@ c4 ?- call((!;1)).
1891
1891
  assertIncludes(repl.stdout, 'T = ./*. .', 'REPL dotted graphic atom answer');
1892
1892
  assertNotIncludes(repl.stdout, "T = './*.'", 'REPL dotted graphic atom has no spurious quotes');
1893
1893
  assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
1894
- assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
1894
+ assertIncludes(repl.stdout, 'error(syntax_error(read_term), [read/1])', 'REPL syntax error');
1895
1895
  assertEqual(repl.stderr, '', 'REPL stderr');
1896
1896
 
1897
1897
  const continuedGraphic = runCli([], {
@@ -2719,8 +2719,8 @@ c4 ?- call((!;1)).
2719
2719
  });
2720
2720
  assertEqual(result.status, 0, 'exit status');
2721
2721
  assertEqual(result.stdout,
2722
- '?- error(type_error(list, [1, [], _A | 2]), eyeprolog).\n' +
2723
- '?- error(type_error(list, [1, [], _A | 2]), eyeprolog).\n' +
2722
+ '?- error(type_error(list, [1, [], _A | 2]), [number_chars/2]).\n' +
2723
+ '?- error(type_error(list, [1, [], _A | 2]), [number_chars/2]).\n' +
2724
2724
  '?- ',
2725
2725
  'stdout');
2726
2726
  assertEqual(result.stderr, '', 'stderr');
@@ -2734,8 +2734,8 @@ c4 ?- call((!;1)).
2734
2734
  });
2735
2735
  assertEqual(result.status, 0, 'exit status');
2736
2736
  assertEqual(result.stdout,
2737
- '?- error(instantiation_error, eyeprolog).\n' +
2738
- '?- Error = instantiation_error, Imp_def = eyeprolog.\n' +
2737
+ '?- error(instantiation_error, [(is)/2]).\n' +
2738
+ '?- Error = instantiation_error, Imp_def = [(is)/2].\n' +
2739
2739
  '?- ',
2740
2740
  'stdout');
2741
2741
  assertEqual(result.stderr, '', 'stderr');
@@ -3710,7 +3710,7 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
3710
3710
  '?- V = fail.\n' +
3711
3711
  '?- true.\n' +
3712
3712
  '?- true.\n' +
3713
- '?- error(existence_error(procedure, missing_after_consult / 0), eyeprolog).\n' +
3713
+ '?- error(existence_error(procedure, missing_after_consult/0), []).\n' +
3714
3714
  '?- ',
3715
3715
  'stdout');
3716
3716
  assertEqual(result.stderr, '', 'stderr');
@@ -4044,7 +4044,7 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
4044
4044
  const result = runCli([], { input: 'statistics(nonsense, Value).\nhalt.\n' });
4045
4045
  assertEqual(result.status, 0, 'exit status');
4046
4046
  assertIncludes(result.stdout,
4047
- 'error(domain_error(statistics_key, nonsense), eyeprolog).',
4047
+ 'error(domain_error(statistics_key, nonsense), [statistics/2]).',
4048
4048
  'statistics key error');
4049
4049
  assertEqual(result.stderr, '', 'stderr');
4050
4050
  },
@@ -4463,6 +4463,42 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
4463
4463
  assertNotIncludes(result.stderr, 'unknown directive', 'not a directive message');
4464
4464
  },
4465
4465
  },
4466
+ {
4467
+ // Issue #98: error contexts are lists of context elements, so a built-in
4468
+ // error composes with library(error)'s call_with_error_context/2 into a
4469
+ // proper list instead of the improper [Element|eyeprolog] it used to be.
4470
+ name: 'error contexts compose into proper lists (issue #98)',
4471
+ run: () => {
4472
+ const input = ':- use_module(library(error)).\n%% goal: answer(X)\n' +
4473
+ 'answer(C) :- catch(call_with_error_context(atom_length(1.0,_), outer-1), error(_,C), true).\n';
4474
+ const result = runCli(['-'], { input });
4475
+ assertEqual(result.status, 0, `exit status; stderr=${result.stderr}`);
4476
+ assertIncludes(result.stdout, 'answer([outer - 1, atom_length / 2])', 'composed context');
4477
+ },
4478
+ },
4479
+ {
4480
+ // Nesting prepends outermost-first and keeps the raising predicate last.
4481
+ name: 'nested call_with_error_context/2 accumulates outermost first',
4482
+ run: () => {
4483
+ const input = ':- use_module(library(error)).\n%% goal: answer(X)\n' +
4484
+ 'answer(C) :- catch(call_with_error_context(call_with_error_context(atom_length(1.0,_), inner-1), outer-2), error(_,C), true).\n';
4485
+ const result = runCli(['-'], { input });
4486
+ assertEqual(result.status, 0, `exit status; stderr=${result.stderr}`);
4487
+ assertIncludes(result.stdout, 'answer([outer - 2, inner - 1, atom_length / 2])', 'nesting order');
4488
+ },
4489
+ },
4490
+ {
4491
+ // UWN asked for copy_term/2 semantics on the added element so that
4492
+ // variables in it are not shared with bindings undone during unwinding.
4493
+ name: 'call_with_error_context/2 copies the context element',
4494
+ run: () => {
4495
+ const input = ':- use_module(library(error)).\n%% goal: answer(X)\n' +
4496
+ 'answer(ok) :- catch(call_with_error_context(atom_length(1.0,_), ctx(V)), error(_,[ctx(W)|_]), (V == W -> fail ; true)).\n';
4497
+ const result = runCli(['-'], { input });
4498
+ assertEqual(result.status, 0, `exit status; stderr=${result.stderr}`);
4499
+ assertIncludes(result.stdout, 'answer(ok)', 'context element is a fresh copy');
4500
+ },
4501
+ },
4466
4502
  {
4467
4503
  name: 'double dash permits option-shaped file names',
4468
4504
  run: () => {
@@ -5082,7 +5118,7 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
5082
5118
  goal: 'answer(T)',
5083
5119
  ioOptions: { input: invalidOctal },
5084
5120
  }).stdout,
5085
- 'answer(error(syntax_error(read_term), eyeprolog)).\n',
5121
+ 'answer(error(syntax_error(read_term), [read / 1])).\n',
5086
5122
  'read/1 rejects non-octal numeric escape',
5087
5123
  );
5088
5124
  },
@@ -649,7 +649,7 @@ export function runIsoStrict(reporter = new TestReporter()) {
649
649
  'close(tmp_in)',
650
650
  'current_input(C)',
651
651
  'stream_property(C,alias(user_input))',
652
- 'catch(set_input(tmp_in),error(existence_error(stream,tmp_in),eyeprolog),true)',
652
+ 'catch(set_input(tmp_in),error(existence_error(stream,tmp_in),[set_input/1]),true)',
653
653
  ].join(','),
654
654
  }).stats.completed_goal_lists, 1, 'stream-term, alias lifetime, target/current input, and close fallback');
655
655
 
@@ -856,14 +856,14 @@ export function runIsoStrict(reporter = new TestReporter()) {
856
856
 
857
857
  reporter.test('closes the ISO 7.12 processor error envelope and classification rows', () => {
858
858
  const caught = [
859
- "catch(atom_length(X,N),error(instantiation_error,eyeprolog),true)",
860
- "catch(atom_length(1,N),error(type_error(atom,1),eyeprolog),true)",
861
- "catch(op(1300,xfx,foo),error(domain_error(operator_priority,1300),eyeprolog),true)",
862
- "catch(call(no_such_predicate),error(existence_error(procedure,no_such_predicate/0),eyeprolog),true)",
863
- "catch(abolish(atom/1),error(permission_error(modify,static_procedure,atom/1),eyeprolog),true)",
864
- "catch(char_code(C,1114112),error(representation_error(character_code),eyeprolog),true)",
865
- "catch(X is 1/0,error(evaluation_error(zero_divisor),eyeprolog),true)",
866
- "catch(X is 1<<4294967296,error(resource_error(memory),eyeprolog),true)",
859
+ "catch(atom_length(X,N),error(instantiation_error,[atom_length/2]),true)",
860
+ "catch(atom_length(1,N),error(type_error(atom,1),[atom_length/2]),true)",
861
+ "catch(op(1300,xfx,foo),error(domain_error(operator_priority,1300),[op/3]),true)",
862
+ "catch(call(no_such_predicate),error(existence_error(procedure,no_such_predicate/0),[]),true)",
863
+ "catch(abolish(atom/1),error(permission_error(modify,static_procedure,atom/1),[abolish/1]),true)",
864
+ "catch(char_code(C,1114112),error(representation_error(character_code),[char_code/2]),true)",
865
+ "catch(X is 1/0,error(evaluation_error(zero_divisor),[(is)/2]),true)",
866
+ "catch(X is 1<<4294967296,error(resource_error(memory),[(is)/2]),true)",
867
867
  ];
868
868
  for (const goal of caught) {
869
869
  equal(run('', { isoStrict: true, goal }).stats.completed_goal_lists, 1, goal);
@@ -871,14 +871,14 @@ export function runIsoStrict(reporter = new TestReporter()) {
871
871
 
872
872
  equal(run('', {
873
873
  isoStrict: true,
874
- goal: "catch(read_term(T,[]),error(syntax_error(read_term),eyeprolog),true)",
874
+ goal: "catch(read_term(T,[]),error(syntax_error(read_term),[read_term/2]),true)",
875
875
  ioOptions: { input: "'unterminated." },
876
876
  }).stats.completed_goal_lists, 1, 'syntax error uses error/2 envelope and implementation-defined context');
877
877
 
878
878
  const systemSolver = new Solver(Program.parse('', { isoStrict: true }), { isoStrict: true });
879
879
  systemSolver.io.flush = () => { throw new Error('simulated host I/O failure'); };
880
880
  equal([...systemSolver.solve([
881
- parseGoalText('catch(flush_output(user_output),error(system_error,eyeprolog),true)', { isoStrict: true }),
881
+ parseGoalText('catch(flush_output(user_output),error(system_error,[flush_output/1]),true)', { isoStrict: true }),
882
882
  ], new Env(), 0)].length, 1, 'system error uses error/2 envelope and implementation-defined context');
883
883
 
884
884
  // ISO 7.12 deliberately leaves the choice implementation-dependent when