eyeprolog 1.5.48 → 1.5.50

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.
@@ -32,7 +32,7 @@ spelling therefore changes this report even when no corpus file is added or remo
32
32
  | context | 11 | 0 | 0 | 0 | 11 |
33
33
  | control | 15 | 0 | 0 | 0 | 15 |
34
34
  | explicit-tabling | 6 | 0 | 0 | 0 | 6 |
35
- | iso | 171 | 217 | 0 | 0 | 388 |
35
+ | iso | 173 | 218 | 0 | 0 | 391 |
36
36
  | lists | 52 | 3 | 0 | 0 | 55 |
37
37
  | modules | 2 | 0 | 0 | 0 | 2 |
38
38
  | negation | 8 | 0 | 19 | 0 | 27 |
@@ -45,7 +45,7 @@ spelling therefore changes this report even when no corpus file is added or remo
45
45
  | terms | 26 | 3 | 0 | 0 | 29 |
46
46
  | unification | 18 | 0 | 0 | 0 | 18 |
47
47
  | variables | 16 | 7 | 0 | 0 | 23 |
48
- | **Total** | **493** | **272** | **19** | **21** | **805** |
48
+ | **Total** | **495** | **273** | **19** | **21** | **808** |
49
49
 
50
50
  ## DCG conformance clarification
51
51
 
@@ -53,6 +53,11 @@ EyeProlog checks the input and remainder of `phrase/2-3` and reports
53
53
  `type_error(list, S)` when an argument is neither a list nor a partial list.
54
54
  These implementation-defined checks follow ISO/IEC TS 13211-3:2025,
55
55
  8.18.1.3 g and h; this behavior is not a known deviation.
56
+ The checks are optional. EyeProlog elects to perform both consistently.
57
+ Dedicated regressions require the exact error for atomic non-lists and
58
+ improper lists across both arities and both sequence positions, while
59
+ accepting variables, proper lists, and partial lists. The upstream quads
60
+ allow checking and non-checking outcomes and do not prove this policy.
56
61
 
57
62
  The Part 3 implementation target is
