eyeprolog 1.5.69 → 1.5.71

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.69",
6
+ "version": "1.5.71",
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
@@ -1345,9 +1345,9 @@ function* currentInputBuiltin({ solver, goal, env }) {
1345
1345
  const value = deref(goal.args[0], env);
1346
1346
  if (value.type !== VAR) {
1347
1347
  const id = streamTermReference(goal.args[0], env);
1348
- const stream = solver.io.resolve(id);
1349
- if (!stream) throw new PrologError('domain_error(stream)', value);
1350
- if (stream.id === solver.io.currentInput) yield env;
1348
+ // A closed handle is still a stream-term, but cannot be current (#107).
1349
+ // Validate its shape, then test identity rather than requiring an open stream.
1350
+ if (id === solver.io.currentInput) yield env;
1351
1351
  return;
1352
1352
  }
1353
1353
  const next = env.clone();
@@ -1357,9 +1357,7 @@ function* currentOutputBuiltin({ solver, goal, env }) {
1357
1357
  const value = deref(goal.args[0], env);
1358
1358
  if (value.type !== VAR) {
1359
1359
  const id = streamTermReference(goal.args[0], env);
1360
- const stream = solver.io.resolve(id);
1361
- if (!stream) throw new PrologError('domain_error(stream)', value);
1362
- if (stream.id === solver.io.currentOutput) yield env;
1360
+ if (id === solver.io.currentOutput) yield env;
1363
1361
  return;
1364
1362
  }
1365
1363
  const next = env.clone();
package/src/lib/error.pl CHANGED
@@ -55,6 +55,7 @@ error__must_be(acyclic, Term) :- !,
55
55
  error__must_be(list, Term) :- !,
56
56
  error__list(Term, Term, must_be/2, complete).
57
57
  error__must_be(list(Type), Term) :- !,
58
+ error__require_type(Type, must_be/2),
58
59
  error__list(Term, Term, must_be/2, complete),
59
60
  error__proper_list_of(Term, Type).
60
61
  error__must_be(character, Term) :- !,
@@ -75,10 +76,30 @@ error__must_be(pair, Term) :- !,
75
76
  error__must_be(not_less_than_zero, Term) :- !,
76
77
  must_be(integer, Term),
77
78
  ( Term >= 0 -> true ; domain_error(not_less_than_zero, Term) ).
78
- error__must_be(Type, Term) :-
79
- ( var(Term) -> error__must_be_throw(instantiation_error)
80
- ; error__must_be_throw(type_error(Type, Term))
81
- ).
79
+ error__must_be(Type, _) :-
80
+ error__must_be_throw(type_error(type, Type)).
81
+
82
+ % Check the type descriptor before accepting a variable or an empty list.
83
+ % The variable guard also prevents validation from instantiating a descriptor.
84
+ error__require_type(Type, Predicate) :- var(Type), !,
85
+ throw(error(instantiation_error, [predicate-Predicate])).
86
+ error__require_type(list(Type), Predicate) :- !,
87
+ error__require_type(Type, Predicate).
88
+ error__require_type(Type, Predicate) :-
89
+ ( error__known_type(Type) -> true
90
+ ; throw(error(type_error(type, Type), [predicate-Predicate])) ).
91
+
92
+ error__known_type(integer).
93
+ error__known_type(atom).
94
+ error__known_type(number).
95
+ error__known_type(var).
96
+ error__known_type(ground).
97
+ error__known_type(acyclic).
98
+ error__known_type(list).
99
+ error__known_type(character).
100
+ error__known_type(chars).
101
+ error__known_type(pair).
102
+ error__known_type(not_less_than_zero).
82
103
 
83
104
  % A list ends in []; a partial list ends in a variable (including a variable
84
105
  % alone). Inspect before matching so validation never completes a partial list.
@@ -102,8 +123,8 @@ error__proper_list_of(Term, _) :-
102
123
  ).
103
124
 
104
125
  can_be(Type, Term) :-
105
- ( var(Type) -> error__can_be_throw(instantiation_error)
106
- ; var(Term) -> true
126
+ error__require_type(Type, can_be/2),
127
+ ( var(Term) -> true
107
128
  ; error__can_be(Type, Term)
108
129
  ).
109
130
 
package/src/solver.js CHANGED
@@ -838,6 +838,16 @@ export class Solver {
838
838
  continue;
839
839
  }
840
840
 
841
+ const appendIterator = bundledAppendIterator(this, group, goal, env);
842
+ if (appendIterator != null) {
843
+ const firstResult = appendIterator.next();
844
+ if (firstResult.done) break;
845
+ goals = rest;
846
+ env = firstResult.value;
847
+ depth++;
848
+ continue;
849
+ }
850
+
841
851
  const comparisonIterator = bundledCompareSiIterator(this, group, goal, env);
842
852
  if (comparisonIterator != null) {
843
853
  const firstResult = comparisonIterator.next();
@@ -1896,6 +1906,44 @@ function* bundledBetweenSolutions(solver, goal, env, state) {
1896
1906
  state.pending = false;
1897
1907
  }
1898
1908
 
1909
+ function bundledAppendIterator(solver, group, goal, env) {
1910
+ if (solver.registry.eyePrologLibrary !== true || group.bundledLibrary !== true ||
1911
+ group.module !== 'lists' || group.name !== 'append' || group.arity !== 3 ||
1912
+ group.tabled || group.clauses.length !== 2) return null;
1913
+ // Preserve source-defined hook ordering and occurs-check diagnostics. Only
1914
+ // forward construction with a plain, unbound result is specialized.
1915
+ if (env._prologAttributes != null || env._delays != null ||
1916
+ solver.prologFlags.get('occurs_check')?.value?.name === 'error' ||
1917
+ deref(goal.args[2], env).type !== VAR) return null;
1918
+
1919
+ const items = [];
1920
+ const seen = new Set();
1921
+ let cursor = deref(goal.args[0], env);
1922
+ while (isCons(cursor)) {
1923
+ if (seen.has(cursor)) return null;
1924
+ seen.add(cursor);
1925
+ items.push(cursor.args[0]);
1926
+ if ((items.length & 255) === 0) solver.checkMemoryLimit(true);
1927
+ cursor = deref(cursor.args[1], env);
1928
+ }
1929
+ if (!isEmptyList(cursor)) return null;
1930
+ return bundledAppendSolutions(solver, goal, env, items);
1931
+ }
1932
+
1933
+ function* bundledAppendSolutions(solver, goal, env, items) {
1934
+ // Copy just the prefix spine, preserving its variables and sharing the
1935
+ // suffix. One normal, occurs-checked unification replaces repeated clause
1936
+ // freshening and scans over every remaining variable-list suffix.
1937
+ let joined = goal.args[1];
1938
+ for (let i = items.length - 1; i >= 0; i--) {
1939
+ joined = cons(items[i], joined);
1940
+ if ((i & 255) === 0) solver.checkMemoryLimit(true);
1941
+ }
1942
+ const next = env.clone();
1943
+ solver.stats.unify_calls++;
1944
+ if (unify(goal.args[2], joined, next)) yield next;
1945
+ }
1946
+
1899
1947
  function bundledMemberIterator(solver, group, goal, env) {
1900
1948
  if (solver.registry.eyePrologLibrary !== true ||
1901
1949
  group.module !== 'lists' || group.name !== 'member' || group.arity !== 2 ||
@@ -28,6 +28,102 @@ import {
28
28
 
29
29
  export function regressionCases() {
30
30
  return [
31
+ {
32
+ name: 'REPL current_input/1 and current_output/1 fail on closed stream handles (issue #107)',
33
+ run: () => {
34
+ const file = sourceAtom(path.join(temp.dir, 'closed-stream-107.txt'));
35
+ const result = runCli([], { input:
36
+ `open(${file},write,S),close(S),current_input(S).\n` +
37
+ `open(${file},write,S),close(S),current_output(S).\nhalt.\n`,
38
+ });
39
+ assertEqual(result.status, 0, result.stderr);
40
+ assertEqual((result.stdout.match(/false\./g) ?? []).length, 2, 'both closed-handle queries fail');
41
+ assertNotIncludes(result.stdout + result.stderr, 'error(', 'no stream domain error');
42
+ },
43
+ },
44
+ {
45
+ name: 'must_be/2 and can_be/2 reject invalid type descriptors before checking values (issue #106)',
46
+ run: () => {
47
+ for (const predicate of ['must_be', 'can_be']) {
48
+ for (const type of ['nontype', '42', 'foo(integer)', 'list(nontype)', 'list(list(nontype))']) {
49
+ const culprit = type.startsWith('list(') ? 'nontype' : type;
50
+ for (const value of ['0', 'X', '[]']) {
51
+ const result = runEyeProlog(`answer(ok) :- catch((${predicate}(${type},${value}),fail),error(type_error(type,${culprit}),[predicate-${predicate}/2]),true),var(X).`, { goals: ['answer(X)'] });
52
+ assertIncludes(result.stdout, 'answer(ok)', `${predicate}(${type},${value})`);
53
+ }
54
+ }
55
+ for (const type of ['T', 'list(T)', 'list(list(T))']) {
56
+ const result = runEyeProlog(`answer(ok) :- catch((${predicate}(${type},[]),fail),error(instantiation_error,[predicate-${predicate}/2]),true),var(T).`, { goals: ['answer(X)'] });
57
+ assertIncludes(result.stdout, 'answer(ok)', `${predicate}(${type},[])`);
58
+ }
59
+ }
60
+ const valid = [
61
+ ['integer', '1'], ['atom', 'a'], ['number', '1.5'], ['var', 'X'],
62
+ ['ground', 'f(a)'], ['acyclic', 'f(X)'], ['list', '[X]'],
63
+ ['character', 'a'], ['chars', '[a,b]'], ['pair', 'a-b'],
64
+ ['not_less_than_zero', '0'], ['list(integer)', '[1,2]'],
65
+ ['list(list(integer))', '[[1],[]]'],
66
+ ];
67
+ for (const [type, value] of valid) {
68
+ const result = runEyeProlog(`answer(ok) :- must_be(${type},${value}),can_be(${type},${value}),can_be(${type},Fresh),var(Fresh).`, { goals: ['answer(X)'] });
69
+ assertIncludes(result.stdout, 'answer(ok)', `supported type ${type}`);
70
+ }
71
+ const context = runEyeProlog('answer(ok) :- catch((call_with_error_context(can_be(nontype,X),outer-1),fail),error(type_error(type,nontype),[outer-1,predicate-can_be/2]),true),var(X).', { goals: ['answer(X)'] });
72
+ assertIncludes(context.stdout, 'answer(ok)', 'composable error context');
73
+ },
74
+ },
75
+ {
76
+ name: 'forward bundled append/3 matches the portable relation across sharing and fallback modes',
77
+ run: () => {
78
+ const goals = [
79
+ 'append([],Y,Z),Y=tail',
80
+ 'append([X,Y],[Y,X],Z),X=a,Y=b',
81
+ 'append([X],T,Z),T=[X|Rest],Rest=tail',
82
+ 'append([a,b],tail,Z)',
83
+ 'append([X],[],X)',
84
+ 'append([a],Z,Z)',
85
+ 'append([X],[],Z),X=Z',
86
+ 'append([a],T,Z),T=Z',
87
+ 'append([a],[],[b])',
88
+ 'append([a|T],[b],[a,c,b])',
89
+ 'append([a|bad],[],Z)',
90
+ 'append(X,Y,[a,b])',
91
+ '(append([a],[],Z);append([b],[],Z))',
92
+ 'freeze(X,Y=woke),append([X],[],Z),X=a',
93
+ 'dif(X,a),append([X],[],Z),X=b',
94
+ 'set_prolog_flag(occurs_check,error),catch(append([X],[],X),error(E,_),true)',
95
+ ];
96
+ for (const goal of goals) {
97
+ const source = `answer(X,Y,Z,T,Rest,E) :- ${goal}.`;
98
+ const options = { goals: ['answer(X,Y,Z,T,Rest,E)'] };
99
+ const optimized = runEyeProlog(source, options);
100
+ const portable = runEyeProlog(
101
+ 'portable_append([],Ys,Ys). portable_append([X|Xs],Ys,[X|Zs]) :- portable_append(Xs,Ys,Zs).\n' +
102
+ source.replaceAll('append(', 'portable_append('), options);
103
+ assertEqual(optimized.stdout, portable.stdout, goal);
104
+ assertEqual(optimized.stderr, portable.stderr, `${goal} diagnostics`);
105
+ }
106
+ const custom = runEyeProlog('append(custom,_,ok).', { goals: ['append(custom,[],Z)'] });
107
+ assertIncludes(custom.stdout, 'append(custom, [], ok)', 'user definition');
108
+ },
109
+ },
110
+ {
111
+ name: 'forward append/3 constructs long shared-variable lists within a bounded heap and time',
112
+ run: () => {
113
+ const script = `
114
+ import {run} from './src/index.js';
115
+ const result=run('answer(ok) :- length(P,8192),append(P,[1],L1),append(P,[2],L2),compare(R,L1,L2),compare_si(S,L1,L2),R==(<),S==(<).', {goals:['answer(X)']});
116
+ if (!result.stdout.includes('answer(ok)')) throw new Error(result.stdout+'\\n'+result.stderr);
117
+ console.log('ok');
118
+ `;
119
+ const result = spawnSync(process.execPath, ['--max-old-space-size=128', '--stack-size=256', '--input-type=module', '--eval', script], {
120
+ cwd: packageRoot, encoding: 'utf8', timeout: 10000,
121
+ });
122
+ if (result.error) throw result.error;
123
+ assertEqual(result.status, 0, result.stderr);
124
+ assertIncludes(result.stdout, 'ok', 'long forward append');
125
+ },
126
+ },
31
127
  {
32
128
  name: 'autoload follows declared meta-predicates and closure arities (issue #105)',
33
129
  run: () => {
@@ -23,6 +23,35 @@ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
23
23
  export function runIsoStrict(reporter = new TestReporter()) {
24
24
  reporter.section('Strict ISO core');
25
25
 
26
+ reporter.test('current stream queries fail for closed handles while stream operations report existence errors (issue #107)', () => {
27
+ const directory = mkdtempSync(join(tmpdir(), 'eyeprolog-stream-107-'));
28
+ const file = `'${join(directory, 'stream.txt').replaceAll("'", "''")}'`;
29
+ try {
30
+ writeFileSync(join(directory, 'stream.txt'), '');
31
+ for (const isoStrict of [false, true]) {
32
+ for (const predicate of ['current_input', 'current_output']) {
33
+ equal(run('', { isoStrict, goal: `open(${file},write,S),close(S),${predicate}(S)` }).stdout,
34
+ '', `${predicate}/1 fails on the reported closed handle`);
35
+ const mode = predicate === 'current_input' ? 'read' : 'write';
36
+ const setter = mode === 'read' ? 'set_input' : 'set_output';
37
+ const source = `answer(ok) :- ${predicate}(Original),open(${file},${mode},S),` +
38
+ `\\+ ${predicate}(S),${setter}(S),${predicate}(S),close(S),` +
39
+ `\\+ ${predicate}(S),${predicate}(Original),` +
40
+ `catch((${setter}(S),fail),error(existence_error(stream,_),_),true),` +
41
+ 'catch((close(S),fail),error(existence_error(stream,_),_),true).';
42
+ equal(run(source, { isoStrict, goal: 'answer(X)' }).stdout, 'answer(ok).\n',
43
+ `${predicate}/1 recognizes live handles and restores the standard stream after close`);
44
+ for (const term of ['foo', '42', "'$stream'(foo)"]) {
45
+ equal(capture(() => run('', { isoStrict, goal: `${predicate}(${term})` })).formal,
46
+ 'domain_error(stream)', `${predicate}/1 rejects malformed stream term ${term}`);
47
+ }
48
+ }
49
+ }
50
+ } finally {
51
+ rmSync(directory, { recursive: true, force: true });
52
+ }
53
+ });
54
+
26
55
  reporter.test('executes ordinary Part 1 clauses', () => {
27
56
  const result = run('p(X) :- X = 1.\n', { isoStrict: true, goal: 'p(1)' });
28
57
  equal(result.stdout, 'p(1).\n', 'stdout');