eyeprolog 1.2.3 → 1.2.5

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 (38) hide show
  1. package/conformance-report.md +2 -2
  2. package/examples/access-control-policy.pl +2 -0
  3. package/examples/d3-group.pl +2 -0
  4. package/examples/fast-fourier-transform.pl +1 -0
  5. package/examples/map-four-color-search.pl +2 -0
  6. package/examples/partial-evaluator.pl +2 -0
  7. package/examples/weighted-interval-scheduling.pl +2 -0
  8. package/package.json +1 -1
  9. package/src/iso.js +5 -2
  10. package/src/parser.js +4 -0
  11. package/src/repl.js +37 -11
  12. package/src/solver.js +1 -1
  13. package/src/write.js +2 -0
  14. package/test/conformance/ISO-COMPLIANCE.md +1 -1
  15. package/test/conformance/ISO-MATRIX.md +4 -4
  16. package/test/conformance/README.md +1 -1
  17. package/test/conformance/cases/aggregation/057_countall_empty_and_nonempty.pl +2 -0
  18. package/test/conformance/cases/aggregation/075_aggregation_edges.pl +2 -0
  19. package/test/conformance/cases/aggregation/aggregate_max_no_answers_fails.pl +2 -0
  20. package/test/conformance/cases/aggregation/countall_zero.pl +2 -0
  21. package/test/conformance/cases/aggregation/findall_empty_bag.pl +2 -0
  22. package/test/conformance/cases/aggregation/sumall_empty_zero.pl +2 -0
  23. package/test/conformance/cases/control/074_forall_edges.pl +2 -0
  24. package/test/conformance/cases/control/extra_not_unbound_unknown_goal_succeeds.pl +2 -0
  25. package/test/conformance/cases/control/extra_once_empty_fails.pl +2 -0
  26. package/test/conformance/cases/control/forall_empty_generator_true.pl +2 -0
  27. package/test/conformance/cases/iso/wg17_syntax_high_risk.pl +5 -1
  28. package/test/conformance/cases/negation/not_unknown_goal_succeeds.pl +2 -0
  29. package/test/conformance/errors/iso/wg17_invalid_octal_escape.pl +1 -0
  30. package/test/conformance/expected/iso/wg17_syntax_high_risk.pl +2 -0
  31. package/test/conformance/expected-errors/iso/wg17_invalid_octal_escape.txt +1 -0
  32. package/test/conformance/warnings/negation/extra_stratified_multiple_negations_quiet.pl +2 -0
  33. package/test/conformance/warnings/negation/extra_warning_preserves_empty_stdout.pl +2 -0
  34. package/test/conformance/warnings/negation/negative_unknown_group_quiet.pl +2 -0
  35. package/test/conformance/warnings/negation/stratified_negative_unknown_after_positive_quiet.pl +2 -0
  36. package/test/conformance/warnings/negation/stratified_quiet.pl +2 -0
  37. package/test/run-regression.mjs +83 -1
  38. package/the-art-of-eyeprolog.md +19 -11
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
11
11
  | builtins | 11 | 0 | 0 | 0 | 11 |
12
12
  | context | 11 | 0 | 0 | 0 | 11 |
13
13
  | control | 15 | 0 | 0 | 0 | 15 |
14
- | iso | 167 | 209 | 0 | 0 | 376 |
14
+ | iso | 167 | 210 | 0 | 0 | 377 |
15
15
  | lists | 52 | 3 | 0 | 0 | 55 |
16
16
  | modules | 2 | 0 | 0 | 0 | 2 |
17
17
  | negation | 8 | 0 | 19 | 0 | 27 |
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
23
23
  | terms | 26 | 3 | 0 | 0 | 29 |
24
24
  | unification | 18 | 0 | 0 | 0 | 18 |
25
25
  | variables | 16 | 9 | 0 | 0 | 25 |
26
- | **Total** | **482** | **260** | **19** | **21** | **782** |
26
+ | **Total** | **482** | **261** | **19** | **21** | **783** |
@@ -53,3 +53,5 @@ status(test1, policy_passed) :-
53
53
 
54
54
  reason(test1, "all required claims are present, one allowed claim is present, and no forbidden claim is present") :-
55
55
  passes_policy(test1, policy_x).
