eyeprolog 1.5.44 → 1.5.46

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.
@@ -0,0 +1,36 @@
1
+ # Acknowledgements
2
+
3
+ This work was carried out at [IDLab](https://idlab.ugent.be/), Ghent University
4
+ – imec, within the [KNoWS](https://knows.idlab.ugent.be/) (Knowledge on Web
5
+ Scale) research team, as part of the **Koreografeye** project.
6
+
7
+ ## Standards
8
+
9
+ Conformance work on this repository is checked against the following documents,
10
+ which are consulted under a single-user licence and are not redistributed here:
11
+
12
+ - ISO/IEC 13211-1:1995, *Information technology — Programming languages —
13
+ Prolog — Part 1: General core*
14
+ - ISO/IEC 13211-1:1995/Cor.1:2007
15
+ - ISO/IEC 13211-1:1995/Cor.2:2012
16
+ - ISO/IEC 13211-1:1995/Cor.3:2017
17
+ - ISO/IEC 13211-2:2000, *Part 2: Modules*, and its 2013 amendment
18
+ - ISO/IEC TS 13211-3:2023, *Part 3: Definite clause grammar rules*
19
+
20
+ ## External conformance corpora
21
+
22
+ The test suite executes third-party material that is vendored unmodified and
23
+ credited in place:
24
+
25
+ - Ulrich Neumerkel's ISO Prolog conformity corpora (TU Wien), including the
26
+ `phrase_quad.pl` and `variable_names_quad.pl` snapshots under
27
+ `test/fixtures/`.
28
+ - The WG17 syntax conformity-testing matrix.
29
+ - Test cases adapted from the Logtalk ISO conformance suite, marked as such in
30
+ the corpus files that use them.
31
+
32
+ ## Citation
33
+
34
+ When referring to this work in a publication, please acknowledge the project as
35
+ described above. A suggested sentence is available in the paper template used by
36
+ the KNoWS team.
package/README.md CHANGED
@@ -72,7 +72,8 @@ printf 'human(socrates).\nmortal(X) :- human(X).\n' |
72
72
  - [rdf-prolog-roundtrip](https://github.com/eyereasoner/rdf-prolog-roundtrip) — standalone RDF 1.2 ↔ ISO Prolog bridge used by the RDF examples
73
73
  - [ISO conformance audit](test/conformance/ISO-COMPLIANCE.md) — supported Part 1 profile
74
74
  - [Latest Neumerkel conformity](test/conformance/NEUMERKEL-LATEST.md) — tracked result from the current live upstream inventory
75
- - [Conformance report](conformance-report.md) — generated executable conformance status and local corpus summary
75
+ - [Conformance report](conformance-report.md) — generated executable conformance status, local corpus summary, and known deviations
76
+ - [Acknowledgements](ACKNOWLEDGEMENTS.md) — funding, standards consulted, and credits for vendored conformance corpora
76
77
  - [OpenRuleBench](openrulebench/README.md) — portable benchmark profile
77
78
  ## RDF, Prolog, and symbiotic knowledge graphs
78
79
 
@@ -25,14 +25,14 @@ spelling therefore changes this report even when no corpus file is added or remo
25
25
 
26
26
  | Category | Positive | Errors | Warnings | Proofs | Total |
27
27
  |---|---:|---:|---:|---:|---:|
28
- | aggregation | 17 | 0 | 0 | 0 | 17 |
28
+ | aggregation | 18 | 0 | 0 | 0 | 18 |
29
29
  | arithmetic | 38 | 0 | 0 | 0 | 38 |
30
30
  | atoms | 23 | 8 | 0 | 0 | 31 |
31
31
  | builtins | 11 | 0 | 0 | 0 | 11 |
32
32
  | context | 11 | 0 | 0 | 0 | 11 |
33
33
  | control | 15 | 0 | 0 | 0 | 15 |
34
34
  | explicit-tabling | 6 | 0 | 0 | 0 | 6 |
35
- | iso | 169 | 217 | 0 | 0 | 386 |
35
+ | iso | 171 | 217 | 0 | 0 | 388 |
36
36
  | lists | 52 | 3 | 0 | 0 | 55 |
37
37
  | modules | 2 | 0 | 0 | 0 | 2 |
38
38
  | negation | 8 | 0 | 19 | 0 | 27 |
@@ -45,4 +45,29 @@ spelling therefore changes this report even when no corpus file is added or remo
45
45
  | terms | 26 | 3 | 0 | 0 | 29 |
46
46
  | unification | 18 | 0 | 0 | 0 | 18 |
47
47
  | variables | 16 | 7 | 0 | 0 | 23 |
48
- | **Total** | **490** | **272** | **19** | **21** | **802** |
48
+ | **Total** | **493** | **272** | **19** | **21** | **805** |
49
+
50
+ ## Known deviations
51
+
52
+ These are conscious departures from a standards clause, not open defects.
53
+
54
+ ### phrase/2-3 terminal-sequence type errors
55
+
56
+ ISO/IEC TS 13211-3:2023, 8.18.1.4 c and d specify that a processor which
57
+ checks the terminal-sequence arguments of `phrase/2-3` shall report
58
+ `type_error(terminal_sequence, S)`. Error clause c is required for
59
+ `phrase/2` and implementation defined for `phrase/3`.
60
+
61
+ EyeProlog checks both arguments but reports `type_error(list, S)`.
62
+ Ulrich Neumerkel's `phrase_quad.pl` corpus, which the release gate fetches
63
+ live from TU Wien, accepts only `false` or `type_error(list, S)` for quads
64
+ 41 and 42, so the published TS and that corpus cannot both be satisfied.
65
+ The corpus predates the TS. Changing the error term is a one-line edit in
66
+ `phraseSolutions` (`src/iso.js`); it additionally requires relaxing quads
67
+ 41-42 of the Neumerkel gate.
68
+
69
+ Note that TS 13211-3, 7.14.7.2 gives `phrase({true}, nonlist, S)` as an
70
+ example which succeeds with `S = nonlist`, which is consistent with the TS
71
+ permitting `phrase/3` to omit the check entirely. EyeProlog prefers the
72
+ stricter diagnostic that the TS also explicitly allows.
73
+
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.44",
6
+ "version": "1.5.46",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -28,6 +28,7 @@
28
28
  "files": [
29
29
  "LICENSE.md",
30
30
  "README.md",
31
+ "ACKNOWLEDGEMENTS.md",
31
32
  "index.js",
32
33
  "index.d.ts",
33
34
  "playground.html",
package/src/cli.js CHANGED
@@ -9,6 +9,15 @@ import { memoryStatistics } from './platform.js';
9
9
  let engineModule = null;
10
10
  let explanationModule = null;
11
11
 
12
+ // The usage text documents the input file as optional, and `--goal` is
13
+ // meaningful on its own. Only fall back to stdin when it is actually
14
+ // redirected: on an interactive terminal there is nothing to read, and
15
+ // blocking on it makes `eyeprolog --goal G` look like it does nothing.
16
+ // An explicit `-` argument still selects stdin in either case.
17
+ export function defaultsToStdin(fileCount, stdinIsTty) {
18
+ return fileCount === 0 && !stdinIsTty;
19
+ }
20
+
12
21
  export async function main(argv) {
13
22
  if (argv.length === 0) {
14
23
  const engine = await loadEngine();
@@ -132,7 +141,7 @@ export async function main(argv) {
132
141
  return;
133
142
  }
134
143
 
135
- if (options.files.length === 0) {
144
+ if (defaultsToStdin(options.files.length, process.stdin.isTTY)) {
136
145
  options.files.push('-');
137
146
  }
138
147
 
@@ -157,6 +166,12 @@ export async function main(argv) {
157
166
  }
158
167
  }
159
168
 
169
+ if (sourceParts.length === 0) {
170
+ // No file and no redirected stdin: run the requested goals against an
171
+ // empty database rather than against nothing at all.
172
+ sourceParts.push({ text: '', filename: '<empty>' });
173
+ }
174
+
160
175
  if (options.goals.length === 0 && !options.quads && options.verifyProof == null) {
161
176
  for (const source of sourceParts) options.goals.push(...goalsFromSource(source.text));
162
177
  }
package/src/iso.js CHANGED
@@ -2688,7 +2688,7 @@ function validateControlCallable(term, culprit, env) {
2688
2688
  // eagerly and reports the whole control term as the culprit.
2689
2689
  if (argument.type === VAR) continue;
2690
2690
  if (argument.type !== ATOM && argument.type !== COMPOUND) {
2691
- throw new PrologError('type_error(callable)', culprit);
2691
+ throw new PrologError('type_error(callable)', copyResolved(culprit, env));
2692
2692
  }
2693
2693
  pending.push(argument);
2694
2694
  }
@@ -3022,6 +3022,12 @@ function* phraseSolutions({ solver, goal, env }, state) {
3022
3022
  const input = goal.args[1];
3023
3023
  const requestedOutput = goal.arity === 2 ? emptyTerminalSequence() : goal.args[2];
3024
3024
  validateDcgEmbeddedGoals(grammarBody, input, requestedOutput);
3025
+ // ISO/IEC TS 13211-3:2023, 8.18.1.4 c and d require
3026
+ // type_error(terminal_sequence, S) here. EyeProlog still reports
3027
+ // type_error(list, S): Ulrich Neumerkel's phrase_quad corpus, which the
3028
+ // release gate fetches live, accepts only `false` or type_error(list, S)
3029
+ // for quads 41-42, so the two cannot currently both be satisfied. See
3030
+ // conformance-report.md, "Known deviations".
3025
3031
  if (!isListOrPartialList(input, env)) {
3026
3032
  throw new PrologError('type_error(list)', deref(input, env));
3027
3033
  }
@@ -445,6 +445,15 @@ export function directGoalDependencyKey(goal) {
445
445
  return `${goal.name}/${goal.arity}`;
446
446
  }
447
447
 
448
+ // ISO 13211-1, 7.1.6.3: `V1^V2^...^Goal` has the iterated goal Goal.
449
+ export function iteratedGoal(goal) {
450
+ let current = goal;
451
+ while (current?.type === COMPOUND && current.name === '^' && current.arity === 2) {
452
+ current = current.args[1];
453
+ }
454
+ return current;
455
+ }
456
+
448
457
  export function collectGoalDependencies(goal, negated, traverseConditionals = false, wfs = false) {
449
458
  if (goal.type === ATOM) return [{ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module, negative: negated, wfs }];
450
459
  if (goal.type !== COMPOUND) return [];
@@ -478,6 +487,11 @@ export function collectGoalDependencies(goal, negated, traverseConditionals = fa
478
487
  if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) {
479
488
  return collectGoalDependencies(goal.args[1], negated, traverseConditionals, wfs);
480
489
  }
490
+ if ((goal.name === 'bagof' || goal.name === 'setof') && goal.arity === 3) {
491
+ // The goal argument is an iterated-goal term (ISO 13211-1, 7.1.6.3), so
492
+ // strip its `^` qualifiers before recording the call it really performs.
493
+ return collectGoalDependencies(iteratedGoal(goal.args[1]), negated, traverseConditionals, wfs);
494
+ }
481
495
  if (goal.name === 'countall' && goal.arity === 2) {
482
496
  return collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs);
483
497
  }
package/src/program.js CHANGED
@@ -1136,6 +1136,18 @@ function collectForwardRulePremiseDependencies(term, collect, out) {
1136
1136
  // intentionally looks through those wrappers; the autoloader must see both the
1137
1137
  // wrapper and any statically visible nested calls so a bundled library can be
1138
1138
  // loaded before execution reaches it.
1139
+ // ISO 13211-1, 7.1.6.3: the iterated goal of `V1^V2^...^Goal` is Goal. The
1140
+ // qualifiers only mark witness variables as existentially quantified, so every
1141
+ // static analysis of a bagof/3 or setof/3 goal argument has to look through
1142
+ // them before it reaches the callable term.
1143
+ function iteratedGoalTerm(goal) {
1144
+ let current = goal;
1145
+ while (current?.type === COMPOUND && current.name === '^' && current.arity === 2) {
1146
+ current = current.args[1];
1147
+ }
1148
+ return current;
1149
+ }
1150
+
1139
1151
  function collectAutoloadGoalDependencies(goal, out = []) {
1140
1152
  if (goal?.type === ATOM) {
1141
1153
  out.push({ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module });
@@ -1167,7 +1179,12 @@ function collectAutoloadGoalDependencies(goal, out = []) {
1167
1179
  collectAutoloadGoalDependencies(goal.args[0], out);
1168
1180
  collectAutoloadGoalDependencies(goal.args[1], out);
1169
1181
  } else if ((goal.name === 'findall' || goal.name === 'bagof' || goal.name === 'setof' || goal.name === 'sumall') && goal.arity === 3) {
1170
- collectAutoloadGoalDependencies(goal.args[1], out);
1182
+ // bagof/3 and setof/3 take an iterated-goal term (ISO 13211-1, 7.1.6.3):
1183
+ // any number of `Witness^Goal` qualifiers wrap the goal that is actually
1184
+ // executed. Autoloading has to see through them, otherwise
1185
+ // setof(X, Y^member(X,L), S) raises existence_error(member/2) while the
1186
+ // unqualified setof(X, member(X,L), S) resolves normally.
1187
+ collectAutoloadGoalDependencies(iteratedGoalTerm(goal.args[1]), out);
1171
1188
  } else if (goal.name === 'countall' && goal.arity === 2) {
1172
1189
  collectAutoloadGoalDependencies(goal.args[0], out);
1173
1190
  } else if ((goal.name === 'aggregate_min' || goal.name === 'aggregate_max') && goal.arity === 5) {
@@ -140,7 +140,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
140
140
  Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
141
141
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
142
142
 
143
- The corpus has 386 cases in `iso/` and 802 file-based conformance cases in total. Of those, 11 cases in `stc/` are explicitly labelled working-draft review evidence rather than normative ISO claims. The separate vendored strict-reader WG17 matrix is a deterministic regression snapshot; the live Neumerkel gate discovers the current upstream inventory at run time; release/report checks separately verify the tracked `NEUMERKEL-LATEST.md`. The generated `conformance-report.md` records local corpus totals and links to that live evidence. Together with regression, documentation-sync, API, example, and book-example checks, `npm test` is the release gate.
143
+ The corpus has 388 cases in `iso/` and 805 file-based conformance cases in total. Of those, 11 cases in `stc/` are explicitly labelled working-draft review evidence rather than normative ISO claims. The separate vendored strict-reader WG17 matrix is a deterministic regression snapshot; the live Neumerkel gate discovers the current upstream inventory at run time; release/report checks separately verify the tracked `NEUMERKEL-LATEST.md`. The generated `conformance-report.md` records local corpus totals and links to that live evidence. Together with regression, documentation-sync, API, example, and book-example checks, `npm test` is the release gate.
144
144
 
145
145
  ## Updating expected output
146
146
 
@@ -0,0 +1,12 @@
1
+ % ISO/IEC 13211-1:1995, 7.1.6.3 (iterated-goal term) with 8.10.2/8.10.3.
2
+ % The goal argument of bagof/3 and setof/3 is `V1^...^Goal`, so every static
3
+ % analysis has to look through the `^` qualifiers to reach the callable term.
4
+ % Regression: the library autoloader used to stop at `^/2`, which made
5
+ % setof(X, Y^member(X,L), S) raise existence_error(member/2) while the
6
+ % unqualified setof(X, member(X,L), S) resolved normally.
7
+ answer(plain, S) :- setof(X, member(X, [b,a,b]), S).
8
+ answer(qualified, S) :- setof(X, Y^member(X, [b,a,b]), S).
9
+ answer(qualified_bag, S) :- bagof(X, Y^member(X, [b,a,b]), S).
10
+ answer(nested, S) :- setof(X, Y^Z^member(X-Y-Z, [1-a-p, 2-b-q]), S).
11
+ answer(witness_kept, S) :- setof(X-Y, Y^member(X-Y, [2-b, 1-a]), S).
12
+ %% goal: answer(X0, X1)
@@ -0,0 +1,35 @@
1
+ % ISO/IEC TS 13211-3:2023, 7.14 - the semantics of each grammar body element,
2
+ % expressed as the phrase/3 identities the TS states for them.
3
+ digit(D) --> [D], { member(D, [a,b]) }.
4
+
5
+ % 7.14.1 phrase([], S0, S) is true iff S0 = S.
6
+ answer(empty_sequence, S) :- phrase([], [a,b], S).
7
+
8
+ % 7.14.3 phrase((A,B), S0, S) iff phrase(A, S0, S1), phrase(B, S1, S).
9
+ answer(concatenation, S) :- phrase(([a], [b]), [a,b,c], S).
10
+
11
+ % 7.14.4 phrase((A;B), S0, S) iff ( phrase(A,S0,S) ; phrase(B,S0,S) ).
12
+ answer(alternative, S) :- phrase(([a] ; [b]), [b,c], S).
13
+
14
+ % 7.14.5.3 the if-then-else examples given verbatim in the TS.
15
+ answer(ite_13, yes) :- phrase((("1" | "2") -> "3" ; "4"), "13").
16
+ answer(ite_23, yes) :- phrase((("1" | "2") -> "3" ; "4"), "23").
17
+ answer(ite_4, yes) :- phrase((("1" | "2") -> "3" ; "4"), "4").
18
+ answer(ite_single, no) :- \+ phrase((("1" | "2") -> "3" ; "4"), [_]).
19
+
20
+ % 7.14.7 phrase({G}, S0, S) is true iff ( G, S0 = S ).
21
+ answer(grammar_body_goal, S) :- phrase({true}, [a], S).
22
+
23
+ % 7.14.9 phrase(phrase(NT), S0, S) iff phrase(NT, S0, S).
24
+ answer(phrase_nonterminal, S) :- phrase(phrase([a]), [a,b], S).
25
+
26
+ % 7.14.10 phrase(!, S0, S) is true iff S0 = S.
27
+ answer(cut_identity, S) :- phrase(!, [a,b], S).
28
+
29
+ % 7.14.11 phrase(\+ GRBody, S0, S) iff ( \+ phrase(GRBody, S0, _), S0 = S ).
30
+ answer(negation_identity, S) :- phrase(\+ [a], [b,c], S).
31
+ answer(negation_blocks, no) :- \+ phrase(\+ [a], [a], _).
32
+
33
+ % 7.14.2 ('.')//2 separates a terminal from the rest of the sequence.
34
+ answer(terminal_split, D) :- phrase(digit(D), [b]).
35
+ %% goal: answer(X0, X1)
@@ -0,0 +1,42 @@
1
+ % ISO/IEC TS 13211-3:2023, 8.18.1.5 - the worked phrase/2 and phrase/3
2
+ % examples, using the grammar given verbatim in that subclause.
3
+ determiner --> [the].
4
+ determiner --> [a].
5
+
6
+ noun --> [boy].
7
+ noun --> [girl].
8
+
9
+ verb --> [likes].
10
+ verb --> [scares].
11
+
12
+ noun_phrase --> determiner, noun.
13
+ noun_phrase --> noun.
14
+
15
+ verb_phrase --> verb.
16
+ verb_phrase --> verb, noun_phrase.
17
+
18
+ sentence --> noun_phrase, verb_phrase.
19
+
20
+ % ?- phrase([the], [the]). true.
21
+ answer(terminal_only, yes) :- phrase([the], [the]).
22
+
23
+ % ?- phrase(sentence, [the, girl, likes, the, boy]). true ; false.
24
+ answer(full_sentence, yes) :- phrase(sentence, [the, girl, likes, the, boy]).
25
+
26
+ % ?- phrase(sentence, [the, girl, likes, the, boy, today]). false.
27
+ answer(trailing_word, no) :- \+ phrase(sentence, [the, girl, likes, the, boy, today]).
28
+
29
+ % ?- phrase(sentence, [the, girl, likes]). true ; false.
30
+ answer(intransitive, yes) :- phrase(sentence, [the, girl, likes]).
31
+
32
+ % ?- phrase(noun_phrase, [the, girl, scares, the, boy], Rest).
33
+ % Rest = [scares, the, boy] ; false.
34
+ answer(remainder, Rest) :- phrase(noun_phrase, [the, girl, scares, the, boy], Rest).
35
+
36
+ % ?- phrase(sentence, Sentence). Sentence = [the, boy, likes] ; ... .
37
+ answer(first_generated, Sentence) :-
38
+ findall(S, phrase(sentence, S), All),
39
+ first_four(All, Sentence).
40
+
41
+ first_four([A,B,C,D|_], [A,B,C,D]).
42
+ %% goal: answer(X0, X1)
@@ -0,0 +1,5 @@
1
+ answer(plain, "ab").
2
+ answer(qualified, "ab").
3
+ answer(qualified_bag, "bab").
4
+ answer(nested, [1, 2]).
5
+ answer(witness_kept, [1 - a, 2 - b]).
@@ -0,0 +1,13 @@
1
+ answer(empty_sequence, "ab").
2
+ answer(concatenation, "c").
3
+ answer(alternative, "c").
4
+ answer(ite_13, yes).
5
+ answer(ite_23, yes).
6
+ answer(ite_4, yes).
7
+ answer(ite_single, no).
8
+ answer(grammar_body_goal, "a").
9
+ answer(phrase_nonterminal, "b").
10
+ answer(cut_identity, "ab").
11
+ answer(negation_identity, "bc").
12
+ answer(negation_blocks, no).
13
+ answer(terminal_split, b).
@@ -0,0 +1,6 @@
1
+ answer(terminal_only, yes).
2
+ answer(full_sentence, yes).
3
+ answer(trailing_word, no).
4
+ answer(intransitive, yes).
5
+ answer(remainder, [scares, the, boy]).
6
+ answer(first_generated, [[the, boy, likes], [the, boy, scares], [the, boy, likes, the, boy], [the, boy, likes, the, girl]]).
@@ -119,6 +119,8 @@ export function formatConformanceReport(report = buildConformanceReport()) {
119
119
  }
120
120
  lines.push(`| **Total** | **${report.total.positive}** | **${report.total.errors}** | **${report.total.warnings}** | **${report.total.proofs}** | **${report.total.total}** |`);
121
121
 
122
+ lines.push(...knownDeviationSection());
123
+
122
124
  if (report.corpusIssues.length > 0) {
123
125
  lines.push('', '## Corpus issues', '');
124
126
  for (const issue of report.corpusIssues) lines.push(`- ${issue}`);
@@ -131,6 +133,39 @@ export function formatConformanceReport(report = buildConformanceReport()) {
131
133
  return `${lines.join('\n')}\n`;
132
134
  }
133
135
 
136
+ // Deviations that are known, deliberate, and traceable to a specific clause.
137
+ // Keeping them in the generated report means they survive regeneration instead
138
+ // of living only in a commit message.
139
+ function knownDeviationSection() {
140
+ return [
141
+ '',
142
+ '## Known deviations',
143
+ '',
144
+ 'These are conscious departures from a standards clause, not open defects.',
145
+ '',
146
+ '### phrase/2-3 terminal-sequence type errors',
147
+ '',
148
+ 'ISO/IEC TS 13211-3:2023, 8.18.1.4 c and d specify that a processor which',
149
+ 'checks the terminal-sequence arguments of `phrase/2-3` shall report',
150
+ '`type_error(terminal_sequence, S)`. Error clause c is required for',
151
+ '`phrase/2` and implementation defined for `phrase/3`.',
152
+ '',
153
+ 'EyeProlog checks both arguments but reports `type_error(list, S)`.',
154
+ 'Ulrich Neumerkel\'s `phrase_quad.pl` corpus, which the release gate fetches',
155
+ 'live from TU Wien, accepts only `false` or `type_error(list, S)` for quads',
156
+ '41 and 42, so the published TS and that corpus cannot both be satisfied.',
157
+ 'The corpus predates the TS. Changing the error term is a one-line edit in',
158
+ '`phraseSolutions` (`src/iso.js`); it additionally requires relaxing quads',
159
+ '41-42 of the Neumerkel gate.',
160
+ '',
161
+ 'Note that TS 13211-3, 7.14.7.2 gives `phrase({true}, nonlist, S)` as an',
162
+ 'example which succeeds with `S = nonlist`, which is consistent with the TS',
163
+ 'permitting `phrase/3` to omit the check entirely. EyeProlog prefers the',
164
+ 'stricter diagnostic that the TS also explicitly allows.',
165
+ '',
166
+ ];
167
+ }
168
+
134
169
  function executeGate(name, runSuite) {
135
170
  const failures = [];
136
171
  const reporter = {
@@ -72,6 +72,29 @@ export function runIsoStrict(reporter = new TestReporter()) {
72
72
  'type_error(float)', 'negative integer exponent correction');
73
73
  });
74
74
 
75
+ reporter.test('round-trips ISO floats through number_chars/2 and quoted write_term/2', () => {
76
+ for (const literal of ['1.0000000000000001', '0.10000000000000002']) {
77
+ const converted = run('', {
78
+ isoStrict: true,
79
+ goal: `N=${literal}, number_chars(N,Chars), number_chars(M,Chars), M == N`,
80
+ });
81
+ equal(converted.stats.completed_goal_lists, 1, `number_chars/2 float round-trip ${literal}`);
82
+
83
+ let written = '';
84
+ run('', {
85
+ isoStrict: true,
86
+ goal: `write_term(${literal},[quoted(true)])`,
87
+ ioOptions: { write: (text) => { written += text; } },
88
+ });
89
+ const reread = run('', {
90
+ isoStrict: true,
91
+ goal: `read_term(X,[]), X == ${literal}`,
92
+ ioOptions: { input: `${written}.` },
93
+ });
94
+ equal(reread.stats.completed_goal_lists, 1, `quoted write_term/2 float round-trip ${literal}`);
95
+ }
96
+ });
97
+
75
98
  reporter.test('keeps Corrigendum 2 core predicates and excludes Part 3 phrase', () => {
76
99
  const registry = createStrictIsoRegistry();
77
100
  equal(Boolean(registry.get('subsumes_term', 2)), true, 'subsumes_term/2');
@@ -1250,6 +1273,16 @@ export function runIsoStrict(reporter = new TestReporter()) {
1250
1273
  'if-then-else commits after a successful condition');
1251
1274
  equal(capture(() => run('', { isoStrict: true, goal: 'call((write(a),3))' })).formal,
1252
1275
  'type_error(callable)', 'nested non-callable control term is rejected');
1276
+ let issue93Output = '';
1277
+ const issue93Error = capture(() => run('', {
1278
+ isoStrict: true,
1279
+ goal: 'X=3,Y=(write(X),X),call(Y)',
1280
+ ioOptions: { write: (chunk) => { issue93Output += chunk; } },
1281
+ }));
1282
+ equal(issue93Error.formal, 'type_error(callable)', 'issue #93 raises the ISO callability error');
1283
+ equal(issue93Error.message, 'error(type_error(callable), (write(3), 3))',
1284
+ 'issue #93 reports the fully instantiated control-term culprit');
1285
+ equal(issue93Output, '', 'issue #93 validates the complete body before executing write/1');
1253
1286
  equal(run('', {
1254
1287
  isoStrict: true,
1255
1288
  goal: 'catch(X,error(instantiation_error,_),true)',
@@ -54,6 +54,7 @@ import {
54
54
  import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText, tryParseClausesFastInto } from '../src/parser.js';
55
55
  import { PrologError, formalErrorTerm } from '../src/iso.js';
56
56
  import { compareTerms } from '../src/term.js';
57
+ import { defaultsToStdin } from '../src/cli.js';
57
58
  import { formatTermForWrite } from '../src/write.js';
58
59
  import { selectClauseCandidates } from '../src/program.js';
59
60
  import {
@@ -1596,6 +1597,25 @@ c4 ?- call((!;1)).
1596
1597
  assertEqual(result.stdout, '250000', 'number_chars/read cross-check count');
1597
1598
  },
1598
1599
  },
1600
+ {
1601
+ name: '--goal without an input file does not block on an interactive stdin',
1602
+ run: () => {
1603
+ // Usage documents the input file as optional. The CLI used to append
1604
+ // '-' unconditionally, so `eyeprolog --goal G` on a terminal waited for
1605
+ // an EOF that never came and appeared to do nothing. stdin is only a
1606
+ // default source when it is actually redirected.
1607
+ assertEqual(defaultsToStdin(0, true), false, 'tty stdin, no file');
1608
+ assertEqual(defaultsToStdin(0, false), true, 'redirected stdin, no file');
1609
+ assertEqual(defaultsToStdin(0, undefined), true, 'non-tty stdin reported as undefined');
1610
+ assertEqual(defaultsToStdin(1, true), false, 'tty stdin, one file');
1611
+ assertEqual(defaultsToStdin(1, false), false, 'redirected stdin, one file');
1612
+
1613
+ // An empty database is still a usable program for --goal.
1614
+ const result = runCli(['--goal', 'X = 1, write(X), nl'], { input: '' });
1615
+ assertEqual(result.status, 0, 'exit status');
1616
+ assertIncludes(result.stdout, '1\n', 'goal output without an input file');
1617
+ },
1618
+ },
1599
1619
  {
1600
1620
  name: 'float literals canonicalize to their represented value and mix with generated floats (issue #92)',
1601
1621
  run: () => {
@@ -1618,6 +1638,20 @@ c4 ?- call((!;1)).
1618
1638
  assertEqual(result.stderr, '', 'stderr');
1619
1639
  },
1620
1640
  },
1641
+ {
1642
+ name: 'call/1 reports the instantiated complete control-term culprit (issue #93)',
1643
+ run: () => {
1644
+ const result = runCli([], {
1645
+ input: 'X = 3, Y = (write(X), X), call(Y).\nhalt.\n',
1646
+ });
1647
+ assertEqual(result.status, 0, 'exit status');
1648
+ assertEqual(result.stdout,
1649
+ '?- error(type_error(callable, (write(3), 3)), eyeprolog).\n' +
1650
+ '?- ',
1651
+ 'fully instantiated call/1 culprit');
1652
+ assertEqual(result.stderr, '', 'stderr');
1653
+ },
1654
+ },
1621
1655
  {
1622
1656
  name: 'number_chars and number_codes keep exponent floats syntactically readable (issue #50)',
1623
1657
  run: () => {
@@ -10353,7 +10353,7 @@ must preserve the same observable outcome. Additional normal-mode syntax may
10353
10353
  accept texts outside the strict grammar, but it may not reinterpret an accepted
10354
10354
  standard case.
10355
10355
 
10356
- The file-based conformance corpus contains 802 cases, including 386 focused ISO cases derived from the success, failure, mode, and error behavior in ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
10356
+ The file-based conformance corpus contains 805 cases, including 388 focused ISO cases derived from the success, failure, mode, and error behavior in ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
10357
10357
  Separate exact-output suites check 210 normal examples and 61 proof examples; all executable chapter programs are parsed and their declared goals are executed. The eight-case
10358
10358
  playground contract suite imports the production worker, sends real reasoning
10359
10359
  requests through its message protocol, and crawls the served module graph for