eyeprolog 1.3.30 → 1.3.32

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/README.md CHANGED
@@ -182,6 +182,8 @@ modules, DCGs, quads, libraries, proofs, and the other documented extensions.
182
182
 
183
183
  The auditable processor-requirement checklist lives in
184
184
  [`test/conformance/ISO-COMPLIANCE.md`](test/conformance/ISO-COMPLIANCE.md).
185
+ The ISO 5.4 implementation-defined/implementation-specific decision index is
186
+ [`test/conformance/ISO-IMPLEMENTATION-DEFINED.md`](test/conformance/ISO-IMPLEMENTATION-DEFINED.md).
185
187
  The separate [WG17 syntax ledger](test/conformance/WG17-SYNTAX-STATUS.md)
186
188
  records executable dispositions for the vendored active upstream WG17 syntax
187
189
  cases and runs as part of `npm test`. Reviewed cases can pin exact outcomes;
@@ -211,7 +213,7 @@ left-associative operators, so state is handed repeatedly from one nonterminal
211
213
  to the next rather than hidden in host code.
212
214
 
213
215
  Deep finite DCG traversal is kept relational but does not have to consume one
214
- general solver frame per token. In particular, the interoperable `...//0`
216
+ general solver frame per token. In particular, the interoperable `... //0`
215
217
  helper from `library(iso_ext)` can scan a finite compact list iteratively, and a
216
218
  following grammar that is statically known to leave the DCG state unchanged can
217
219
  be continued without rebuilding a full clause-resolution frame for every
@@ -242,7 +244,7 @@ headroom, so an exhausted finite heap is reported as a catchable
242
244
  `resource_error(memory)` instead of degenerating into quadratic list checks.
243
245
 
244
246
  `library(iso_ext)` is also accepted as a common interop module name.
245
- EyeProlog exports `call_nth/2`, `time/1`, and the DCG helper `...//0` there.
247
+ EyeProlog exports `call_nth/2`, `time/1`, and the DCG helper `... //0` there.
246
248
  The latter describes an arbitrary number of input elements and supports the
247
249
  nonterminal hand-off benchmark discussed in issue #49. These common predicates
248
250
  may be imported explicitly, while source/CLI/API dependency loading can resolve
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.30",
6
+ "version": "1.3.32",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -55,6 +55,10 @@
55
55
  "test:wg17": "node test/run-wg17.mjs",
56
56
  "test:examples": "node test/run-examples.mjs",
57
57
  "test:regression": "node test/run-regression.mjs",
58
+ "test:regression:api": "node test/run-regression.mjs api",
59
+ "test:regression:core": "node test/run-regression.mjs regression",
60
+ "test:regression:docs": "node test/run-regression.mjs docs",
61
+ "test:regression:white-box": "node test/run-regression.mjs white-box",
58
62
  "test:playground": "node test/run-playground.mjs",
59
63
  "wg17:upgrade": "node tools/upgrade-wg17.mjs",
60
64
  "report:wg17": "node tools/report-wg17-syntax-coverage.mjs",