56
+
57
+ :- set_prolog_flag(unknown, fail).
@@ -102,3 +102,5 @@ subgroupCount(d3_group, Count) :-
102
102
  length(Groups, Count).
103
103
 
104
104
  reason(d3_group, "findall enumerates candidate subgroups and sort gives canonical subgroup order").
105
+
106
+ :- set_prolog_flag(unknown, fail).
@@ -53,3 +53,4 @@ node(n(A1, N1), [n(A, N)|T]-[n(A1, N1), n(A, N)|T]) :-
53
53
  % query
54
54
  %% goal: fft([0, 1, 2, 3, 4, 5, 6, 7], _)
55
55
 
56
+ :- set_prolog_flag(unknown, fail).
@@ -66,3 +66,5 @@ four_color_answer(european_union, Coloring) :-
66
66
  place_order(Places),
67
67
  place_pairs(Places, Coloring),
68
68
  once(color_places(Coloring)).
69
+
70
+ :- set_prolog_flag(unknown, fail).
@@ -72,3 +72,5 @@ residual_program(Name, Residual) :- program(Name, Expr, Env), pe(Env, Expr, Resi
72
72
 
73
73
  partialEvalAnswer(residual(Name), Residual) :- residual_program(Name, Residual).
74
74
  partialEvalAnswer(note, "static inputs are folded while dynamic variables remain as residual code") :- residual_program(poly_y, _).
75
+
76
+ :- set_prolog_flag(unknown, fail).
@@ -82,3 +82,5 @@ weighted_interval_answer(chosen_interval, interval(I, Start, Finish, Value)) :-
82
82
  chosen_from(1, I),
83
83
  interval(I, Start, Finish, Value).
84
84
  weighted_interval_answer(candidate_count, Count) :- countall(interval(_i, _start, _finish, _value), Count).
85
+
86
+ :- set_prolog_flag(unknown, fail).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.3",
6
+ "version": "1.2.5",
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
@@ -1075,9 +1075,12 @@ function quotedEscapeEnd(source, index) {
1075
1075
  while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
1076
1076
  return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
1077
1077
  }
1078
- if (/^[0-7]$/.test(escaped)) {
1078
+ if (/^[0-9]$/.test(escaped)) {
1079
1079
  let cursor = index + 1;
1080
- while (/^[0-7]$/.test(source[cursor] ?? '')) cursor++;
1080
+ // Scan all decimal digits here, including 8 and 9. This scanner only
1081
+ // locates the end of a candidate quoted escape; the parser remains
1082
+ // authoritative and rejects non-octal digits.
1083
+ while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
1081
1084
  return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
1082
1085
  }
1083
1086
 
package/src/parser.js CHANGED
@@ -303,6 +303,10 @@ class Parser {
303
303
  if (this.take() !== '\\') throw new Error(`parse line ${line}: bad octal escape`);
304
304
  return String.fromCodePoint(Number.parseInt(digits, 8));
305
305
  }
306
+ // A backslash followed by a decimal digit is numeric-escape syntax, but
307
+ // ISO octal digits are limited to 0..7. Do not reinterpret \8 or \9 as
308
+ // implementation-specific one-character escapes.
309
+ if (/^[0-9]$/.test(escaped)) throw new Error(`parse line ${line}: bad octal escape`);
306
310
  return escaped;
307
311
  }
308
312
  nextToken() {
package/src/repl.js CHANGED
@@ -34,7 +34,7 @@ export async function runRepl(engine, options = {}) {
34
34
  const goal = parseGoal(engine, state, text);
35
35
  if (!options.isoStrict && isUseModuleGoal(goal)) {
36
36
  sources.push({ text: `:- ${text}.\n`, filename: '<repl>' });
37
- state = makeState(engine, sources, output, options);
37
+ state = makeState(engine, sources, output, options, state);
38
38
  state.solver.runInitializations();
39
39
  output.write(' true.\n');
40
40
  continue;
@@ -42,17 +42,22 @@ export async function runRepl(engine, options = {}) {
42
42
  const consultFiles = options.isoStrict ? null : consultDesignations(engine, goal);
43
43
  if (consultFiles != null) {
44
44
  for (const filename of consultFiles) sources.push(await readSource(filename));
45
- state = makeState(engine, sources, output, options);
45
+ state = makeState(engine, sources, output, options, state);
46
46
  state.solver.runInitializations();
47
47
  output.write(' true.\n');
48
48
  continue;
49
49
  }
50
50
 
51
- await prepareInteractiveTermInput(state, goal, reader);
52
- const result = await solveQuery(engine, state, goal, reader, output);
53
- if (result?.halted) {
54
- exitCode = result.code;
55
- break;
51
+ const flagsBefore = snapshotFlagValues(state.solver);
52
+ try {
53
+ await prepareInteractiveTermInput(state, goal, reader);
54
+ const result = await solveQuery(engine, state, goal, reader, output);
55
+ if (result?.halted) {
56
+ exitCode = result.code;
57
+ break;
58
+ }
59
+ } finally {
60
+ rememberFlagOverrides(state, flagsBefore);
56
61
  }
57
62
  } catch (error) {
58
63
  if (error?.name === 'HaltSignal') {
@@ -143,7 +148,7 @@ class LineReader {
143
148
  }
144
149
  }
145
150
 
146
- function makeState(engine, sources, output, options = {}) {
151
+ function makeState(engine, sources, output, options = {}, previousState = null) {
147
152
  const strictIso = options.isoStrict === true;
148
153
  const program = engine.Program.parseSources(sources, { strictIso, sourceMetadata: strictIso });
149
154
  const solver = new engine.Solver(program, {
@@ -151,7 +156,25 @@ function makeState(engine, sources, output, options = {}) {
151
156
  isoStrict: strictIso,
152
157
  ioOptions: { write: (text) => output.write(String(text)) },
153
158
  });
154
- return { program: solver.program, solver, strictIso };
159
+ const flagOverrides = new Map(previousState?.flagOverrides ?? []);
160
+ for (const [name, value] of flagOverrides) {
161
+ const definition = solver.prologFlags.get(name);
162
+ if (definition?.changeable && definition.allowed.includes(value)) {
163
+ definition.value = engine.atom(value);
164
+ }
165
+ }
166
+ return { program: solver.program, solver, strictIso, flagOverrides };
167
+ }
168
+
169
+ function snapshotFlagValues(solver) {
170
+ return new Map([...solver.prologFlags].map(([name, definition]) => [name, definition.value?.name]));
171
+ }
172
+
173
+ function rememberFlagOverrides(state, before) {
174
+ for (const [name, definition] of state.solver.prologFlags) {
175
+ const value = definition.value?.name;
176
+ if (before.get(name) !== value) state.flagOverrides.set(name, value);
177
+ }
155
178
  }
156
179
 
157
180
  async function readQuery(reader) {
@@ -230,9 +253,12 @@ function quotedEscapeEnd(source, index) {
230
253
  while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
231
254
  return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
232
255
  }
233
- if (/^[0-7]$/.test(escaped)) {
256
+ if (/^[0-9]$/.test(escaped)) {
234
257
  let cursor = index + 1;
235
- while (/^[0-7]$/.test(source[cursor] ?? '')) cursor++;
258
+ // Scan all decimal digits here, including 8 and 9. This scanner only
259
+ // locates the end of a candidate quoted escape; the parser remains
260
+ // authoritative and rejects non-octal digits.
261
+ while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
236
262
  return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
237
263
  }
238
264
 
package/src/solver.js CHANGED
@@ -49,7 +49,7 @@ export class Solver {
49
49
  this.inferenceLimitExceeded = false;
50
50
  this.solutionLimit = options.solutionLimit ?? 10000000;
51
51
  this.solutionsSeen = 0;
52
- this.prologFlags = options.prologFlags ?? defaultPrologFlags(this.registry?.eyePrologLibrary ? 'fail' : 'error', this.isoStrict);
52
+ this.prologFlags = options.prologFlags ?? defaultPrologFlags('error', this.isoStrict);
53
53
  if (this.isoStrict) {
54
54
  for (const name of [...this.prologFlags.keys()]) {
55
55
  if (!ISO_CORE_FLAG_NAMES.has(name)) this.prologFlags.delete(name);
package/src/write.js CHANGED
@@ -22,6 +22,7 @@ function quoteAtom(name) {
22
22
  for (const ch of name) {
23
23
  if (ch === "'") out += "''";
24
24
  else if (ch === '\\') out += '\\\\';
25
+ else if (ch === '\x00') out += '\\0\\';
25
26
  else if (ch === '\x07') out += '\\a';
26
27
  else if (ch === '\b') out += '\\b';
27
28
  else if (ch === '\r') out += '\\r';
@@ -59,6 +60,7 @@ function writeString(value) {
59
60
  let out = '"';
60
61
  for (const ch of value) {
61
62
  if (ch === '"' || ch === '\\') out += `\\${ch}`;
63
+ else if (ch === '\x00') out += '\\0\\';
62
64
  else if (ch === '\x07') out += '\\a';
63
65
  else if (ch === '\b') out += '\\b';
64
66
  else if (ch === '\r') out += '\\r';
@@ -38,7 +38,7 @@ error-ordering alternative to an individual executable assertion.
38
38
  | 7.8 — control constructs and exceptions | audit | call, cut, conjunction, disjunction, if-then, catch/throw, renamed-copy tests. |
39
39
  | 7.9 — expression evaluation | audit | Arithmetic/evaluation/error suites, including Corrigenda. Exceptional-value/error-precedence matrix remains to be exhaustively enumerated. |
40
40
  | 7.10 — input/output concepts | audit | Stream, character/byte I/O, read/write options, operator-sensitive write-back, Corrigendum 3 writer cases. Full option cross-product remains open. |
41
- | 7.11 — flags | audit | Required Part 1 flags implemented. Strict mode excludes the EyeProlog `occurs_check` extension and uses ISO `unknown=error`. |
41
+ | 7.11 — flags | audit | Required Part 1 flags implemented. Normal and strict modes use the ISO `unknown=error` default; strict mode excludes the EyeProlog `occurs_check` extension. |
42
42
  | 7.12 — errors | audit | ISO `error(Error, Context)` envelope, type/domain/permission/representation/evaluation/syntax families and focused error cases. Complete prescribed-error ordering remains open. |
43
43
  | 8.2-8.17 — built-in predicates | audit | Predicate-family coverage is mapped in ISO-MATRIX.md; Corrigendum 2 additions (`subsumes_term/2`, `term_variables/2`, `call/2..8`, `false/0`) are in the strict registry. Mode/error matrix is not yet one-row-per-standard-row. |
44
44
  | Clause 9 — evaluable functors | audit | Integer/float/rounding/transcendental/bitwise suites and corrigendum cases. Host floating-point representation choices remain documented implementation-defined behavior. |
@@ -8,7 +8,7 @@ compliance audit and the remaining work before a full conformance claim.
8
8
 
9
9
  | Standard area | Implementation | Representative executable coverage |
10
10
  | --- | --- | --- |
11
- | Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, `wg17_syntax_high_risk`, syntax error cases |
11
+ | Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, `wg17_syntax_high_risk`, `wg17_invalid_octal_escape`, syntax error cases |
12
12
  | Clause 7 term order and unification | finite-tree unification, identity, standard order, errors | `unification_control_information`, `swipl_occurs_check`, `term_modes_and_ordering`, `logtalk_compare_standard_order` |
13
13
  | Clause 7 control and exceptions | call, cut, conjunction, disjunction, if-then-else, catch and throw | `cut_control`, `control_and_terms`, `exceptions_and_flags`, `corrigenda_catch_callability`, `throw_copies_ball` |
14
14
  | 8.2-8.5 term predicates | unification, Corrigendum 2 tests, comparison, sorting, creation and decomposition | `corrigenda_term_predicates`, `corrigenda_sort_keysort`, `logtalk_arg_unification`, `logtalk_univ`, associated error cases |
@@ -31,9 +31,9 @@ integer powers (Cor.3).
31
31
  Implementation-defined choices are documented in *The Art of EyeProlog*:
32
32
  integers and arity are unbounded by the Prolog model (subject to host memory),
33
33
  ordinary unification performs an occurs check, `double_quotes` defaults to
34
- `chars`, strict core mode uses ISO `unknown=error` while normal EyeProlog
35
- defaults `unknown` to `fail`, `//` rounds toward zero, floating-point operations
36
- use finite ECMAScript numbers, and character codes use Unicode scalar values.
34
+ `chars`, both strict core mode and normal EyeProlog use the ISO
35
+ `unknown=error` default, `//` rounds toward zero, floating-point operations use
36
+ finite ECMAScript numbers, and character codes use Unicode scalar values.
37
37
 
38
38
  This is an executable conformance matrix, not a certification issued by an
39
39
  independent standards body. Release gating runs the ISO cases and the dedicated
@@ -102,7 +102,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
102
102
  Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
103
103
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
104
104
 
105
- The corpus has 376 cases in `iso/` and 782 file-based conformance cases in
105
+ The corpus has 377 cases in `iso/` and 783 file-based conformance cases in
106
106
  total. The generated `conformance-report.md` is the authoritative source for
107
107
  current category totals. Together with regression, documentation-sync, API,
108
108
  example, and book-example checks, `npm test` is the release gate.
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  item(a).
2
4
  item(b).
3
5
  answer(counts, counts(C, Z)) :- countall(item(X), C), countall(missing(X), Z).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  % Reference 9.1: aggregation handles empty result sets, structured templates, and ordered best answers.
2
4
  %% goal: answer(X0, X1)
3
5
 
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  answer(ok) :- \+ aggregate_max(Key, Value, missing(Value), BestKey, BestValue).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  answer(N) :- countall(missing(X), N).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  answer(Bag) :- findall(X, missing(X), Bag).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  answer(N) :- sumall(X, missing(X), N).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  % Reference 9.1: forall/2 succeeds for every generated binding, including the empty generator case.
2
4
  %% goal: answer(X0, X1)
3
5
 
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  answer(not_unbound_unknown_goal_succeeds) :- \+ missing(X).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  answer(once_empty_fails) :- once(missing).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  answer(ok) :- \+ empty_counterexample.
@@ -1,6 +1,6 @@
1
1
  % High-risk ISO syntax/write regressions, independently derived from
2
2
  % ISO/IEC 13211-1 clauses 6.3, 6.4 and 7.10 and cross-checked against the
3
- % public WG17 conformity-testing syntax cases (#1, #14-15, #28-31, #33-34).
3
+ % public WG17 conformity-testing syntax cases (#1, #14-15, #28-31, #33-34, #301).
4
4
 
5
5
  %% goal: wg17_numeric_escape
6
6
  wg17_numeric_escape :-
@@ -24,3 +24,7 @@ wg17_operator_precedence :-
24
24
  %% goal: wg17_canonical_list
25
25
  wg17_canonical_list :-
26
26
  write_canonical([a]), nl.
27
+
28
+ %% goal: wg17_zero_character_escape
29
+ wg17_zero_character_escape :-
30
+ writeq('\0\'), nl.
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  % Negation succeeds when its inner goal has no solution.
2
4
  %% goal: answer(X0)
3
5
 
@@ -11,3 +11,5 @@ a :- b, c
11
11
  wg17_operator_precedence.
12
12
  '.'(a,[])
13
13
  wg17_canonical_list.
14
+ '\0\'
15
+ wg17_zero_character_escape.
@@ -0,0 +1 @@
1
+ parse line 1: bad octal escape
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  p(a).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  p(a) :- \+ q(a).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  answer(ok) :- \+ missing(ok).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  %% goal: answer(X0)
2
4
 
3
5
  item(a).
@@ -1,3 +1,5 @@
1
+ :- set_prolog_flag(unknown, fail).
2
+
1
3
  % Stratified negation emits no portability warning.
2
4
  %% goal: answer(X0)
3
5
 
@@ -671,6 +671,31 @@ c4 ?- call((!;1)).
671
671
  assertEqual(result.stderr, '', 'stderr');
672
672
  },
673
673
  },
674
+ {
675
+ name: 'writeq preserves the NUL character with an ISO octal escape',
676
+ run: () => {
677
+ const result = runCli([], {
678
+ input: "writeq('\\0\\').\nhalt.\n",
679
+ });
680
+ assertEqual(result.status, 0, 'exit status');
681
+ assertEqual(result.stdout, "?- '\\0\\' true.\n?- ", 'stdout');
682
+ assertEqual(result.stderr, '', 'stderr');
683
+ },
684
+ },
685
+ {
686
+ name: 'REPL rejects non-octal numeric escapes without waiting for continuation',
687
+ run: () => {
688
+ const result = runCli([], {
689
+ input: "'\\8\\'.\nhalt.\n",
690
+ });
691
+ assertEqual(result.status, 0, 'exit status');
692
+ assertEqual(result.stdout,
693
+ '?- parse line 1: bad octal escape.\n' +
694
+ '?- ',
695
+ 'stdout');
696
+ assertEqual(result.stderr, '', 'stderr');
697
+ },
698
+ },
674
699
  {
675
700
  name: 'REPL read predicates consume following interactive term input',
676
701
  run: () => {
@@ -716,6 +741,36 @@ c4 ?- call((!;1)).
716
741
  assertEqual(result.stderr, '', 'stderr');
717
742
  },
718
743
  },
744
+ {
745
+ name: 'REPL preserves runtime unknown flag across consultation',
746
+ run: () => {
747
+ const filename = path.join(tmp, `repl-empty-${++tmpCounter}.pl`);
748
+ fs.writeFileSync(filename, '');
749
+ const result = runCli([], {
750
+ input:
751
+ 'current_prolog_flag(unknown, V).\n' +
752
+ 'set_prolog_flag(unknown, fail).\n' +
753
+ `[${sourceAtom(filename)}].\n` +
754
+ 'current_prolog_flag(unknown, V).\n' +
755
+ 'set_prolog_flag(unknown, error).\n' +
756
+ `[${sourceAtom(filename)}].\n` +
757
+ 'missing_after_consult.\n' +
758
+ 'halt.\n',
759
+ });
760
+ assertEqual(result.status, 0, 'exit status');
761
+ assertEqual(result.stdout,
762
+ '?- V = error.\n' +
763
+ '?- true.\n' +
764
+ '?- true.\n' +
765
+ '?- V = fail.\n' +
766
+ '?- true.\n' +
767
+ '?- true.\n' +
768
+ '?- error(existence_error(procedure, missing_after_consult / 0), eyeprolog).\n' +
769
+ '?- ',
770
+ 'stdout');
771
+ assertEqual(result.stderr, '', 'stderr');
772
+ },
773
+ },
719
774
  {
720
775
  name: 'REPL use_module imports Part 2 library predicates',
721
776
  run: () => {
@@ -723,7 +778,11 @@ c4 ?- call((!;1)).
723
778
  input: 'append(X, Y, [1, 2, 3, 4]).\nuse_module(library(lists)).\nappend(X, Y, [1, 2, 3, 4]).\n\nhalt.\n',
724
779
  });
725
780
  assertEqual(result.status, 0, 'exit status');
726
- assertEqual(result.stdout, '?- false.\n?- true.\n?- X = [], Y = [1, 2, 3, 4]\n; ... .\n?- ', 'stdout');
781
+ assertEqual(result.stdout,
782
+ '?- error(existence_error(procedure, append / 3), eyeprolog).\n' +
783
+ '?- true.\n' +
784
+ '?- X = [], Y = [1, 2, 3, 4]\n; ... .\n?- ',
785
+ 'stdout');
727
786
  assertEqual(result.stderr, '', 'stderr');
728
787
  },
729
788
  },
@@ -1120,6 +1179,19 @@ c4 ?- call((!;1)).
1120
1179
  assertEqual(infix.arity, 2, 'quad query arity');
1121
1180
  },
1122
1181
  },
1182
+ {
1183
+ name: 'normal EyeProlog uses the ISO unknown=error default',
1184
+ run: () => {
1185
+ const program = Program.parse('');
1186
+ const solver = new Solver(program, { registry: getEyePrologRegistry() });
1187
+ assertEqual(solver.prologFlags.get('unknown')?.value?.name, 'error', 'normal unknown default');
1188
+ assertEqual(
1189
+ run('', { goal: 'current_prolog_flag(unknown, V)' }).stdout,
1190
+ 'current_prolog_flag(unknown, error).\n',
1191
+ 'public runner unknown default',
1192
+ );
1193
+ },
1194
+ },
1123
1195
  {
1124
1196
  name: 'strict ISO core mode exposes only the Part 1 registry and flag surface',
1125
1197
  run: () => {
@@ -1265,6 +1337,16 @@ c4 ?- call((!;1)).
1265
1337
  "answer('\\a').\n",
1266
1338
  'read/1 user_input numeric escape',
1267
1339
  );
1340
+
1341
+ const invalidOctal = String.fromCharCode(39, 92, 56, 92, 39, 46);
1342
+ assertEqual(
1343
+ run('answer(T) :- catch(read(T), E, T=E).\n', {
1344
+ goal: 'answer(T)',
1345
+ ioOptions: { input: invalidOctal },
1346
+ }).stdout,
1347
+ 'answer(error(syntax_error(read_term), eyeprolog)).\n',
1348
+ 'read/1 rejects non-octal numeric escape',
1349
+ );
1268
1350
  },
1269
1351
  },
1270
1352
  {
@@ -5279,7 +5279,10 @@ message("café").
5279
5279
  Inside a quoted atom, a single quote is doubled: `'don''t'`. Quoted characters
5280
5280
  support the ISO symbolic control escapes such as `\a`, `\n`, and `\t`, and numeric
5281
5281
  octal or hexadecimal escapes are terminated by a backslash; for example, `'\7\'`
5282
- and `'\x7\'` both denote the alert character. Double-quoted lists use the same
5282
+ and `'\x7\'` both denote the alert character. The NUL character, when present in
5283
+ the processor character set, is written readably as `'\0\'`; digits `8` and `9`
5284
+ are not octal digits, so forms such as `'\8\'` are syntax errors rather than
5285
+ continuation input. Double-quoted lists use the same
5283
5286
  quoted-character escapes. Whitespace is insignificant
5284
5287
  between tokens, and a `%` comment continues to the end of its line. Doubling
5285
5288
  the active delimiter is also accepted inside either quoted form, so `""`
@@ -5667,18 +5670,23 @@ silently changing a static program.
5667
5670
  | `max_integer` | `unbounded` | `unbounded` | no |
5668
5671
  | `min_integer` | `unbounded` | `unbounded` | no |
5669
5672
  | `max_arity` | `unbounded` | `unbounded` | no |
5670
- | `unknown` | `fail` | `error`, `fail`, `warning` | yes |
5673
+ | `unknown` | `error` | `error`, `fail`, `warning` | yes |
5671
5674
  | `double_quotes` | `chars` | `chars`, `codes`, `atom` | yes |
5672
5675
  | `occurs_check` | `true` | `true`, `error` | yes |
5673
5676
 
5674
- Strict ISO core mode defaults `unknown` to `error`; the normal EyeProlog
5675
- environment defaults it to `fail`. The `occurs_check` flag is an EyeProlog
5676
- diagnostic extension rather than an ISO-defined core flag: it is absent in
5677
- strict mode, while normal mode keeps its `true` default and optional `error`
5678
- diagnostic for STO attempts. Operator and flag directives are processed per
5679
- program rather than changing global JavaScript state. The `double_quotes`
5680
- setting affects subsequent source text, included files, command-line and API
5681
- goal text, and terms read by `read_term/*`:
5677
+ Both normal EyeProlog and strict ISO core mode use the ISO `unknown=error`
5678
+ default. Interactive `set_prolog_flag/2` changes are retained when the REPL
5679
+ consults another file or imports a module instead of being reset by the host
5680
+ rebuild of the program. Programs that intentionally treat an undefined
5681
+ predicate as failure must opt in with `set_prolog_flag(unknown, fail)`; bundled
5682
+ examples and non-ISO corpus cases that depend on that policy do so explicitly.
5683
+ The `occurs_check` flag is an EyeProlog diagnostic
5684
+ extension rather than an ISO-defined core flag: it is absent in strict mode,
5685
+ while normal mode keeps its `true` default and optional `error` diagnostic for
5686
+ STO attempts. Operator and flag directives are processed per program rather
5687
+ than changing global JavaScript state. The `double_quotes` setting affects
5688
+ subsequent source text, included files, command-line and API goal text, and
5689
+ terms read by `read_term/*`:
5682
5690
 
5683
5691
  ```text
5684
5692
  % Default: a list of one-character atoms.
@@ -6941,7 +6949,7 @@ precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
6941
6949
  maps language families to representative executable cases.
6942
6950
 
6943
6951
  The complete suite must pass before release. The file-based conformance corpus
6944
- contains 782 cases, including 376 focused ISO
6952
+ contains 783 cases, including 377 focused ISO
6945
6953
  cases derived from the success, failure, mode, and error behavior in
6946
6954
  ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
6947
6955
  Separate exact-output suites check 189 normal