58
63
  [ISO/IEC TS 13211-3:2025](https://www.iso.org/standard/83635.html).
@@ -5,6 +5,9 @@
5
5
  %% goal: report(X0, X1)
6
6
 
7
7
 
8
+ % clause/2 may only inspect a public procedure, so sale/3 is declared dynamic.
9
+ :- dynamic(sale/3).
10
+
8
11
  sale(north, ada, 7).
9
12
  sale(north, ada, 7).
10
13
  sale(north, ben, 5).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.48",
6
+ "version": "1.5.50",
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
@@ -733,8 +733,14 @@ function* clauseSolutions({ solver, goal, env }, state) {
733
733
  // EyeProlog selects private-procedure access before Body callability when both
734
734
  // conditions hold. ISO 7.12 permits an implementation-dependent choice when
735
735
  // more than one error condition is simultaneously satisfied.
736
- if (isProcessorStaticProcedure(solver, head) ||
737
- (solver.isoStrict && group && !group.dynamic)) {
736
+ //
737
+ // A procedure defined by a Prolog text is static unless a dynamic/1
738
+ // directive says otherwise (ISO 7.5.2 and 8.9), and clause/2 may only
739
+ // inspect a public (dynamic) procedure (ISO 8.8.1.3). Static is therefore
740
+ // the default in every mode, not only under --iso-strict: assert/1,
741
+ // retract/1, and abolish/1 already reject static procedures unconditionally,
742
+ // and clause/2 now agrees with them.
743
+ if (isProcessorStaticProcedure(solver, head) || (group && !group.dynamic)) {
738
744
  throw new PrologError('permission_error(access, private_procedure)', indicator);
739
745
  }
740
746
  callableOrVariable(goal.args[1], env);
@@ -3024,7 +3030,9 @@ function* phraseSolutions({ solver, goal, env }, state) {
3024
3030
  validateDcgEmbeddedGoals(grammarBody, input, requestedOutput);
3025
3031
  // ISO/IEC TS 13211-3:2025, 8.18.1.3 g and h permit these checks
3026
3032
  // and specify type_error(list, S) for invalid terminal-sequence arguments.
3027
- // Keep both diagnostics and the unmodified upstream phrase quad gate.
3033
+ // EyeProlog elects to perform both optional checks consistently, including
3034
+ // improper lists. The upstream quads allow non-checking outcomes too; the
3035
+ // dedicated regression matrix requires our exact diagnostics.
3028
3036
  // See conformance-report.md and issue #94.
3029
3037
  if (!isListOrPartialList(input, env)) {
3030
3038
  throw new PrologError('type_error(list)', deref(input, env));
package/src/program.js CHANGED
@@ -778,12 +778,19 @@ class ProgramBuilder {
778
778
  }
779
779
 
780
780
  if (!isDirectiveClause(clause)) {
781
- if (program.strictIso && Array.isArray(clause.body) && clause.body.length > 0) {
781
+ if (Array.isArray(clause.body) && clause.body.length > 0) {
782
782
  // ISO 7.6.2 applies when source text is prepared as well as when a
783
783
  // clause term is asserted dynamically. Keep the original variable
784
784
  // objects while wrapping variable goals in call/1 so sharing with
785
785
  // the head (and recursively through ;/2 and ->/2) is preserved for
786
786
  // later clause/2 and retract/1 conversion back to terms.
787
+ //
788
+ // 7.5.1 prepares the read-terms of a Prolog text into database
789
+ // clauses by this same conversion, so it is not conditional on the
790
+ // strict profile. Skipping it left a bare variable goal where the
791
+ // standard requires call/1, which both reports the wrong body term
792
+ // through clause/2 (8.8.1.4, legs/2) and makes a cut in that goal
793
+ // transparent instead of local to the call (7.8.3).
787
794
  clause.body = clause.body.map((goal) => convertClauseBodyTerm(goal));
788
795
  }
789
796
  assertHeadIsDefinable(clause.head, program.strictIso);
@@ -141,7 +141,7 @@ The Corrigendum 2 additions have the same shape.
141
141
 
142
142
  | Clause / predicate | Prescribed row | Status | Executable evidence |
143
143
  | --- | --- | --- | --- |
144
- | 8.8.1 `clause/2` | enumerate clauses of a public user-defined procedure | covered | dynamic-clause tests and grouped-solutions/clause corpus |
144
+ | 8.8.1 `clause/2` | enumerate clauses of a public user-defined procedure | covered | dynamic-clause tests and grouped-solutions/clause corpus; `iso/clause_static_and_dynamic_access` and `error/iso/clause_static_user_procedure` pin the static/private default in all modes |
145
145
  | 8.8.1 error (a) | variable head -> instantiation error | covered | strict `clause(X,_)` |
146
146
  | 8.8.1 error (b) | non-callable head -> callable type error | covered | strict `clause(4,_)` |
147
147
  | 8.8.1 error (c) | private procedure -> access/private-procedure permission error | covered | strict static/private tests |
@@ -37,12 +37,23 @@ implementation-dependent negation/if-then choices documented in the reference.
37
37
  `--iso-strict` leaves `-->/2` as ordinary Part 1 operator syntax and excludes
38
38
  Part 3 grammar expansion and `phrase/2-3` from the strict registry.
39
39
 
40
- One compatibility difference is explicit: EyeProlog currently reports
41
- `type_error(list)` when its `phrase/2-3` terminal-sequence validation rejects a
42
- non-list. The Part 3 terminal-sequence specification uses its own terminal
43
- sequence error category. EyeProlog retains the list-shaped error for its normal
44
- interoperability profile, so this behavior must not be presented as evidence of
45
- complete Part 3 conformance.
40
+ EyeProlog elects to perform both implementation-defined terminal-sequence
41
+ checks in ISO/IEC TS 13211-3:2025, 8.18.1.3 g and h. The checks are optional;
42
+ when performed, their specified error type is `list`. EyeProlog consistently
43
+ raises `type_error(list, Culprit)` for a non-list input to `phrase/2` or
44
+ `phrase/3`, and for a non-list remainder to `phrase/3`. The culprit is the
45
+ whole invalid argument, including an improper list, not just its tail.
46
+ Variables, proper lists, and partial lists are accepted by these checks.
47
+ For otherwise valid grammar bodies, validation precedes execution, even when
48
+ the grammar would fail or produce a side effect.
49
+
50
+ The upstream phrase quads permit both checking and non-checking outcomes and
51
+ therefore do not establish this consistency. A dedicated regression matrix in
52
+ `test/run-regression.mjs` requires the exact diagnostic across both arities,
53
+ both sequence positions, atomic and compound non-lists, improper lists at
54
+ several depths, and several grammar bodies. Positive cases protect variables,
55
+ proper lists, and partial lists. These are policy checks, not a relaxation of
56
+ the unmodified upstream corpus.
46
57
 
47
58
  The Part 3 implementation otherwise keeps sequence validation, grammar-body
48
59
  callability, variable-body instantiation errors, `phrase/3` steadfastness, and
@@ -37,7 +37,7 @@ and preparation error cases.
37
37
  | --- | --- | --- |
38
38
  | 7.5.1 initial database and clause order | covered | Prepared clauses retain textual preparation order. The implementation-defined initial database/standard-procedure choices are documented separately. |
39
39
  | 7.5.2 static and dynamic procedures | covered | Source procedures are static unless declared dynamic; asserting a previously absent predicate creates a dynamic procedure; modification of protected/static procedures is rejected. |
40
- | 7.5.3 private and public procedures | covered | User static procedures are private to `clause/2`; dynamic procedures are public; standardized built-ins/control forms remain static/private. |
40
+ | 7.5.3 private and public procedures | covered | User static procedures are private to `clause/2` in every execution mode, not only under `--iso-strict`; dynamic procedures are public; standardized built-ins/control forms remain static/private. Corpus: `iso/clause_static_and_dynamic_access`, `error/iso/clause_static_user_procedure`. |
41
41
  | 7.5.4 database update visibility | covered | Dynamic calls use the logical-update view: an activation continues over the clause set visible when it began, while later activations see successful assertions/retractions. `retractall/1` retains an empty dynamic procedure and `abolish/1` removes it. |
42
42
 
43
43
  The strict test `closes ISO 7.5-7.7 database, conversion, and execution rows`
@@ -51,7 +51,7 @@ The complete 8.8-8.9 database built-in modes/errors remain covered by
51
51
  | Row | Status | EyeProlog disposition / executable evidence |
52
52
  | --- | --- | --- |
53
53
  | 7.6.1 term-to-goal/body boundary | covered | Callable terms enter execution as goals; variables are deferred through `call/1`; non-callable numeric/body terms are rejected rather than silently coerced. |
54
- | 7.6.2 term-to-body conversion | covered | Variables become `call/1`; conjunction, disjunction, and if-then bodies are converted recursively; non-control callable terms retain their predication; variable identity is preserved across the converted clause. |
54
+ | 7.6.2 term-to-body conversion | covered | Variables become `call/1`; conjunction, disjunction, and if-then bodies are converted recursively; non-control callable terms retain their predication; variable identity is preserved across the converted clause. The conversion runs in every execution mode, for clauses prepared from a Prolog text as well as for `assert`ed clauses, so both paths yield the same body term. Corpus: `iso/clause_body_conversion`. |
55
55
  | 7.6.3 clause-to-term observation | covered | `clause/2` returns fresh renamed clause observations with the required sharing between head and body. |
56
56
  | 7.6.4 update conversion | covered | `asserta/1`, `assertz/1`, `retract/1`, and prepared source clauses use the same strict body-conversion rules and reject malformed bodies before database mutation. |
57
57
 
@@ -140,7 +140,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
140
140
  Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
141
141
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
142
142
 
143
- The corpus has 388 cases in `iso/` and 805 file-based conformance cases in total. Of those, 11 cases in `stc/` are explicitly labelled working-draft review evidence rather than normative ISO claims. The separate vendored strict-reader WG17 matrix is a deterministic regression snapshot; the live Neumerkel gate discovers the current upstream inventory at run time; release/report checks separately verify the tracked `NEUMERKEL-LATEST.md`. The generated `conformance-report.md` records local corpus totals and links to that live evidence. Together with regression, documentation-sync, API, example, and book-example checks, `npm test` is the release gate.
143
+ The corpus has 391 cases in `iso/` and 808 file-based conformance cases in total. Of those, 11 cases in `stc/` are explicitly labelled working-draft review evidence rather than normative ISO claims. The separate vendored strict-reader WG17 matrix is a deterministic regression snapshot; the live Neumerkel gate discovers the current upstream inventory at run time; release/report checks separately verify the tracked `NEUMERKEL-LATEST.md`. The generated `conformance-report.md` records local corpus totals and links to that live evidence. Together with regression, documentation-sync, API, example, and book-example checks, `npm test` is the release gate.
144
144
 
145
145
  ## Updating expected output
146
146
 
@@ -0,0 +1,41 @@
1
+ % ISO 7.6.2 a: when a term is converted to the body of a clause, a variable
2
+ % goal becomes call/1. ISO 7.6.2 b applies the conversion recursively through
3
+ % (,)/2, (;)/2 and (->)/2. ISO 7.5.1 prepares the read-terms of a Prolog text
4
+ % into database clauses by that same conversion, so a clause loaded from a
5
+ % Prolog text and the identical clause added by assertz/1 must agree.
6
+
7
+ :- dynamic(from_text/1).
8
+ from_text(Goal) :- Goal.
9
+
10
+ :- dynamic(nested/3).
11
+ nested(A, B, C) :- (A ; B), (C -> true ; true).
12
+
13
+ :- dynamic(cut_scope/1).
14
+ cut_scope(Goal) :- Goal.
15
+ cut_scope(_) :- true.
16
+
17
+ %% goal: text_body(X0)
18
+
19
+ text_body(Body) :-
20
+ clause(from_text(true), Body).
21
+
22
+ %% goal: nested_body(X0)
23
+
24
+ nested_body(Body) :-
25
+ clause(nested(a, b, c), Body).
26
+
27
+ %% goal: assert_agrees_with_text(X0)
28
+
29
+ % The same clause added at run time must convert to the same body term.
30
+ assert_agrees_with_text(Agree) :-
31
+ assertz((asserted(Goal) :- Goal)),
32
+ clause(from_text(true), TextBody),
33
+ clause(asserted(true), AssertedBody),
34
+ ( TextBody == AssertedBody -> Agree = agree ; Agree = differ ).
35
+
36
+ %% goal: cut_is_local(X0)
37
+
38
+ % 7.8.3: a cut in the argument of call/1 is local to that call, so it must not
39
+ % prune the remaining clauses of cut_scope/1.
40
+ cut_is_local(Solutions) :-
41
+ findall(taken, cut_scope(!), Solutions).
@@ -0,0 +1,26 @@
1
+ % Procedures defined by a Prolog text are static unless a dynamic/1 directive
2
+ % makes them public. clause/2 inspects public procedures and raises a
3
+ % permission error for static ones, in every execution mode.
4
+ % https://github.com/eyereasoner/eyeprolog/issues/96
5
+
6
+ :- dynamic(elk/1).
7
+ elk(X) :- moose(X).
8
+
9
+ moose(bertha).
10
+
11
+ %% goal: public_clause(X0)
12
+
13
+ public_clause(Body) :-
14
+ clause(elk(bertha), Body).
15
+
16
+ %% goal: private_clause(X0)
17
+
18
+ private_clause(Culprit) :-
19
+ catch(clause(moose(_), _), error(Formal, _), true),
20
+ Formal = permission_error(access, private_procedure, Culprit).
21
+
22
+ %% goal: asserted_clause(X0)
23
+
24
+ asserted_clause(Body) :-
25
+ assertz(caribou(rudolf)),
26
+ clause(caribou(_), Body).
@@ -1,3 +1,8 @@
1
+ % clause/2 may only inspect public (dynamic) procedures. Predicates that are
2
+ % read back with clause/2 below are declared dynamic; the rest stay static.
3
+ :- dynamic(parent/2).
4
+ :- dynamic(same/1).
5
+
1
6
  b(2, two).
2
7
  b(1, one).
3
8
  b(1, one).
@@ -2,6 +2,8 @@
2
2
  % https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#37
3
3
  %% goal: clause_variable_identity
4
4
 
5
+ % clause/2 may only inspect a public procedure, so a/1 is declared dynamic.
6
+ :- dynamic(a/1).
5
7
  a(X) :- b(X).
6
8
 
7
9
  clause_variable_identity :-
@@ -0,0 +1,13 @@
1
+ % A procedure defined by a Prolog text is static, and therefore private, unless
2
+ % a dynamic/1 directive makes it public (ISO 7.5.2, 8.9). clause/2 may only
3
+ % inspect a public procedure (ISO 8.8.1.3), so reading a static user procedure
4
+ % is a permission error rather than a source of solutions.
5
+ % https://github.com/eyereasoner/eyeprolog/issues/96
6
+ %% goal: answer
7
+
8
+ elk(X) :- moose(X).
9
+
10
+ moose(bertha).
11
+
12
+ answer :-
13
+ clause(elk(N), Body).
@@ -0,0 +1,4 @@
1
+ text_body(call(true)).
2
+ nested_body(((call(a) ; call(b)), (call(c) -> true ; true))).
3
+ assert_agrees_with_text(agree).
4
+ cut_is_local([taken, taken]).
@@ -0,0 +1,3 @@
1
+ public_clause(moose(bertha)).
2
+ private_clause(moose / 1).
3
+ asserted_clause(true).
@@ -0,0 +1 @@
1
+ error(permission_error(access, private_procedure), /(elk, 1))
@@ -143,6 +143,11 @@ function dcgConformanceSection() {
143
143
  '`type_error(list, S)` when an argument is neither a list nor a partial list.',
144
144
  'These implementation-defined checks follow ISO/IEC TS 13211-3:2025,',
145
145
  '8.18.1.3 g and h; this behavior is not a known deviation.',
146
+ 'The checks are optional. EyeProlog elects to perform both consistently.',
147
+ 'Dedicated regressions require the exact error for atomic non-lists and',
148
+ 'improper lists across both arities and both sequence positions, while',
149
+ 'accepting variables, proper lists, and partial lists. The upstream quads',
150
+ 'allow checking and non-checking outcomes and do not prove this policy.',
146
151
  '',
147
152
  'The Part 3 implementation target is',
148
153
  '[ISO/IEC TS 13211-3:2025](https://www.iso.org/standard/83635.html).',
@@ -1230,6 +1230,44 @@ why(
1230
1230
  assertEqual(forbiddenFresh.failed, 1, 'fresh thrown variable is detected');
1231
1231
  },
1232
1232
  },
1233
+ {
1234
+ name: 'phrase terminal-sequence checks are consistent across arities and list shapes (issue #94)',
1235
+ run: () => {
1236
+ // The upstream quads allow both checking and non-checking processors.
1237
+ // Require EyeProlog's chosen diagnostic, not either portable outcome.
1238
+ const bodies = ['[]', '[a]', '{true}', '{fail}', '{write(reached)}', '!', 'p'];
1239
+ const invalid = ['non_list', '0', 'f(a)', '[a|non_list]', '[a,b|0]', '[a,b,c|f(t)]'];
1240
+ for (const body of bodies) {
1241
+ for (const bad of invalid) {
1242
+ for (const call of [`phrase(${body}, Bad)`, `phrase(${body}, Bad, [])`,
1243
+ `phrase(${body}, [], Bad)`]) {
1244
+ let output = '';
1245
+ runEyeProlog('p --> [a].', {
1246
+ ioOptions: { write: (text) => { output += text; } },
1247
+ goal: `Bad=${bad}, catch((${call}, write(missed)), error(type_error(list, Bad), _), write(ok))`,
1248
+ });
1249
+ assertEqual(output, 'ok', `${call} with ${bad}`);
1250
+ }
1251
+ }
1252
+ }
1253
+ for (const goal of [
1254
+ 'phrase([], [])',
1255
+ 'phrase([a], [a])',
1256
+ 'phrase([a], L), L == [a]',
1257
+ 'phrase([a], [a|T]), T == []',
1258
+ 'phrase([], S, S), var(S)',
1259
+ 'phrase([], [a|T], [a|T]), var(T)',
1260
+ 'phrase([], L, [a|T]), L == [a|T], var(T)',
1261
+ 'phrase([a], [a|T], T), var(T)',
1262
+ 'phrase([f(a),1], [f(a),1])',
1263
+ ]) {
1264
+ let output = '';
1265
+ runEyeProlog('', { goal: `(${goal}), write(ok)`,
1266
+ ioOptions: { write: (text) => { output += text; } } });
1267
+ assertEqual(output, 'ok', goal);
1268
+ }
1269
+ },
1270
+ },
1233
1271
  {
1234
1272
  name: 'runQuads matches the corrected ISO phrase quad boundaries',
1235
1273
  run: () => {
@@ -4813,6 +4851,132 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
4813
4851
  assertEqual(answers.length, 1, 'dynamic clause answer count');
4814
4852
  },
4815
4853
  },
4854
+ {
4855
+ // ISO 7.6.2 a/b, checked against the 8.8.1.4 worked example for legs/2.
4856
+ name: 'preparing a Prolog text converts variable body goals to call/1',
4857
+ run: () => {
4858
+ const program = Program.parse(':- dynamic(legs/2).\nlegs(A, 7) :- A, call(A).\n');
4859
+ const solver = new Solver(program, {});
4860
+ const goal = parseGoalText('clause(legs(c, 7), Body)');
4861
+ const answers = [...solver.solve([goal], new Env(), 0)];
4862
+ assertEqual(answers.length, 1, 'clause answer count');
4863
+ assertEqual(termToString(copyResolved(goal.args[1], answers[0])), '(call(c), call(c))', 'converted body');
4864
+ },
4865
+ },
4866
+ {
4867
+ name: 'preparation and assertz/1 agree on clause body conversion',
4868
+ run: () => {
4869
+ const fromText = Program.parse(':- dynamic(p/1).\np(G) :- G.\n');
4870
+ const textGoal = parseGoalText('clause(p(true), B)');
4871
+ const textAnswers = [...new Solver(fromText, {}).solve([textGoal], new Env(), 0)];
4872
+ const asserted = Program.parse('run(B) :- assertz((q(G) :- G)), clause(q(true), B).\n');
4873
+ const assertGoal = parseGoalText('run(B)');
4874
+ const assertAnswers = [...new Solver(asserted, {}).solve([assertGoal], new Env(), 0)];
4875
+ assertEqual(textAnswers.length, 1, 'text clause answer count');
4876
+ assertEqual(assertAnswers.length, 1, 'asserted clause answer count');
4877
+ assertEqual(
4878
+ termToString(copyResolved(textGoal.args[1], textAnswers[0])),
4879
+ termToString(copyResolved(assertGoal.args[0], assertAnswers[0])),
4880
+ 'converted bodies agree',
4881
+ );
4882
+ },
4883
+ },
4884
+ {
4885
+ // 7.6.2 b converts the arguments of ;/2 and ->/2 recursively.
4886
+ name: 'body conversion recurses through disjunction and if-then',
4887
+ run: () => {
4888
+ const program = Program.parse(':- dynamic(r/3).\nr(A, B, C) :- (A ; B), (C -> true ; true).\n');
4889
+ const solver = new Solver(program, {});
4890
+ const goal = parseGoalText('clause(r(a, b, c), Body)');
4891
+ const answers = [...solver.solve([goal], new Env(), 0)];
4892
+ assertEqual(answers.length, 1, 'clause answer count');
4893
+ assertEqual(
4894
+ termToString(copyResolved(goal.args[1], answers[0])),
4895
+ "(';'(call(a), call(b)), ';'(->(call(c), true), true))",
4896
+ 'recursively converted body',
4897
+ );
4898
+ },
4899
+ },
4900
+ {
4901
+ // 7.8.3: a cut inside call/1 is local, so the remaining clauses survive.
4902
+ name: 'a variable body goal from a Prolog text keeps cut local',
4903
+ run: () => {
4904
+ const program = Program.parse('s(G) :- G.\ns(_) :- true.\n');
4905
+ const solver = new Solver(program, {});
4906
+ const answers = [...solver.solve([parseGoalText('s(!)')], new Env(), 0)];
4907
+ assertEqual(answers.length, 2, 'cut did not prune the second clause');
4908
+ },
4909
+ },
4910
+ {
4911
+ // https://github.com/eyereasoner/eyeprolog/issues/96
4912
+ name: 'clause/2 keeps static procedures private in the default mode',
4913
+ run: () => {
4914
+ const program = Program.parse('elk(X) :- moose(X).\nmoose(bertha).\n');
4915
+ const solver = new Solver(program, {});
4916
+ let caught = null;
4917
+ try {
4918
+ [...solver.solve([parseGoalText('clause(elk(N), Body)')], new Env(), 0)];
4919
+ } catch (error) {
4920
+ caught = error;
4921
+ }
4922
+ if (!caught) throw new Error('clause/2 unexpectedly inspected a static procedure');
4923
+ assertEqual(caught.formal, 'permission_error(access, private_procedure)', 'formal error');
4924
+ assertEqual(termToString(caught.culprit), '/(elk, 1)', 'culprit predicate indicator');
4925
+ },
4926
+ },
4927
+ {
4928
+ name: 'clause/2 inspects a procedure declared dynamic in the default mode',
4929
+ run: () => {
4930
+ const program = Program.parse(':- dynamic(elk/1).\nelk(X) :- moose(X).\nmoose(bertha).\n');
4931
+ const solver = new Solver(program, {});
4932
+ const answers = [...solver.solve([parseGoalText('clause(elk(N), Body)')], new Env(), 0)];
4933
+ assertEqual(answers.length, 1, 'dynamic clause answer count');
4934
+ },
4935
+ },
4936
+ {
4937
+ name: 'clause/2 inspects procedures created by assertz/1 in the default mode',
4938
+ run: () => {
4939
+ const program = Program.parse('run(Body) :- assertz(elk(bertha)), clause(elk(_), Body).\n');
4940
+ const solver = new Solver(program, {});
4941
+ const answers = [...solver.solve([parseGoalText('run(Body)')], new Env(), 0)];
4942
+ assertEqual(answers.length, 1, 'asserted clause answer count');
4943
+ },
4944
+ },
4945
+ {
4946
+ name: 'clause/2 and assertz/1 agree on which procedures are static',
4947
+ run: () => {
4948
+ const program = Program.parse('elk(bertha).\n');
4949
+ const solver = new Solver(program, {});
4950
+ const formals = [];
4951
+ for (const goal of ['clause(elk(_), Body)', 'assertz(elk(clara))', 'retract(elk(bertha))']) {
4952
+ try {
4953
+ [...solver.solve([parseGoalText(goal)], new Env(), 0)];
4954
+ formals.push(null);
4955
+ } catch (error) {
4956
+ formals.push(error.formal);
4957
+ }
4958
+ }
4959
+ assertEqual(formals[0], 'permission_error(access, private_procedure)', 'clause/2 access');
4960
+ assertEqual(formals[1], 'permission_error(modify, static_procedure)', 'assertz/1 modification');
4961
+ assertEqual(formals[2], 'permission_error(modify, static_procedure)', 'retract/1 modification');
4962
+ },
4963
+ },
4964
+ {
4965
+ // A static procedure stays private even when clause/2 is also given a
4966
+ // non-callable body, matching the existing strict-mode error ordering.
4967
+ name: 'clause/2 reports private access before body callability',
4968
+ run: () => {
4969
+ const program = Program.parse('elk(bertha).\n');
4970
+ const solver = new Solver(program, {});
4971
+ let caught = null;
4972
+ try {
4973
+ [...solver.solve([parseGoalText('clause(elk(_), 4)')], new Env(), 0)];
4974
+ } catch (error) {
4975
+ caught = error;
4976
+ }
4977
+ assertEqual(caught?.formal, 'permission_error(access, private_procedure)', 'formal error');
4978
+ },
4979
+ },
4816
4980
  {
4817
4981
  name: 'strict ISO core mode disables normal-profile recursion planning',
4818
4982
  run: () => {
@@ -5870,10 +5870,14 @@ look_ahead(X), [X] --> [X].
5870
5870
  `phrase(+Body,?Sequence)` accepts or generates a complete sequence.
5871
5871
  `phrase(+Body,?Sequence,?Rest)` leaves `Rest` unconsumed and is steadfast in
5872
5872
  that argument. A variable body raises `instantiation_error`; a non-callable
5873
- body raises `type_error(callable)`. EyeProlog performs terminal-sequence checks and currently reports
5874
- `type_error(list)` for a rejected non-list. This is a normal-profile
5875
- interoperability choice, not a Part 3 conformance claim; Part 3 defines a
5876
- dedicated terminal-sequence error category. See
5873
+ body raises `type_error(callable)`. EyeProlog elects to perform the optional
5874
+ terminal-sequence checks of ISO/IEC TS 13211-3:2025, 8.18.1.3 g and h.
5875
+ It consistently reports `type_error(list, Culprit)` for invalid input in both
5876
+ arities and invalid remainder in `phrase/3`, including improper lists.
5877
+ Variables, proper lists, and partial lists pass these checks. Validation
5878
+ precedes grammar execution; an otherwise valid failing grammar does not
5879
+ suppress the diagnostic. Dedicated regressions enforce this policy separately
5880
+ from the portable quads, which accept both checking and non-checking outcomes. See
5877
5881
  [`ISO-PART2-PART3-SCOPE.md`](test/conformance/ISO-PART2-PART3-SCOPE.md).
5878
5882
 
5879
5883
  #### A bidirectional expression grammar
@@ -6142,7 +6146,7 @@ partial list.
6142
6146
 
6143
6147
  #### Dynamic database and procedure information
6144
6148
 
6145
- - **`clause(+Head,?Body)`** — Enumerates fresh copies of source clauses matching the callable `Head`; facts have body `true`. Access to built-ins raises *permission_error(access,private_procedure)*.
6149
+ - **`clause(+Head,?Body)`** — Enumerates fresh copies of source clauses matching the callable `Head`; facts have body `true`. Only *public* procedures can be inspected: a procedure defined by a Prolog text is static unless a *dynamic/1* directive declares it, and a procedure first created by *assertz/1* or *asserta/1* is dynamic. Access to a static user procedure, or to a built-in, raises *permission_error(access,private_procedure)*. This applies in every execution mode, not only under `--iso-strict`, and matches the *permission_error(modify,static_procedure)* that *assertz/1* and *retract/1* already raise for the same procedures.
6146
6150
  - **`asserta(+Clause)`, `assertz(+Clause)`** — Insert a copied fact or rule at the beginning or end of a predicate declared *dynamic/1*. Static and built-in procedures cannot be modified.
6147
6151
  - **`retract(+Clause)`** — Removes matching dynamic clauses one at a time on backtracking. A call sees the logical update view captured when it began. A fact pattern matches facts only.
6148
6152
  - **`retractall(+Head)`** — Removes every matching clause from a dynamic procedure, succeeds when none match, and keeps the empty dynamic procedure known.
@@ -10361,7 +10365,7 @@ must preserve the same observable outcome. Additional normal-mode syntax may
10361
10365
  accept texts outside the strict grammar, but it may not reinterpret an accepted
10362
10366
  standard case.
10363
10367
 
10364
- The file-based conformance corpus contains 805 cases, including 388 focused ISO cases derived from the success, failure, mode, and error behavior in ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
10368
+ The file-based conformance corpus contains 808 cases, including 391 focused ISO cases derived from the success, failure, mode, and error behavior in ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
10365
10369
  Separate exact-output suites check 210 normal examples and 61 proof examples; all executable chapter programs are parsed and their declared goals are executed. The eight-case
10366
10370
  playground contract suite imports the production worker, sends real reasoning
10367
10371
  requests through its message protocol, and crawls the served module graph for