package/src/cli.js CHANGED
@@ -173,7 +173,7 @@ export async function main(argv) {
173
173
 
174
174
  async function loadEngine() {
175
175
  if (engineModule == null) {
176
- const [term, parser, program, solver, iso, library, write, quads] = await Promise.all([
176
+ const [term, parser, program, solver, iso, library, write, quads, execute] = await Promise.all([
177
177
  import('./term.js'),
178
178
  import('./parser.js'),
179
179
  import('./program.js'),
@@ -182,8 +182,9 @@ async function loadEngine() {
182
182
  import('./standard-library.js'),
183
183
  import('./write.js'),
184
184
  import('./quads.js'),
185
+ import('./execute.js'),
185
186
  ]);
186
- engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write, ...quads };
187
+ engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write, ...quads, ...execute };
187
188
  }
188
189
  return engineModule;
189
190
  }
@@ -201,49 +202,16 @@ async function runDefault(engine, program, options) {
201
202
  ioOptions: { write: (text) => process.stdout.write(String(text)) },
202
203
  });
203
204
  program = solver.program;
204
- const goals = options.goals.map((text) => {
205
- const goal = engine.parseGoalText(text, {
206
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
207
- operatorDefinitions: [...program.operators.values()],
208
- isoStrict: options.isoStrict,
209
- });
210
- if (goal.type === 'var') throw new engine.PrologError('instantiation_error');
211
- if (goal.type !== 'atom' && goal.type !== 'compound') throw new engine.PrologError('type_error(callable)', goal);
212
- return goal;
213
- });
214
- const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
215
- const writeOptions = {
216
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
217
- operators: [...program.operators.values()],
218
- quoted: true,
219
- };
220
- const facts = program.sourceFactLines(queriedKeys, writeOptions);
221
- const lines = new Set();
205
+ const goals = engine.normalizeGoals(options.goals, solver);
222
206
  const explanation = options.proof ? await loadExplanation() : null;
223
207
  try {
224
- solver.runInitializations();
225
- for (const goal of goals) {
226
- solver.solutionsSeen = 0;
227
- for (const env of solver.solve([goal], new engine.Env(), 0)) {
228
- if (!engine.termIsGround(goal, env)) continue;
229
-
230
- const currentWriteOptions = {
231
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
232
- operators: [...program.operators.values()],
233
- quoted: true,
234
- };
235
- const line = `${engine.formatTermForWrite(goal, env, currentWriteOptions)}.\n`;
236
- if (facts.has(line) || lines.has(line)) continue;
237
-
238
- lines.add(line);
239
-
208
+ const { haltCode } = engine.executeGoals(program, solver, goals, {
209
+ onAnswer: (line, resolved) => {
240
210
  process.stdout.write(line);
241
- if (options.proof) writeExplanation(explanation, program, engine.copyResolved(goal, env), registry);
242
- }
243
- }
244
- } catch (error) {
245
- if (error?.name !== 'HaltSignal') throw error;
246
- process.exitCode = error.code;
211
+ if (options.proof) writeExplanation(explanation, program, resolved, registry);
212
+ },
213
+ });
214
+ if (haltCode != null) process.exitCode = haltCode;
247
215
  } finally {
248
216
  if (options.stats) printStats(solver.stats);
249
217
  }
package/src/execute.js ADDED
@@ -0,0 +1,56 @@
1
+ // Shared goal preparation and execution for the CLI and embedding API.
2
+ import { ATOM, COMPOUND, VAR, Env, copyResolved, termIsGround } from './term.js';
3
+ import { parseGoalText } from './parser.js';
4
+ import { HaltSignal, PrologError } from './iso.js';
5
+ import { formatTermForWrite } from './write.js';
6
+
7
+ export function normalizeGoals(requestedGoals, solver) {
8
+ return requestedGoals.map((requestedGoal) => {
9
+ const goal = typeof requestedGoal === 'string'
10
+ ? parseGoalText(requestedGoal, {
11
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
12
+ operatorDefinitions: [...solver.program.operators.values()],
13
+ isoStrict: solver.isoStrict,
14
+ })
15
+ : requestedGoal;
16
+ if (goal.type === VAR) throw new PrologError('instantiation_error');
17
+ if (goal.type !== ATOM && goal.type !== COMPOUND) throw new PrologError('type_error(callable)', goal);
18
+ return goal;
19
+ });
20
+ }
21
+
22
+ export function executeGoals(program, solver, goals, { onAnswer = () => {} } = {}) {
23
+ const initialWriteOptions = currentWriteOptions(program, solver);
24
+ const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
25
+ const facts = program.sourceFactLines(queriedKeys, initialWriteOptions);
26
+ const seen = new Set();
27
+ let haltCode = null;
28
+
29
+ try {
30
+ solver.runInitializations();
31
+ for (const goal of goals) {
32
+ solver.solutionsSeen = 0;
33
+ for (const env of solver.solve([goal], new Env(), 0)) {
34
+ if (!termIsGround(goal, env)) continue;
35
+ const resolved = copyResolved(goal, env);
36
+ const line = `${formatTermForWrite(resolved, new Env(), currentWriteOptions(program, solver))}.\n`;
37
+ if (facts.has(line) || seen.has(line)) continue;
38
+ seen.add(line);
39
+ onAnswer(line, resolved);
40
+ }
41
+ }
42
+ } catch (error) {
43
+ if (!(error instanceof HaltSignal)) throw error;
44
+ haltCode = error.code;
45
+ }
46
+
47
+ return { haltCode };
48
+ }
49
+
50
+ function currentWriteOptions(program, solver) {
51
+ return {
52
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
53
+ operators: [...program.operators.values()],
54
+ quoted: true,
55
+ };
56
+ }
package/src/index.js CHANGED
@@ -27,14 +27,12 @@ export {
27
27
  export { StreamManager } from './io.js';
28
28
  export { runQuads } from './quads.js';
29
29
 
30
- import { ATOM, COMPOUND, VAR, Env, copyResolved, termIsGround } from './term.js';
31
30
  import { Program, autoloadProgramGoals } from './program.js';
32
31
  import { Solver } from './solver.js';
33
32
  import { whyNoProof, whyProof } from './explain.js';
34
- import { HaltSignal, PrologError, getStrictIsoRegistry } from './iso.js';
33
+ import { getStrictIsoRegistry } from './iso.js';
35
34
  import { getEyePrologRegistry } from './standard-library.js';
36
- import { parseGoalText } from './parser.js';
37
- import { formatTermForWrite } from './write.js';
35
+ import { executeGoals, normalizeGoals } from './execute.js';
38
36
 
39
37
  export function run(source, options = {}) {
40
38
  const includeWhy = options.proof === true || options.why === true || options.explain === true;
@@ -67,56 +65,14 @@ export function run(source, options = {}) {
67
65
  },
68
66
  });
69
67
  program = solver.program;
70
- const goals = normalizeGoals(options, solver);
71
- const writeOptions = {
72
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
73
- operators: [...program.operators.values()],
74
- quoted: true,
75
- };
76
- const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
77
- const facts = program.sourceFactLines(queriedKeys, writeOptions);
78
- const seen = new Set();
79
- let haltCode = null;
80
- try {
81
- solver.runInitializations();
82
- for (const goal of goals) {
83
- solver.solutionsSeen = 0;
84
- for (const env of solver.solve([goal], new Env(), 0)) {
85
- const resolved = copyResolved(goal, env);
86
- if (!termIsGround(resolved)) continue;
87
- const currentWriteOptions = {
88
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
89
- operators: [...program.operators.values()],
90
- quoted: true,
91
- };
92
- const line = `${formatTermForWrite(resolved, new Env(), currentWriteOptions)}.\n`;
93
- if (facts.has(line) || seen.has(line)) continue;
94
- seen.add(line);
95
- output.push(line);
96
- if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
97
- }
98
- }
99
- } catch (error) {
100
- if (!(error instanceof HaltSignal)) throw error;
101
- haltCode = error.code;
102
- }
103
- return { stdout: output.join(''), stats: solver.stats, haltCode };
104
- }
105
-
106
- function normalizeGoals(options, solver) {
107
- const requested = options.goals ?? (options.goal == null ? [] : [options.goal]);
108
- return requested.map((requestedGoal) => {
109
- const goal = typeof requestedGoal === 'string'
110
- ? parseGoalText(requestedGoal, {
111
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
112
- operatorDefinitions: [...solver.program.operators.values()],
113
- isoStrict: solver.isoStrict,
114
- })
115
- : requestedGoal;
116
- if (goal.type === VAR) throw new PrologError('instantiation_error');
117
- if (goal.type !== ATOM && goal.type !== COMPOUND) throw new PrologError('type_error(callable)', goal);
118
- return goal;
68
+ const goals = normalizeGoals(requestedGoals, solver);
69
+ const { haltCode } = executeGoals(program, solver, goals, {
70
+ onAnswer: (line, resolved) => {
71
+ output.push(line);
72
+ if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
73
+ },
119
74
  });
75
+ return { stdout: output.join(''), stats: solver.stats, haltCode };
120
76
  }
121
77
 
122
78
  function appendExplanation(output, program, resolved, registry) {
package/src/solver.js CHANGED
@@ -1333,7 +1333,7 @@ function bundledEllipsisPlan(solver, group, goal, rest, env) {
1333
1333
  }
1334
1334
 
1335
1335
  // length/2 and other native constructors can leave a known finite list as a
1336
- // compact spine. The ordinary ...//0 relation simply enumerates every
1336
+ // compact spine. The ordinary ... //0 relation simply enumerates every
1337
1337
  // suffix of such a list; doing that directly avoids clause freshening and
1338
1338
  // recursive solver depth for every consumed element. Non-compact and open
1339
1339
  // list cases retain the ordinary Prolog definition.
@@ -118,7 +118,7 @@ export const eyePrologInteropAutoload = Object.freeze({
118
118
  // allowing Trealla-style unqualified source to use the same autoload entry.
119
119
  'call_nth/2': 'iso_ext',
120
120
  // Trealla exposes time/1 as a meta timing predicate and library(iso_ext)
121
- // supplies ...//0. Autoload both so UWN's DCG hand-off benchmark runs
121
+ // supplies ... //0. Autoload both so UWN's DCG hand-off benchmark runs
122
122
  // unchanged while their implementations remain outside the ISO core.
123
123
  'time/1': 'iso_ext',
124
124
  '.../2': 'iso_ext',
@@ -21,9 +21,9 @@ error-ordering alternative to an individual executable assertion.
21
21
  | 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the [complete vendored WG17 syntax matrix](WG17-SYNTAX-STATUS.md). Wider shall-by-shall text-processing audit remains open. |
22
22
  | 5.1(b) execute conforming Prolog goals | audit | Clause 7-9 conformance corpus plus regression/API/example gates. A normative goal-semantics ledger is still being expanded. |
23
23
  | 5.1(c) reject nonconforming text/read-terms | audit | Dedicated syntax-error cases and strict-core extension rejection. Exhaustive lexical rejection coverage remains open. |
24
- | 5.1(d) document permitted variations | audit | Major implementation-defined choices are documented in *The Art of EyeProlog*. Every occurrence of implementation defined/dependent/specific in Part 1 still needs a final documentation cross-check. |
24
+ | 5.1(d) document permitted variations | covered | The clause-by-clause [ISO 5.4 decision index](ISO-IMPLEMENTATION-DEFINED.md) records every explicit implementation-defined decision found in the Part 1 + Corrigenda baseline and separately inventories implementation-specific extension families. Rows marked `audit gap` remain conformance work, but the variation is no longer undocumented. |
25
25
  | 5.1(e) offer a strictly conforming mode | covered | `--iso-strict` and API option `isoStrict: true` restrict the processor to the Part 1 + Corrigenda 1-3 core language surface, remove EyeProlog-only registry/flag/operator features, and disable automatic tabling/recursion guards. |
26
- | 5.4 accompanying documentation | audit | The book is the implementation reference. The implementation-defined-feature inventory is not yet a closed checklist. |
26
+ | 5.4 accompanying documentation | covered | *The Art of EyeProlog* remains the implementation reference; [ISO-IMPLEMENTATION-DEFINED.md](ISO-IMPLEMENTATION-DEFINED.md) is the closed clause-by-clause 5.4 decision index and points each decision to implementation evidence or an explicit audit gap. |
27
27
  | 5.5 extensions preserve standard text | covered | Default mode retains EyeProlog extensions; strict core mode removes their language/runtime interpretation. Regression tests ensure the default profile remains unchanged. |
28
28
 
29
29
  ## Normative language families
@@ -95,6 +95,6 @@ ISO/IEC 13211-1 processor” until all of the following are true:
95
95
  difference explained or fixed;
96
96
  4. prescribed modes, errors, side effects, and relevant error precedence for
97
97
  every Part 1 built-in have executable coverage;
98
- 5. every implementation-defined/dependent/specific choice required to be
99
- documented is linked to the implementation reference; and
98
+ 5. every `audit gap` recorded in the ISO 5.4 decision index is either fixed or
99
+ explicitly excluded from the strict-conformance claim; and
100
100
  6. an external conformance run has found no unexplained deviations.
@@ -0,0 +1,146 @@
1
+ # ISO/IEC 13211-1 implementation-defined and implementation-specific profile
2
+
3
+ This is the clause-by-clause ISO 5.4 decision index for EyeProlog. The
4
+ normative baseline used for this table is **ISO/IEC 13211-1:1995** together
5
+ with **Technical Corrigenda 1:2007, 2:2012, and 3:2017**. The current WG17/STC
6
+ pages at complang.tuwien.ac.at are useful review input, but draft proposals are
7
+ not silently treated as normative changes to that licensed baseline.
8
+
9
+ [*The Art of EyeProlog*](../../the-art-of-eyeprolog.md) remains the single
10
+ implementation reference. This file is an audit index: it identifies each
11
+ explicitly implementation-defined decision in Part 1, states the EyeProlog
12
+ choice, and points to the implementation boundary that realizes it. Repeated
13
+ references to the same decision are folded into one row. Requirements that the
14
+ standard calls **implementation dependent** are listed separately because ISO
15
+ 5.4 does not require their documentation in the same way.
16
+
17
+ Status values are:
18
+
19
+ - **defined** — the current behavior is implemented and stated here;
20
+ - **not applicable** — the standard decision is conditional and the condition
21
+ is false for EyeProlog's selected profile;
22
+ - **audit gap** — the current code behavior is stated, but strict-mode
23
+ conformance still needs a correction or a narrower profile before this row
24
+ can support a full conformance claim.
25
+
26
+ ## Explicit implementation-defined decisions
27
+
28
+ | Clause | Decision completed by ISO 5.4 documentation | EyeProlog choice | Status / implementation evidence |
29
+ | --- | --- | --- | --- |
30
+ | 5.5.11 | Reserved atoms and the effect of instantiating a variable to one | EyeProlog reserves no Prolog atom under 5.5.11. Atoms with implementation-looking names remain ordinary terms unless a particular predicate interprets them. | **defined** — term representation and built-ins in `src/term.js`, `src/iso.js`. |
31
+ | 6.5 | Processor character set (PCS) | The unquoted ISO lexical classes are the Part 1 ASCII characters implemented by `src/parser.js`. Quoted character data additionally accepts Unicode scalar values. | **audit gap** — the Unicode quoted-character extension is currently also accepted by `--iso-strict`; strict extension rejection still needs a narrower PCS rule or a documented conforming classification. |
32
+ | 6.5 | Classification of additional/extended PCS characters | Non-ASCII scalar values are not accepted as unquoted small-letter, capital-letter, graphic, solo, layout, or meta characters; they are accepted only inside quoted character data. | **audit gap** — same strict-mode boundary as the preceding row. |
33
+ | 6.6 | Collating-sequence integers | Character codes are Unicode scalar values. Atom comparison uses ECMAScript string lexicographic order; on the ISO ASCII repertoire this is code-point order and satisfies the required monotonic ranges. | **defined** — `src/term.js` (`compareTerms`), `src/iso.js` character-code predicates. |
34
+ | 6.6 | Collating values of control escapes and extended characters | Control escapes and character-code predicates use Unicode scalar values. Atom ordering is ECMAScript string order; for non-BMP one-char atoms that order is based on UTF-16 code units rather than scalar values. | **audit gap** — the ISO ASCII repertoire is conforming, but the extended-character collating rule still needs one coherent documented integer/order mapping in strict mode. |
35
+ | 7.1.2.2 | Mapping between a character code and bytes | Text file streams decode and encode UTF-8. Binary streams expose bytes 0..255 directly. | **defined** — `src/io.js`. |
36
+ | 7.1.4.1 | Set `C` of characters represented by one-char atoms | Character predicates accept Unicode scalar values U+0000..U+10FFFF excluding surrogate code points. | **defined** — `src/iso.js` character-code validation. |
37
+ | 7.4.2.4 | Whether `op/3` directives affect other Prolog texts or execution | An `op/3` directive changes parsing of subsequent text loaded into the same `Program`; the resulting operator table is also used by execution-time term I/O. Separately created `Program` objects are independent. | **defined** — `src/parser.js`, `src/program.js`, `src/iso.js`. |
38
+ | 7.4.2.5 | Whether directive-created `Convc` affects other text/execution | Directive mappings are copied into the solver's execution-time `charConversions` map. The source tokenizer itself does not currently apply `char_conversion/2` to later unquoted source characters. | **audit gap** — `src/program.js`, `src/solver.js`; source-preparation conversion remains to be aligned with 7.11.2.1. |
39
+ | 7.4.2.6 | Order of `initialization/1` goals | Initialization goals run once, in source/inclusion order, before requested goals; each must obtain a first solution. | **defined** — `Program.initializations`, `Solver.runInitializations()`. |
40
+ | 7.4.2.7 | Ground term designating a Prolog text for `include/1` | A source designation is an atom interpreted as a file path; relative paths resolve from the including file's directory (or the current working directory for unanchored input). | **defined** — `src/program.js` (`readIncludedSource`). |
41
+ | 7.4.2.8 | Ground term designating a Prolog text for `ensure_loaded/1` | Same atom/file-path designation as `include/1`. | **defined** — `src/program.js`. |
42
+ | 7.4.2.8 | Position at which an ensured text is included | The first `ensure_loaded/1` expands the text in place at the directive; later requests for the same resolved path are ignored. | **defined** — `src/program.js` (`ensured` set and in-place builder loading). |
43
+ | 7.4.2.9 | Whether `set_prolog_flag/2` directives affect other texts/execution | Parser-relevant `double_quotes` changes following source text in the same parse; recorded flag directives initialize the solver used for execution. Separately created programs/solvers are independent. | **defined** — `src/parser.js`, `src/program.js`, `src/solver.js`. |
44
+ | 7.5.1 | Means by which the processor is asked to prepare Prolog text | Text is prepared through the CLI/REPL, the JavaScript `Program.parse`/`run` API, source files/URLs, and `include/1`/`ensure_loaded/1`. | **defined** — Chapter 40 and `src/cli.js`, `src/index.js`, `src/program.js`. |
45
+ | 7.5.1 | Effects of directive-driven reordering, addition, or removal of clauses during preparation | Includes are expanded in place; declarations annotate the affected procedure; clauses otherwise retain source order. No directive silently reorders ordinary clauses. | **defined** — streaming builder in `src/program.js`. |
46
+ | 7.7.1 | External form of a negative answer | The interactive top level prints `false.`. Non-interactive `run()`/CLI corpus execution emits no answer line for a failed goal. | **defined** — Chapter 40, `src/repl.js`, `src/execute.js`. |
47
+ | 7.7.1 | External form of a positive answer | The REPL prints `true.` for a solution without visible bindings or `Name = Term` bindings; the file/API runner emits each resolved ground goal as a Prolog term followed by `.`. | **defined** — Chapter 40, `src/repl.js`, `src/execute.js`. |
48
+ | 7.7.3 | Method by which a user delivers a goal | REPL queries, CLI `--goal`, `%% goal:` host comments, and the JavaScript `goal` option are supported host interfaces. | **defined** — Chapter 40. |
49
+ | 7.10.1 | Additional source/sink possibilities | Node runtimes support local files plus the predefined standard streams; embedding supplies `user_input` text and a `user_output` callback. URL loading is a source-loading facility, not an ISO stream opened by `open/4`. | **defined** — `src/io.js`, `src/cli.js`, `src/index.js`. |
50
+ | 7.10.1 | Ground term designating a source/sink to `open/4` | An atom is interpreted as a local file-system path. | **defined** — `src/iso.js`, `src/io.js`. |
51
+ | 7.10.2.6 | Record-based/non-record-based text stream support | EyeProlog exposes non-record-based text streams. | **defined** — `src/io.js`. |
52
+ | 7.10.2.6 | Alteration of spaces at line ends | None. EyeProlog preserves text-stream characters; it does not trim or pad line endings. | **defined** — `src/io.js`. |
53
+ | 7.10.2.6 | Whether the last line is followed by newline / close adds one | Closing a text output stream writes exactly the accumulated content and does not synthesize a final newline. | **defined** — `StreamManager.close()`. |
54
+ | 7.10.2.6 | Effect of outputting control characters | The Unicode character is appended unchanged to a text stream (subject to the host sink's ordinary string handling). | **defined** — `StreamManager.writeUnit()`. |
55
+ | 7.10.2.7 | Number of zero bytes that may be appended on binary re-input | Zero. Binary file output is written and read byte-for-byte. | **defined** — `src/io.js`. |
56
+ | 7.10.2.8 | Whether a source/sink can be arbitrarily repositioned | File streams are repositionable only when opened with `reposition(true)`; standard streams are not repositionable. | **defined** — `src/io.js`, `set_stream_position/2` in `src/iso.js`. |
57
+ | 7.10.2.9 | Terms denoting end/past-end stream positions when repositionable | Stream positions are non-negative integer offsets; `position(N)` is also accepted by `set_stream_position/2`. End/past-end state is separately exposed by `end_of_stream/1`. | **defined** — `streamProperties()` and `setStreamPositionBuiltin()` in `src/iso.js`. |
58
+ | 7.10.2.11 | Default `eof_action` | Newly opened file streams default to `error`. The predefined standard streams use `reset`. | **defined** — `src/io.js`. |
59
+ | 7.10.2.13 | `eof_action(Action)` property when the stream uses its default action | Reports the same selected action: `error` for ordinary opened files and `reset` for the predefined standard streams. | **defined** — `streamProperties()` in `src/iso.js`, defaults in `src/io.js`. |
60
+ | 7.10.2.11 | Whether `reposition(false)` streams may nevertheless be repositioned | No. `set_stream_position/2` raises a permission error unless the stream was created with `reposition(true)`. | **defined** — `src/iso.js`. |
61
+ | 7.11.1.1 | Default `bounded` flag | `false`: EyeProlog's Prolog integer model uses arbitrary-precision `BigInt`, subject to host resources. | **defined** — `defaultPrologFlags()` in `src/solver.js`. |
62
+ | 7.11.1.2 | Default `max_integer` when `bounded=true` | The bounded-profile default is not applicable because EyeProlog selects `bounded=false`. However, EyeProlog currently exposes the sentinel atom `unbounded` through `current_prolog_flag/2` instead of making the `max_integer` query fail as 7.11.1.1 prescribes for an unbounded processor. | **audit gap** — `src/solver.js`, `current_prolog_flag/2`. |
63
+ | 7.11.1.3 | Default `min_integer` when `bounded=true` | The bounded-profile default is not applicable because EyeProlog selects `bounded=false`. However, EyeProlog currently exposes the sentinel atom `unbounded` through `current_prolog_flag/2` instead of making the `min_integer` query fail as 7.11.1.1 prescribes for an unbounded processor. | **audit gap** — `src/solver.js`, `current_prolog_flag/2`. |
64
+ | 7.11.1.4 | Default `integer_rounding_function` flag | `toward_zero`; `//` uses truncation toward zero. `div` is provided separately with downward/floor division semantics. | **defined** — `src/solver.js`, `src/iso.js`. |
65
+ | 9.1.3.1 | Integer division rounding function `rndI` | Truncation toward zero, matching the `integer_rounding_function=toward_zero` flag. | **defined** — BigInt division for `//` in `src/iso.js`. |
66
+ | 7.11.2.1 | Whether preparation-time `Convc` affects execution-time `Convc` | Yes: mappings recorded by `char_conversion/2` directives initialize the solver map. Source-token conversion itself has the audit gap noted at 7.4.2.5. | **defined / audit note** — `src/solver.js`. |
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
+ | 7.11.2.3 | Default `max_arity` | `unbounded` in the Prolog model, subject to host memory and practical JavaScript array/index limits. | **defined** — `src/solver.js`; relevant guards report representation/resource errors. |
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`. |
71
+ | 7.12.2(f) | Implementation-defined representation limits | Character and character-code operations use Unicode scalar limits; 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
+ | 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` flag; `max_integer`/`min_integer` use the `unbounded` sentinel described above. | **defined** — strict registry/flag filtering in `src/solver.js`. |
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. |
74
+ | 8.17.4 | Meaning/effects of `halt(Status)` | Integer `Status` is converted to the host process/runner halt code; it produces no Prolog solution. | **defined** — `haltBuiltin()`, `src/execute.js`, `src/cli.js`. |
75
+ | 9.1.4.1 | Floating-point rounding function `rndF` | Floating values and operations use ECMAScript `Number` (IEEE-754 binary64) and the host's specified binary64 arithmetic/conversions. | **defined** — `src/iso.js`, `src/number-value.js`. |
76
+ | 9.1.4.2 | Floating-point result function, including tiny non-zero arithmetic results | For arithmetic operation results that underflow to zero from non-zero operands, EyeProlog chooses the exceptional value `underflow`, exposed as `evaluation_error(underflow)`. Float token/`number_chars/2` input is a separate conversion path and finite input underflow rounds to `0.0`. | **defined** — `evaluateOperation()` and parser/number conversion; executable issue-56 regression below. |
77
+ | 9.1.4.3 | Approximate-addition function | ECMAScript binary64 addition is used; subtraction is implemented through the corresponding host operation and all finite results remain binary64 values. | **defined** — `evaluateOperation()` in `src/iso.js`. |
78
+ | 9.4 | Representation of negative integers for bitwise operations | BigInt's unbounded signed binary semantics are used, equivalent to an infinite two's-complement sign extension for bitwise operations. | **defined** — `src/iso.js` BigInt bitwise operators. |
79
+ | 9.4.1 | Right shift of negative integers and unusual shift counts | `>>` is arithmetic/sign-propagating. A negative count reverses direction according to JavaScript BigInt shift semantics; there is no finite integer bit-size ceiling in the Prolog model. | **defined** — `a >> b` in `src/iso.js`. |
80
+ | 9.4.2 | Left shift of negative integers and unusual shift counts | `<<` uses BigInt signed shift semantics; a negative count reverses direction. Resource exhaustion remains possible for very large results. | **defined** — `a << b` in `src/iso.js`. |
81
+ | 9.4.3 | Bitwise AND with negative operands | BigInt infinite-two's-complement semantics. | **defined** — `a & b`. |
82
+ | 9.4.4 | Bitwise OR with negative operands | BigInt infinite-two's-complement semantics. | **defined** — `a \| b`. |
83
+ | 9.4.5 | Bitwise complement | BigInt complement, i.e. `~N = -N-1`. | **defined** — `~a`. |
84
+ | Cor.2 9.4.6 | `xor/2` with negative operands | BigInt infinite-two's-complement semantics. | **defined** — `a ^ b`. |
85
+
86
+ ### Why issue #56's two float results differ
87
+
88
+ The arithmetic expression and the float token go through different specified
89
+ layers. Clause 9.1.4 defines arithmetic operations in terms of the
90
+ implementation-defined floating rounding/result functions, and EyeProlog's
91
+ chosen `resultF` policy reports underflow for a non-zero arithmetic result that
92
+ falls below the normal range and rounds to zero. By contrast, reading the token
93
+ `0.1e-999` is an input conversion; the 1995 text does not precisely define the
94
+ rounding of an inexact float token, a gap also called out in the later
95
+ [WG17/STC item #40](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/stc#40).
96
+ EyeProlog's documented input policy is therefore to accept finite token
97
+ underflow as `0.0` while arithmetic underflow remains an evaluation error.
98
+
99
+ The regression suite contains an issue-56 case that checks both observations,
100
+ and the existing `stc/float_underflow_input` conformance case covers the related
101
+ input-conversion policy.
102
+
103
+ ## Implementation-specific features required to be documented by 5.4
104
+
105
+ Part 1 clause 5.5 permits extensions only as implementation-specific features.
106
+ Corrigendum 3 additionally makes extra option names explicit implementation-
107
+ specific features. EyeProlog's normal profile provides the following extension
108
+ families; `--iso-strict` is intended to remove their Part 1 interpretation.
109
+
110
+ | Part 1 extension hook | EyeProlog normal-profile feature | Strict-core disposition |
111
+ | --- | --- | --- |
112
+ | 5.5.1 Syntax | Part 2 modules, Part 3 grammar-rule expansion, and embedded quad syntax | Module directives are rejected; grammar rules remain ordinary `-->/2` terms rather than being expanded; quad syntax is rejected. Unicode quoted-character handling is the open PCS audit item above. |
113
+ | 5.5.2 Predefined operators | Part 3 `|` and EyeProlog's labelable infix `(?-)/2`; CLP(Z) operators when that library is imported | Only the Part 1 operator table is predefined; a conforming `op/3` may still add permitted operators. |
114
+ | 5.5.3 Character-conversion mapping | No non-identity initial `Convc` extension | Identity initial mapping. |
115
+ | 5.5.4 Types | No additional runtime Prolog term type is exposed by the core solver | Only variable, integer, float, atom, and compound term ordering participates in strict mode. |
116
+ | 5.5.5 Directives | `module/2`, `use_module/1-2`, `meta_predicate/1` and normal-profile library behavior | Rejected as implementation-specific Part 1 directives. |
117
+ | 5.5.6 Side effects | Host output/statistics, library-specific state, proof/statistics collection | The strict registry excludes library adapters; ordinary Part 1 I/O/database/flag/operator side effects remain. |
118
+ | 5.5.7 Control constructs | `tnot/1` and normal-profile execution optimizations | `tnot/1` is absent; automatic tabling/recursion guards are disabled. |
119
+ | 5.5.8 Flags | `occurs_check` | Absent in strict mode. |
120
+ | 5.5.9 Built-in predicates | EyeProlog libraries, CLP(Z), statistics, Part 3 `phrase/2-3`, and interop autoloaded predicates | Strict registry contains only the Part 1 + Corrigenda core registry. |
121
+ | 5.5.10 Evaluable functors | Library/runtime extensions when registered outside the strict core | Strict arithmetic registry is the Part 1 + Corrigenda set. |
122
+ | 5.5.11 Reserved atoms | None | None. |
123
+ | Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Strict core accepts the standard option names implemented for Part 1; invalid options follow Corrigendum 3 validation. |
124
+
125
+ Part 2 and Part 3 are separately standardized profiles when EyeProlog is run in
126
+ its normal mode; the table above describes them as extensions only relative to
127
+ the Part 1 strict-core boundary.
128
+
129
+ ## Important implementation-dependent behavior (not the 5.4 mandatory table)
130
+
131
+ For portability work, EyeProlog also documents these choices even though the
132
+ 1995 text labels them implementation **dependent**, not implementation defined:
133
+
134
+ - distinct variables are ordered by first encounter within the operation that
135
+ needs a stable ordering;
136
+ - expression arguments are evaluated left-to-right by the JavaScript evaluator;
137
+ - generated variable names use EyeProlog's stable `_A`, `_B`, ... style within
138
+ a write operation/top-level query;
139
+ - resource and syntax-error detail atoms are EyeProlog implementation details;
140
+ - the order of `current_prolog_flag/2` solutions is the insertion order of the
141
+ flag registry.
142
+
143
+ These rows are intentionally separate so that the ISO 5.4 checklist does not
144
+ blur the standard's distinction between *implementation defined* (documented),
145
+ *implementation dependent* (need not be documented), and *implementation
146
+ specific* (an extension that must be documented for a conforming processor).
@@ -28,12 +28,13 @@ uninstantiation errors, and corrected `catch/3` behavior (Cor.2); and option
28
28
  validation, variable-name traversal/output, canonical list output, and negative
29
29
  integer powers (Cor.3).
30
30
 
31
- Implementation-defined choices are documented in *The Art of EyeProlog*:
32
- integers and arity are unbounded by the Prolog model (subject to host memory),
33
- ordinary unification performs an occurs check, `double_quotes` defaults to
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.
31
+ Implementation-defined and implementation-specific choices are indexed
32
+ clause-by-clause in [ISO-IMPLEMENTATION-DEFINED.md](ISO-IMPLEMENTATION-DEFINED.md),
33
+ with *The Art of EyeProlog* remaining the implementation reference. In
34
+ particular, the index records the unbounded integer model, `double_quotes=chars`,
35
+ `//` rounding toward zero, the ECMAScript binary64 float policy (including the
36
+ 9.1.4.2 arithmetic-underflow choice), stream/character decisions, and the
37
+ normal-profile extension boundary.
37
38
 
38
39
  This is an executable conformance matrix, not a certification issued by an
39
40
  independent standards body. Release gating runs the ISO cases and the dedicated
package/test/run-all.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // Unified test runner used by `npm test`.
3
3
  // Running all suites in one process keeps the numbering continuous and avoids
4
4
  // npm's intermediate script banners between conformance, regression, and examples.
5
- import { TestReporter } from './test-style.mjs';
5
+ import { runStandalone } from './test-style.mjs';
6
6
  import { runConformance } from './run-conformance.mjs';
7
7
  import { runRegression } from './run-regression.mjs';
8
8
  import { runIsoStrict } from './run-iso-strict.mjs';
@@ -12,9 +12,7 @@ import { runBookExamples } from './run-book-examples.mjs';
12
12
  import { runWg17 } from './run-wg17.mjs';
13
13
  import { runOpenRuleBenchChecks } from './run-openrulebench.mjs';
14
14
 
15
- const reporter = new TestReporter();
16
-
17
- try {
15
+ await runStandalone(async (reporter) => {
18
16
  runConformance(reporter);
19
17
  runIsoStrict(reporter);
20
18
  runWg17(reporter);
@@ -23,8 +21,4 @@ try {
23
21
  await runPlayground(reporter);
24
22
  runExamples(reporter);
25
23
  runBookExamples(reporter);
26
- reporter.totalLine();
27
- process.exit(0);
28
- } catch (_) {
29
- process.exit(1);
30
- }
24
+ });
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
6
  import { Program, run } from '../src/index.js';
7
- import { TestReporter, isMainModule } from './test-style.mjs';
7
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
8
8
  import { goalsFromSource } from './goal-metadata.mjs';
9
9
 
10
10
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -39,11 +39,5 @@ function listPrograms(directory) {
39
39
  }
40
40
 
41
41
  if (isMainModule(import.meta.url)) {
42
- const reporter = new TestReporter();
43
- try {
44
- runBookExamples(reporter);
45
- reporter.totalLine();
46
- } catch (_) {
47
- process.exit(1);
48
- }
42
+ await runStandalone(runBookExamples);
49
43
  }
@@ -5,6 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { listPrologFiles } from './test-support.mjs';
8
9
 
9
10
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
10
11
  const packageRoot = path.resolve(root, '..');
@@ -24,7 +25,7 @@ export function buildConformanceReport() {
24
25
  for (const { kind, expectedKind, expectedExt, column } of KINDS) {
25
26
  const base = path.join(conformanceRoot, kind);
26
27
  if (!fs.existsSync(base)) continue;
27
- for (const file of listEyePrologFiles(base)) {
28
+ for (const file of listPrologFiles(base)) {
28
29
  const category = categoryOf(file);
29
30
  const counts = ensureCategory(categories, category);
30
31
  counts[column]++;
@@ -77,19 +78,6 @@ export function formatConformanceReport(report = buildConformanceReport()) {
77
78
  return `${lines.join('\n')}\n`;
78
79
  }
79
80
 
80
- function listEyePrologFiles(base, dir = base) {
81
- const files = [];
82
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
83
- const full = path.join(dir, entry.name);
84
- if (entry.isDirectory()) {
85
- files.push(...listEyePrologFiles(base, full));
86
- } else if (entry.isFile() && entry.name.endsWith('.pl')) {
87
- files.push(path.relative(base, full).split(path.sep).join('/'));
88
- }
89
- }
90
- return files.sort();
91
- }
92
-
93
81
  function categoryOf(file) {
94
82
  const parts = file.split('/');
95
83
  return parts.length > 1 ? parts[0] : 'legacy-numbered';
@@ -6,27 +6,12 @@ import path from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { Program, createDefaultRegistry, run } from '../src/index.js';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { TestReporter, isMainModule } from './test-style.mjs';
9
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
10
10
  import { goalsFromSource } from './goal-metadata.mjs';
11
+ import { listPrologFiles, withStandardModules } from './test-support.mjs';
11
12
 
12
13
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
13
14
  const filterArg = process.argv[2] ?? null;
14
- const libraryCall = /\b(?:uuid|difference|maplist|lt|gt|le|ge|between|smallest_divisor_from|random|matches|split|replace|lowercase|uppercase|trim|number_string|atom_string|term_string|append|string_concat|contains|join|substring|member|select|last|nth0|nth1|set_nth0|take|drop|slice|reverse|length|sum_list|min_list|max_list|list_to_set|countall|sumall|aggregate_min|aggregate_max)\s*\(/;
15
-
16
- function withStandardModules(text) {
17
- if (!libraryCall.test(text) || text.includes('use_module(library(')) return text;
18
- return `:- use_module(library(aggregate)).
19
- :- use_module(library(comparison)).
20
- :- use_module(library(dates)).
21
- :- use_module(library(iso_ext)).
22
- :- use_module(library(lists)).
23
- :- use_module(library(primes)).
24
- :- use_module(library(prologue), [between/3]).
25
- :- use_module(library(random)).
26
- :- use_module(library(strings)).
27
- :- use_module(library(uuid)).
28
- ${text}`;
29
- }
30
15
 
31
16
  export function runConformance(reporter = new TestReporter(), requestedFilter = null) {
32
17
  const filter = requestedFilter ?? filterArg;
@@ -42,7 +27,7 @@ export function runConformance(reporter = new TestReporter(), requestedFilter =
42
27
  function listCaseFiles(kind, filter = null) {
43
28
  const base = path.join(root, 'conformance', kind);
44
29
  if (!fs.existsSync(base)) return [];
45
- return listEyePrologFiles(base)
30
+ return listPrologFiles(base)
46
31
  .filter((name) => matchesFilter(kind, name, filter))
47
32
  .sort();
48
33
  }
@@ -58,19 +43,6 @@ function matchesFilter(kind, name, filter) {
58
43
  || `${label}/${stem}`.includes(filter);
59
44
  }
60
45
 
61
- function listEyePrologFiles(base, dir = base) {
62
- const files = [];
63
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
64
- const full = path.join(dir, entry.name);
65
- if (entry.isDirectory()) {
66
- files.push(...listEyePrologFiles(base, full));
67
- } else if (entry.isFile() && entry.name.endsWith('.pl')) {
68
- files.push(path.relative(base, full).split(path.sep).join('/'));
69
- }
70
- }
71
- return files;
72
- }
73
-
74
46
  function runCaseFile(reporter, file) {
75
47
  const name = file.slice(0, -3);
76
48
  reporter.test(name, () => runCase(name, file));
@@ -194,11 +166,5 @@ function diffText(expected, actualText) {
194
166
  }
195
167
 
196
168
  if (isMainModule(import.meta.url)) {
197
- const reporter = new TestReporter();
198
- try {
199
- runConformance(reporter);
200
- reporter.totalLine();
201
- } catch (_) {
202
- process.exit(1);
203
- }
169
+ await runStandalone(runConformance);
204
170
  }
@@ -6,7 +6,7 @@ import path from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { Program, run } from '../src/index.js';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { TestReporter, isMainModule } from './test-style.mjs';
9
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
10
10
  import { goalsInProgramOrder } from './goal-metadata.mjs';
11
11
 
12
12
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
@@ -156,11 +156,5 @@ function diffText(expected, actualText) {
156
156
  }
157
157
 
158
158
  if (isMainModule(import.meta.url)) {
159
- const reporter = new TestReporter();
160
- try {
161
- runExamples(reporter);
162
- reporter.totalLine();
163
- } catch (_) {
164
- process.exit(1);
165
- }
159
+ await runStandalone(runExamples);
166
160
  }
@@ -10,7 +10,7 @@ import {
10
10
  parseGoalText,
11
11
  run,
12
12
  } from '../src/index.js';
13
- import { TestReporter, isMainModule } from './test-style.mjs';
13
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
14
14
 
15
15
  export function runIsoStrict(reporter = new TestReporter()) {
16
16
  reporter.section('Strict ISO core');
@@ -103,11 +103,5 @@ function includes(actual, expected, label) {
103
103
  }
104
104
 
105
105
  if (isMainModule(import.meta.url)) {
106
- const reporter = new TestReporter();
107
- try {
108
- runIsoStrict(reporter);
109
- reporter.totalLine();
110
- } catch (_) {
111
- process.exit(1);
112
- }
106
+ await runStandalone(runIsoStrict);
113
107
  }
@@ -2,57 +2,27 @@
2
2
  // Fast structural checks for the generated multi-engine OpenRuleBench corpus.
3
3
  // Full benchmark execution remains separate because it requires external
4
4
  // Prolog implementations and is intentionally performance-oriented.
5
- import path from 'node:path';
6
- import process from 'node:process';
7
- import { spawnSync } from 'node:child_process';
8
- import { fileURLToPath } from 'node:url';
9
- import { TestReporter, isMainModule } from './test-style.mjs';
10
-
11
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
5
+ import { validateOpenRuleBench } from '../openrulebench/tools/check.mjs';
6
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
12
7
 
13
8
  export function runOpenRuleBenchChecks(reporter = new TestReporter()) {
9
+ let report = null;
14
10
  reporter.section('OpenRuleBench source integrity');
15
- runChecker(
16
- reporter,
17
- 'generated sources pass lexical checks',
18
- 'openrulebench/tools/check_sources.mjs',
19
- 'eyeprolog: 14 sources; lexical checks ok',
20
- );
21
- runChecker(
22
- reporter,
23
- 'engine variants preserve table and WFS adaptations',
24
- 'openrulebench/tools/check_multiengine.mjs',
25
- 'OK: 14 benchmarks x 4 engines; table/WFS adaptations verified.',
26
- );
11
+ reporter.test('generated sources pass lexical checks', () => {
12
+ report = validateOpenRuleBench();
13
+ assertNoErrors(report.lexicalErrors);
14
+ });
15
+ reporter.test('engine variants preserve table and WFS adaptations', () => {
16
+ report ??= validateOpenRuleBench();
17
+ assertNoErrors(report.adaptationErrors);
18
+ });
27
19
  reporter.sectionTotal('OpenRuleBench source-integrity');
28
20
  }
29
21
 
30
- function runChecker(reporter, name, relativeScript, expectedOutput) {
31
- reporter.test(name, () => {
32
- const result = spawnSync(process.execPath, [relativeScript], {
33
- cwd: packageRoot,
34
- encoding: 'utf8',
35
- timeout: 30000,
36
- });
37
- if (result.error) throw result.error;
38
- if (result.status !== 0) {
39
- throw new Error(
40
- `${relativeScript} exited with ${result.status}\n` +
41
- `${result.stdout ?? ''}${result.stderr ?? ''}`.trimEnd(),
42
- );
43
- }
44
- if (!String(result.stdout).includes(expectedOutput)) {
45
- throw new Error(`${relativeScript} did not report its expected summary\n${result.stdout ?? ''}`.trimEnd());
46
- }
47
- });
22
+ function assertNoErrors(errors) {
23
+ if (errors.length > 0) throw new Error(errors.join('\n'));
48
24
  }
49
25
 
50
26
  if (isMainModule(import.meta.url)) {
51
- const reporter = new TestReporter();
52
- try {
53
- runOpenRuleBenchChecks(reporter);
54
- reporter.totalLine();
55
- } catch (_) {
56
- process.exit(1);
57
- }
27
+ await runStandalone(runOpenRuleBenchChecks);
58
28
  }
@@ -10,7 +10,14 @@ import {
10
10
  executePlaygroundRequest,
11
11
  installPlaygroundWorker,
12
12
  } from '../src/playground-worker.js';
13
- import { TestReporter, isMainModule } from './test-style.mjs';
13
+ import {
14
+ TestReporter,
15
+ assertEqual,
16
+ assertIncludes,
17
+ assertNotIncludes,
18
+ isMainModule,
19
+ runStandalone,
20
+ } from './test-style.mjs';
14
21
 
15
22
  const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
16
23
  const packageRoot = path.resolve(testRoot, '..');
@@ -220,30 +227,6 @@ function assert(condition, message) {
220
227
  if (!condition) throw new Error(message);
221
228
  }
222
229
 
223
- function assertEqual(actual, expected, label) {
224
- if (actual !== expected) {
225
- throw new Error(`${label} mismatch\nexpected: ${JSON.stringify(expected)}\nactual: ${JSON.stringify(actual)}`);
226
- }
227
- }
228
-
229
- function assertIncludes(actual, expected, label) {
230
- if (!String(actual).includes(expected)) {
231
- throw new Error(`${label} did not include ${JSON.stringify(expected)}\nactual: ${JSON.stringify(actual)}`);
232
- }
233
- }
234
-
235
- function assertNotIncludes(actual, expected, label) {
236
- if (String(actual).includes(expected)) {
237
- throw new Error(`${label} unexpectedly included ${JSON.stringify(expected)}`);
238
- }
239
- }
240
-
241
230
  if (isMainModule(import.meta.url)) {
242
- const reporter = new TestReporter();
243
- try {
244
- await runPlayground(reporter);
245
- reporter.totalLine();
246
- } catch (_) {
247
- process.exit(1);
248
- }
231
+ await runStandalone(runPlayground);
249
232
  }
@@ -48,13 +48,21 @@ import { PrologError, formalErrorTerm } from '../src/iso.js';
48
48
  import { compareTerms } from '../src/term.js';
49
49
  import { formatTermForWrite } from '../src/write.js';
50
50
  import { selectClauseCandidates } from '../src/program.js';
51
- import { TestReporter, isMainModule } from './test-style.mjs';
51
+ import {
52
+ TestReporter,
53
+ assertEqual,
54
+ assertIncludes,
55
+ assertNotIncludes,
56
+ isMainModule,
57
+ runStandalone,
58
+ } from './test-style.mjs';
52
59
  import { buildConformanceReport, formatConformanceReport } from './run-conformance-report.mjs';
53
60
  import { proofExamples } from './run-examples.mjs';
54
61
  import { goalsFromSource } from './goal-metadata.mjs';
55
62
  import { renderWg17SyntaxStatus } from '../tools/report-wg17-syntax-coverage.mjs';
56
63
  import { parseWg17SyntaxTable } from '../tools/upgrade-wg17.mjs';
57
64
  import { executeWg17Item, matchesUpstreamExpectation, readWg17SyntaxFixture } from './run-wg17.mjs';
65
+ import { withStandardModules } from './test-support.mjs';
58
66
 
59
67
  const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
60
68
  const packageRoot = path.resolve(testRoot, '..');
@@ -62,22 +70,6 @@ const bin = path.join(packageRoot, 'bin', 'eyeprolog.js');
62
70
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
63
71
  let tmp = null;
64
72
  let tmpCounter = 0;
65
- const libraryCall = /\b(?:uuid|difference|maplist|foldl|call_nth|lt|gt|le|ge|between|smallest_divisor_from|random|matches|split|replace|lowercase|uppercase|trim|number_string|atom_string|term_string|append|string_concat|contains|join|substring|member|select|last|nth0|nth1|set_nth0|take|drop|slice|reverse|length|sum_list|min_list|max_list|list_to_set|countall|sumall|aggregate_min|aggregate_max)\s*\(/;
66
-
67
- function withStandardModules(source) {
68
- if (!libraryCall.test(source) || source.includes('use_module(library(') || source.includes(':- module(')) return source;
69
- return `:- use_module(library(aggregate)).
70
- :- use_module(library(comparison)).
71
- :- use_module(library(dates)).
72
- :- use_module(library(iso_ext)).
73
- :- use_module(library(lists)).
74
- :- use_module(library(primes)).
75
- :- use_module(library(prologue), [between/3]).
76
- :- use_module(library(random)).
77
- :- use_module(library(strings)).
78
- :- use_module(library(uuid)).
79
- ${source}`;
80
- }
81
73
 
82
74
  function run(source, options = {}) {
83
75
  const programSource = Array.isArray(source) ? source.join('\n') : source;
@@ -92,15 +84,24 @@ function sourceAtom(value) {
92
84
  return `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "''")}'`;
93
85
  }
94
86
 
95
- export function runRegression(reporter = new TestReporter()) {
87
+ export function runRegression(reporter = new TestReporter(), requestedSection = null) {
88
+ const sections = [
89
+ { key: 'regression', name: 'Regression', cases: regressionCases },
90
+ { key: 'docs', name: 'Documentation sync', cases: documentationSyncCases },
91
+ { key: 'api', name: 'API', cases: apiCases },
92
+ { key: 'white-box', name: 'White-box', cases: whiteBoxCases },
93
+ ];
94
+ const selected = requestedSection == null
95
+ ? sections
96
+ : sections.filter((section) => section.key === requestedSection);
97
+ if (selected.length === 0) {
98
+ throw new Error(`unknown regression section: ${requestedSection}; expected ${sections.map(({ key }) => key).join(', ')}`);
99
+ }
100
+
96
101
  tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeprolog-regression.'));
97
102
  tmpCounter = 0;
98
-
99
103
  try {
100
- runSection(reporter, 'Regression', regressionCases());
101
- runSection(reporter, 'Documentation sync', documentationSyncCases());
102
- runSection(reporter, 'API', apiCases());
103
- runSection(reporter, 'White-box', whiteBoxCases());
104
+ for (const section of selected) runSection(reporter, section.name, section.cases());
104
105
  } finally {
105
106
  fs.rmSync(tmp, { recursive: true, force: true });
106
107
  tmp = null;
@@ -738,6 +739,14 @@ c4 ?- call((!;1)).
738
739
  assertEqual(result.stderr, '', 'stderr');
739
740
  },
740
741
  },
742
+ {
743
+ name: 'arithmetic underflow choice differs from float-token input underflow (issue #56)',
744
+ run: () => {
745
+ const result = run('', { goal: 'catch((N is 0.1*10** -999), error(evaluation_error(underflow), _), N = underflow)' });
746
+ assertEqual(result.stdout, 'catch(underflow is 0.1 * 10 ** -999, error(evaluation_error(underflow), eyeprolog), underflow = underflow).\n', 'operation underflow');
747
+ assertEqual(run('', { goal: 'N = 0.1e-999' }).stdout, '0.0 = 0.0.\n', 'float-token input underflow');
748
+ },
749
+ },
741
750
  {
742
751
  name: 'float literals reject overflow and normalize underflow (issue #54)',
743
752
  run: () => {
@@ -1519,7 +1528,7 @@ c4 ?- call((!;1)).
1519
1528
  },
1520
1529
  },
1521
1530
  {
1522
- name: 'Trealla-style DCG hand-off autoloads time/1 and ...//0 without quadratic occurs checks (issue #49)',
1531
+ name: 'Trealla-style DCG hand-off autoloads time/1 and ... //0 without quadratic occurs checks (issue #49)',
1523
1532
  run: () => {
1524
1533
  const engineUrl = new URL('../src/index.js', import.meta.url).href;
1525
1534
  const script = `
@@ -1592,7 +1601,7 @@ c4 ?- call((!;1)).
1592
1601
  },
1593
1602
  },
1594
1603
  {
1595
- name: 'REPL applies conservative autoloading to interactive time/1 and consulted ...//0',
1604
+ name: 'REPL applies conservative autoloading to interactive time/1 and consulted ... //0',
1596
1605
  run: () => {
1597
1606
  const filename = path.join(tmp, `issue49-handoff-${tmpCounter++}.pl`);
1598
1607
  fs.writeFileSync(filename, 'a --> ..., epsilon.\nepsilon --> [].\n');
@@ -3399,6 +3408,33 @@ function documentationSyncCases() {
3399
3408
  name: 'documentation uses EyeProlog source style',
3400
3409
  run: () => assertArrayEqual(documentationSourceStyleIssues(), [], 'documentation source style'),
3401
3410
  },
3411
+ {
3412
+ name: 'DCG nonterminal indicator prose uses valid ... //0 spacing (issue #49)',
3413
+ run: () => {
3414
+ for (const filename of ['README.md', 'the-art-of-eyeprolog.md', 'src/standard-library.js', 'src/solver.js']) {
3415
+ const text = fs.readFileSync(path.join(packageRoot, filename), 'utf8');
3416
+ assertNotIncludes(text, '...' + '//0', `${filename} invalid compact nonterminal indicator`);
3417
+ }
3418
+ },
3419
+ },
3420
+ {
3421
+ name: 'ISO 5.4 decision index inventories implementation-defined choices (issue #56)',
3422
+ run: () => {
3423
+ const filename = path.join(testRoot, 'conformance', 'ISO-IMPLEMENTATION-DEFINED.md');
3424
+ const text = fs.readFileSync(filename, 'utf8');
3425
+ for (const clause of [
3426
+ '5.5.11', '6.5', '6.6', '7.1.2.2', '7.1.4.1', '7.4.2.4', '7.4.2.5',
3427
+ '7.4.2.6', '7.4.2.7', '7.4.2.8', '7.4.2.9', '7.5.1', '7.7.1', '7.7.3',
3428
+ '7.10.1', '7.10.2.6', '7.10.2.7', '7.10.2.8', '7.10.2.9', '7.10.2.11',
3429
+ '7.10.2.13', '7.11.1.1', '7.11.1.2', '7.11.1.3', '7.11.1.4', '7.11.2.1',
3430
+ '7.11.2.2', '7.11.2.3', '7.11.2.5', '7.12.1', '7.12.2(f)', '8.17.1',
3431
+ '8.17.3', '8.17.4', '9.1.3.1', '9.1.4.1', '9.1.4.2', '9.1.4.3', '9.4',
3432
+ '9.4.1', '9.4.2', '9.4.3', '9.4.4', '9.4.5', 'Cor.2 9.4.6',
3433
+ ]) assertIncludes(text, `| ${clause} |`, `ISO 5.4 clause ${clause}`);
3434
+ assertIncludes(text, 'Why issue #56', 'issue #56 explanation');
3435
+ assertIncludes(text, 'Implementation-specific features required to be documented by 5.4', '5.5 extension inventory');
3436
+ },
3437
+ },
3402
3438
  {
3403
3439
  name: 'book is the single implementation reference',
3404
3440
  run: () => assertArrayEqual(bookReferenceDocumentationIssues(), [], 'book reference documentation'),
@@ -5739,18 +5775,6 @@ function runCli(args, options = {}) {
5739
5775
  });
5740
5776
  }
5741
5777
 
5742
- function assertEqual(actual, expected, label) {
5743
- if (actual !== expected) throw new Error(`${label} mismatch\nexpected: ${format(expected)}\nactual: ${format(actual)}`);
5744
- }
5745
-
5746
- function assertIncludes(actual, expected, label) {
5747
- if (!actual.includes(expected)) throw new Error(`${label} did not include ${format(expected)}\nactual: ${format(actual)}`);
5748
- }
5749
-
5750
- function assertNotIncludes(actual, expected, label) {
5751
- if (String(actual).includes(expected)) throw new Error(`${label} unexpectedly included ${format(expected)}\nactual: ${format(actual)}`);
5752
- }
5753
-
5754
5778
  function arrayDiffMessages(actual, expected, label) {
5755
5779
  const messages = [];
5756
5780
  const actualSet = new Set(actual);
@@ -5776,11 +5800,5 @@ function format(value) {
5776
5800
  }
5777
5801
 
5778
5802
  if (isMainModule(import.meta.url)) {
5779
- const reporter = new TestReporter();
5780
- try {
5781
- runRegression(reporter);
5782
- reporter.totalLine();
5783
- } catch (_) {
5784
- process.exit(1);
5785
- }
5803
+ await runStandalone((reporter) => runRegression(reporter, process.argv[2] ?? null));
5786
5804
  }
package/test/run-wg17.mjs CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  } from '../src/index.js';
12
12
  import { parseTermText } from '../src/parser.js';
13
13
  import { variantTerms } from '../src/term.js';
14
- import { TestReporter, isMainModule } from './test-style.mjs';
14
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
15
15
 
16
16
  const testRoot = path.dirname(fileURLToPath(import.meta.url));
17
17
  const fixturePath = path.join(testRoot, 'conformance', 'wg17-syntax-cases.json');
@@ -377,12 +377,5 @@ export function runWg17(reporter = new TestReporter()) {
377
377
  }
378
378
 
379
379
  if (isMainModule(import.meta.url)) {
380
- const reporter = new TestReporter();
381
- try {
382
- runWg17(reporter);
383
- reporter.totalLine();
384
- } catch (error) {
385
- process.stderr.write(`${error?.stack ?? error}\n`);
386
- process.exitCode = 1;
387
- }
380
+ await runStandalone(runWg17);
388
381
  }
@@ -102,6 +102,43 @@ export function isMainModule(metaUrl) {
102
102
  return process.argv[1] != null && path.resolve(process.argv[1]) === fileURLToPath(metaUrl);
103
103
  }
104
104
 
105
+ export async function runStandalone(runSuite) {
106
+ const reporter = new TestReporter();
107
+ try {
108
+ await runSuite(reporter);
109
+ reporter.totalLine();
110
+ } catch (error) {
111
+ // TestReporter already prints failures raised inside a test. Preserve a
112
+ // diagnostic for setup/teardown failures that occur outside reporter.test.
113
+ if (reporter.ok === reporter.total) {
114
+ reporter.stderr.write(`${error?.stack ?? String(error)}\n`);
115
+ }
116
+ process.exitCode = 1;
117
+ }
118
+ }
119
+
120
+ export function assertEqual(actual, expected, label) {
121
+ if (actual !== expected) {
122
+ throw new Error(`${label} mismatch\nexpected: ${formatValue(expected)}\nactual: ${formatValue(actual)}`);
123
+ }
124
+ }
125
+
126
+ export function assertIncludes(actual, expected, label) {
127
+ if (!String(actual).includes(expected)) {
128
+ throw new Error(`${label} did not include ${formatValue(expected)}\nactual: ${formatValue(actual)}`);
129
+ }
130
+ }
131
+
132
+ export function assertNotIncludes(actual, expected, label) {
133
+ if (String(actual).includes(expected)) {
134
+ throw new Error(`${label} unexpectedly included ${formatValue(expected)}\nactual: ${formatValue(actual)}`);
135
+ }
136
+ }
137
+
138
+ function formatValue(value) {
139
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
140
+ }
141
+
105
142
  function defaultSectionLabel(name) {
106
143
  return String(name)
107
144
  .replace(/^Conformance\s+/, 'conformance ')
@@ -0,0 +1,36 @@
1
+ // Shared fixtures and filesystem helpers used across test suites.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const libraryCall = /\b(?:uuid|difference|maplist|foldl|call_nth|lt|gt|le|ge|between|smallest_divisor_from|random|matches|split|replace|lowercase|uppercase|trim|number_string|atom_string|term_string|append|string_concat|contains|join|substring|member|select|last|nth0|nth1|set_nth0|take|drop|slice|reverse|length|sum_list|min_list|max_list|list_to_set|countall|sumall|aggregate_min|aggregate_max)\s*\(/;
6
+
7
+ const standardModulePrelude = `:- use_module(library(aggregate)).
8
+ :- use_module(library(comparison)).
9
+ :- use_module(library(dates)).
10
+ :- use_module(library(iso_ext)).
11
+ :- use_module(library(lists)).
12
+ :- use_module(library(primes)).
13
+ :- use_module(library(prologue), [between/3]).
14
+ :- use_module(library(random)).
15
+ :- use_module(library(strings)).
16
+ :- use_module(library(uuid)).
17
+ `;
18
+
19
+ export function withStandardModules(source) {
20
+ const text = String(source);
21
+ if (!libraryCall.test(text) || text.includes('use_module(library(') || text.includes(':- module(')) return text;
22
+ return `${standardModulePrelude}${text}`;
23
+ }
24
+
25
+ export function listPrologFiles(base, dir = base) {
26
+ const files = [];
27
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
28
+ const full = path.join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ files.push(...listPrologFiles(base, full));
31
+ } else if (entry.isFile() && entry.name.endsWith('.pl')) {
32
+ files.push(path.relative(base, full).split(path.sep).join('/'));
33
+ }
34
+ }
35
+ return files.sort();
36
+ }
@@ -5668,7 +5668,7 @@ round-tripping. The checked answers are in
5668
5668
 
5669
5669
  #### Deep sequence hand-off
5670
5670
 
5671
- `library(iso_ext)` provides the common `...//0` helper, which describes an
5671
+ `library(iso_ext)` provides the common `... //0` helper, which describes an
5672
5672
  arbitrary number of input elements. It is not part of ISO Part 3, but it is a
5673
5673
  useful interoperability and stress-test relation. A compact hand-off test is:
5674
5674
 
@@ -5677,7 +5677,7 @@ a --> ..., epsilon.
5677
5677
  epsilon --> [].
5678
5678
  ```
5679
5679
 
5680
- Here the remaining sequence is repeatedly passed from `...//0` to another
5680
+ Here the remaining sequence is repeatedly passed from `... //0` to another
5681
5681
  nonterminal. For a finite compact list, EyeProlog can scan the arbitrary
5682
5682
  sequence iteratively instead of consuming one ordinary solver depth level per
5683
5683
  list cell. If the continuation is structurally proven to be a zero-width
@@ -6207,7 +6207,7 @@ The current interoperability profile recognizes these library roles:
6207
6207
  | Library | Role in the interoperability profile |
6208
6208
  | --- | --- |
6209
6209
  | `library(lists)` | Common list module. A conservative subset of its exports is in the shared predicate profile. |
6210
- | `library(iso_ext)` | Common extension-module name. `call_nth/2`, `time/1`, and the `...//0` arbitrary-sequence helper are conservatively autoloaded for cross-engine source. |
6210
+ | `library(iso_ext)` | Common extension-module name. `call_nth/2`, `time/1`, and the `... //0` arbitrary-sequence helper are conservatively autoloaded for cross-engine source. |
6211
6211
  | `library(lambda)` | Scryer-aligned higher-order notation. It is imported explicitly because loading it also installs the `+\` operator. |
6212
6212
  | `library(prologue)` | EyeProlog compatibility module, not a common interop library name. `between/3` is nevertheless autoloaded from it so portable source need not name this EyeProlog-specific provider. |
6213
6213
 
@@ -6234,7 +6234,7 @@ recovery headroom so finite-heap exhaustion remains a catchable
6234
6234
  EyeProlog API belongs to the shared profile. `call_nth/2`, `time/1`, and
6235
6235
  `.../2` are mapped there. `time/1` measures each solution of a meta-call and
6236
6236
  prints elapsed time, EyeProlog inference count, and MLips in Trealla-style form,
6237
- for example `% Time elapsed 0.832s, 65551 Inferences, 0.079 MLips`; `...//0`
6237
+ for example `% Time elapsed 0.832s, 65551 Inferences, 0.079 MLips`; `... //0`
6238
6238
  describes an arbitrary number of input elements. Together they let the
6239
6239
  Trealla/Scryer DCG hand-off benchmark run in EyeProlog without source changes
6240
6240
  (assuming the usual list library is already imported in an interactive
@@ -7416,6 +7416,10 @@ Part 1 conformance audit. It distinguishes implemented/tested families from
7416
7416
  requirements whose normative `shall` clauses, option combinations, or error
7417
7417
  precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
7418
7418
  maps language families to representative executable cases.
7419
+ `test/conformance/ISO-IMPLEMENTATION-DEFINED.md` is the ISO 5.4 decision
7420
+ index: it enumerates the Part 1 implementation-defined decisions, the
7421
+ implementation-specific extension families, and any remaining strict-mode audit
7422
+ gaps without turning draft WG17/STC proposals into the licensed baseline.
7419
7423
  `test/conformance/WG17-SYNTAX-STATUS.md` separately traces the vendored active
7420
7424
  upstream syntax cases. Reviewed cases can pin exact strict-reader outcomes, while
7421
7425
  newly upgraded cases execute directly against the upstream Codex expectation.