eyeprolog 1.3.47 → 1.3.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.47",
6
+ "version": "1.3.49",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -20,10 +20,57 @@ function evaluate(term, env, options = {}) {
20
20
  throw new PrologError('type_error(evaluable)', compound('/', [atom(term.name), numberTerm(0)]));
21
21
  }
22
22
  if (term.type !== COMPOUND) throw new PrologError('type_error(evaluable)', term);
23
- const args = term.args.map((arg) => evaluate(arg, env, options));
24
- return evaluateOperation(term, args);
23
+
24
+ let args;
25
+ if (options.isoStrict === true) {
26
+ // ISO 7.9.2(b) gives a direct variable argument precedence over the other
27
+ // expression errors of the same compound expression. Check the direct
28
+ // arguments before selecting an implementation-dependent evaluation order.
29
+ const operands = term.args.map((arg) => deref(arg, env));
30
+ if (operands.some((arg) => arg.type === VAR)) throw new PrologError('instantiation_error');
31
+ args = operands.map((arg) => evaluateOperand(term, arg, env, options));
32
+ } else {
33
+ // Preserve the normal EyeProlog profile's established left-to-right
34
+ // evaluator and diagnostics; the stricter prescribed errors belong to
35
+ // the explicit ISO profile.
36
+ args = term.args.map((arg) => evaluate(arg, env, options));
37
+ }
38
+ return evaluateOperation(term, args, options);
39
+ }
40
+
41
+ function evaluatesAtomDirectly(term, options) {
42
+ return term.type === ATOM && (term.name === 'pi' || (term.name === 'e' && options.isoStrict !== true));
25
43
  }
26
- function evaluateOperation(term, args) {
44
+
45
+ function directOperandType(term) {
46
+ const name = term.name;
47
+ const arity = term.arity;
48
+ if ((arity === 1 && name === '\\') ||
49
+ (arity === 2 && ['>>', '<<', '/\\', '\\/', 'xor'].includes(name))) return 'integer';
50
+
51
+ if ((arity === 1 && ['+', '-', 'abs', 'sign', 'float', 'truncate', 'round', 'ceiling', 'floor',
52
+ 'float_integer_part', 'float_fractional_part', 'sin', 'cos', 'atan', 'asin', 'acos', 'tan',
53
+ 'exp', 'log', 'sqrt'].includes(name)) ||
54
+ (arity === 2 && ['+', '-', '*', '/', '//', 'div', 'mod', 'rem', '**', '^', 'max', 'min', 'atan2'].includes(name))) {
55
+ return 'number';
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function evaluateOperand(parent, operand, env, options) {
61
+ // The arithmetic-functor error clauses use the offending operand as the
62
+ // culprit when an atomic operand is not numeric (e.g. +(foo,77),
63
+ // truncate(foo), and the bitwise families). A nested compound still goes
64
+ // through ordinary expression evaluation so an unknown nested F/N retains
65
+ // type_error(evaluable,F/N).
66
+ if (operand.type === ATOM && !evaluatesAtomDirectly(operand, options)) {
67
+ const expected = directOperandType(parent);
68
+ if (expected) throw new PrologError(`type_error(${expected})`, operand);
69
+ }
70
+ return evaluate(operand, env, options);
71
+ }
72
+
73
+ function evaluateOperation(term, args, options = {}) {
27
74
  const name = term.name;
28
75
  const arity = term.arity;
29
76
  if (arity === 1 && (name === '+' || name === '-')) {
@@ -38,6 +85,12 @@ function evaluateOperation(term, args) {
38
85
  if (arity === 1 && ['abs', 'sign', 'float', 'truncate', 'round', 'ceiling', 'floor',
39
86
  'float_integer_part', 'float_fractional_part',
40
87
  'sin', 'cos', 'atan', 'asin', 'acos', 'tan', 'exp', 'log', 'sqrt'].includes(name)) {
88
+ // The four float-to-integer rounding functors have only F->I operators
89
+ // (9.1.1/9.1.6). Corrigendum 1/2 therefore requires type_error(float,...)
90
+ // when their evaluated argument has integer type.
91
+ if (options.isoStrict === true && ['truncate', 'round', 'ceiling', 'floor'].includes(name) && args[0].integer) {
92
+ throw new PrologError('type_error(float)', numericTerm(args[0]));
93
+ }
41
94
  const a = Number(args[0].value);
42
95
  if (name === 'abs' && args[0].integer) return { integer: true, value: args[0].value < 0n ? -args[0].value : args[0].value };
43
96
  if (name === 'sign' && args[0].integer) return { integer: true, value: args[0].value < 0n ? -1n : args[0].value > 0n ? 1n : 0n };
@@ -105,6 +158,11 @@ function evaluateOperation(term, args) {
105
158
  if (x === 0 && y === 0) throw new PrologError('evaluation_error(undefined)');
106
159
  value = Math.atan2(x, y);
107
160
  }
161
+ else if ((name === '**' || name === '^') && x === 0 && y < 0) {
162
+ // 9.3.1.3(d) and Cor.2 9.3.10.3(d): zero to a negative power is
163
+ // undefined, not floating-point overflow.
164
+ throw new PrologError('evaluation_error(undefined)');
165
+ }
108
166
  else if (name === '+') value = x + y;
109
167
  else if (name === '-') value = x - y;
110
168
  else if (name === '*') value = x * y;
@@ -152,6 +210,22 @@ export function compareArithmeticValues(left, right) {
152
210
  if (right.integer) return -compareIntegerToFloat(b, a);
153
211
  return a < b ? -1 : a > b ? 1 : 0;
154
212
  }
213
+
214
+ function compareStrictIsoArithmeticValues(left, right) {
215
+ if (left.integer === right.integer) return compareArithmeticValues(left, right);
216
+
217
+ // ISO/IEC 13211-1:1995 8.7 specifies mixed arithmetic comparisons by
218
+ // floatI->F conversion of the integer operand. That conversion can round
219
+ // an otherwise exact integer and can itself overflow. Normal EyeProlog
220
+ // deliberately keeps its later exact cross-type comparison extension; the
221
+ // strict profile follows the Part 1 operation table.
222
+ const leftValue = left.integer ? Number(left.value) : left.value;
223
+ const rightValue = right.integer ? Number(right.value) : right.value;
224
+ if (!Number.isFinite(leftValue) || !Number.isFinite(rightValue)) {
225
+ throw new PrologError('evaluation_error(float_overflow)');
226
+ }
227
+ return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
228
+ }
155
229
  export function* isBuiltin({ solver, goal, env }) {
156
230
  const result = arithmeticValueTerm(evaluateArithmetic(goal.args[1], env, { isoStrict: solver?.isoStrict === true }));
157
231
  const next = env.clone();
@@ -162,7 +236,9 @@ export function arithmeticComparison(test) {
162
236
  const options = { isoStrict: solver?.isoStrict === true };
163
237
  const left = evaluateArithmetic(goal.args[0], env, options);
164
238
  const right = evaluateArithmetic(goal.args[1], env, options);
165
- const cmp = compareArithmeticValues(left, right);
239
+ const cmp = solver?.isoStrict === true
240
+ ? compareStrictIsoArithmeticValues(left, right)
241
+ : compareArithmeticValues(left, right);
166
242
  if (test(cmp)) yield env;
167
243
  };
168
244
  }
package/src/iso.js CHANGED
@@ -415,11 +415,19 @@ function* functorBuiltin({ goal, env }) {
415
415
  }
416
416
 
417
417
  function* argBuiltin({ goal, env }) {
418
- const index = requireInteger(goal.args[0], env);
418
+ const indexTerm = deref(goal.args[0], env);
419
419
  const term = deref(goal.args[1], env);
420
+ // ISO 8.5.2.3 prescribes both instantiation checks before either type
421
+ // check. In particular arg(a, X, _) is an instantiation error because
422
+ // the second argument is still a variable.
423
+ if (indexTerm.type === VAR) throw new PrologError('instantiation_error');
420
424
  if (term.type === VAR) throw new PrologError('instantiation_error');
425
+ if (indexTerm.type !== NUMBER || !isDecimalInteger(indexTerm.name)) {
426
+ throw new PrologError('type_error(integer)', indexTerm);
427
+ }
421
428
  if (term.type !== COMPOUND) throw new PrologError('type_error(compound)', term);
422
- if (index < 0n) throw new PrologError('domain_error(not_less_than_zero)', deref(goal.args[0], env));
429
+ const index = BigInt(indexTerm.name);
430
+ if (index < 0n) throw new PrologError('domain_error(not_less_than_zero)', indexTerm);
423
431
  if (index === 0n || index > BigInt(term.arity)) return;
424
432
  const next = env.clone();
425
433
  if (unify(goal.args[2], term.args[Number(index) - 1], next)) yield next;
@@ -537,7 +545,7 @@ function* clauseBuiltin({ solver, goal, env }) {
537
545
  const indicator = compound('/', [atom(head.name), numberTerm(head.arity)]);
538
546
  const group = solver.program.findGroup(head.name, head.arity, head.module ?? goal.module ?? 'user');
539
547
  // ISO 8.8.1.3 places private-procedure access before Body callability.
540
- if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(solver, head) ||
548
+ if (isProcessorStaticProcedure(solver, head) ||
541
549
  (solver.isoStrict && group && !group.dynamic)) {
542
550
  throw new PrologError('permission_error(access, private_procedure)', indicator);
543
551
  }
@@ -581,9 +589,19 @@ function isGrammarRuleProcedure(solver, head) {
581
589
  return !solver.isoStrict && head.name === '-->' && head.arity === 2;
582
590
  }
583
591
 
592
+ function isProcessorStaticProcedure(solver, head) {
593
+ // Conjunction is an ISO control construct (7.5/Table 9) but is executed
594
+ // directly by the solver rather than through the builtin registry. Keep it
595
+ // in the same protected static/private family as the registered Part 1
596
+ // control constructs for runtime database operations.
597
+ return Boolean(solver.registry.get(head.name, head.arity)) ||
598
+ isGrammarRuleProcedure(solver, head) ||
599
+ (solver.isoStrict && head.name === ',' && head.arity === 2);
600
+ }
601
+
584
602
  function assertModifiable(solver, head, module = 'user') {
585
603
  const group = solver.program.findGroup(head.name, head.arity, head.module ?? module);
586
- if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(solver, head) || (group && !group.dynamic)) {
604
+ if (isProcessorStaticProcedure(solver, head) || (group && !group.dynamic)) {
587
605
  throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(head));
588
606
  }
589
607
  }
@@ -610,7 +628,7 @@ function* retractBuiltin({ solver, goal, env }) {
610
628
  const parts = clauseParts(goal.args[0], env);
611
629
  requireClauseHead(parts.head);
612
630
  const group = solver.program.findGroup(parts.head.name, parts.head.arity, parts.head.module ?? goal.module ?? 'user');
613
- if (solver.registry.get(parts.head.name, parts.head.arity) || isGrammarRuleProcedure(solver, parts.head) || (group && !group.dynamic)) {
631
+ if (isProcessorStaticProcedure(solver, parts.head) || (group && !group.dynamic)) {
614
632
  throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(parts.head));
615
633
  }
616
634
  if (!group) return;
@@ -633,7 +651,7 @@ function* retractAllBuiltin({ solver, goal, env }) {
633
651
  const head = deref(goal.args[0], env);
634
652
  requireClauseHead(head);
635
653
  const group = solver.program.findGroup(head.name, head.arity, head.module ?? goal.module ?? 'user');
636
- if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(solver, head) || (group && !group.dynamic)) {
654
+ if (isProcessorStaticProcedure(solver, head) || (group && !group.dynamic)) {
637
655
  throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(head));
638
656
  }
639
657
  if (group) {
@@ -667,7 +685,7 @@ function* abolishBuiltin({ solver, goal, env }) {
667
685
  const target = predicateIndicatorParts(goal.args[0], env);
668
686
  const module = goal.module ?? 'user';
669
687
  const group = solver.program.findGroup(target.name, target.arity, module);
670
- if (solver.registry.get(target.name, target.arity) || isGrammarRuleProcedure(solver, target) || (group && !group.dynamic)) {
688
+ if (isProcessorStaticProcedure(solver, target) || (group && !group.dynamic)) {
671
689
  throw new PrologError('permission_error(modify, static_procedure)', target.indicator);
672
690
  }
673
691
  solver.program.abolishDynamicGroup(target.name, target.arity, module);
@@ -815,8 +833,14 @@ function conversionCharacter(term, env, current = false, solver = null) {
815
833
  return value;
816
834
  }
817
835
  function* charConversionBuiltin({ solver, goal, env }) {
818
- const input = conversionCharacter(goal.args[0], env, false, solver);
819
- const output = conversionCharacter(goal.args[1], env, false, solver);
836
+ // ISO 8.14.5.3 checks both arguments for instantiation before testing
837
+ // either character representation.
838
+ const inputTerm = deref(goal.args[0], env);
839
+ const outputTerm = deref(goal.args[1], env);
840
+ if (inputTerm.type === VAR) throw new PrologError('instantiation_error');
841
+ if (outputTerm.type === VAR) throw new PrologError('instantiation_error');
842
+ const input = conversionCharacter(inputTerm, env, false, solver);
843
+ const output = conversionCharacter(outputTerm, env, false, solver);
820
844
  if (input.name === output.name) solver.charConversions.delete(input.name);
821
845
  else solver.charConversions.set(input.name, output.name);
822
846
  yield env;
@@ -970,7 +994,7 @@ function* openBuiltin({ solver, goal, env }) {
970
994
  if (!['read', 'write', 'append'].includes(mode.name)) throw new PrologError('domain_error(io_mode)', mode);
971
995
  const options = goal.arity === 3 ? {} : openOptions(optionTerm, env);
972
996
  if (options.alias && solver.io.resolve(options.alias)) {
973
- throw new PrologError('permission_error(open, source_sink)', atom(options.alias));
997
+ throw new PrologError('permission_error(open, source_sink)', compound('alias', [atom(options.alias)]));
974
998
  }
975
999
  let stream;
976
1000
  try {
@@ -1689,11 +1713,16 @@ function* atomLengthBuiltin({ goal, env }) {
1689
1713
  }
1690
1714
 
1691
1715
  function* atomConcatBuiltin({ goal, env }) {
1692
- const first = resolvedOrVariable(goal.args[0], env, ATOM);
1693
- const second = resolvedOrVariable(goal.args[1], env, ATOM);
1694
- const whole = resolvedOrVariable(goal.args[2], env, ATOM);
1716
+ const first = deref(goal.args[0], env);
1717
+ const second = deref(goal.args[1], env);
1718
+ const whole = deref(goal.args[2], env);
1719
+ // ISO 8.16.2.3 puts the two under-instantiation modes ahead of all atom
1720
+ // type diagnostics.
1695
1721
  if (first.type === VAR && whole.type === VAR) throw new PrologError('instantiation_error');
1696
1722
  if (second.type === VAR && whole.type === VAR) throw new PrologError('instantiation_error');
1723
+ if (first.type !== VAR && first.type !== ATOM) throw new PrologError('type_error(atom)', first);
1724
+ if (second.type !== VAR && second.type !== ATOM) throw new PrologError('type_error(atom)', second);
1725
+ if (whole.type !== VAR && whole.type !== ATOM) throw new PrologError('type_error(atom)', whole);
1697
1726
 
1698
1727
  const candidates = [];
1699
1728
  if (whole.type === ATOM && first.type === VAR && second.type === VAR) {
@@ -1713,23 +1742,33 @@ function* atomConcatBuiltin({ goal, env }) {
1713
1742
  }
1714
1743
  }
1715
1744
 
1716
- function optionalNonNegativeInteger(term, env) {
1745
+ function optionalInteger(term, env) {
1717
1746
  const value = deref(term, env);
1718
1747
  if (value.type === VAR) return null;
1719
1748
  if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(integer)', value);
1720
- const integer = BigInt(value.name);
1721
- if (integer < 0n) throw new PrologError('domain_error(not_less_than_zero)', value);
1722
- return integer;
1749
+ return BigInt(value.name);
1723
1750
  }
1724
1751
 
1725
1752
  function* subAtomBuiltin({ goal, env }) {
1726
1753
  const source = deref(goal.args[0], env);
1727
1754
  if (source.type === VAR) throw new PrologError('instantiation_error');
1728
1755
  if (source.type !== ATOM) throw new PrologError('type_error(atom)', source);
1729
- const sub = resolvedOrVariable(goal.args[4], env, ATOM);
1730
- const before = optionalNonNegativeInteger(goal.args[1], env);
1731
- const length = optionalNonNegativeInteger(goal.args[2], env);
1732
- const after = optionalNonNegativeInteger(goal.args[3], env);
1756
+ const sub = deref(goal.args[4], env);
1757
+ if (sub.type !== VAR && sub.type !== ATOM) throw new PrologError('type_error(atom)', sub);
1758
+ // ISO 8.16.3.3 prescribes all type checks before the non-negative domain
1759
+ // checks, in Before/Length/After order.
1760
+ const before = optionalInteger(goal.args[1], env);
1761
+ const length = optionalInteger(goal.args[2], env);
1762
+ const after = optionalInteger(goal.args[3], env);
1763
+ if (before != null && before < 0n) {
1764
+ throw new PrologError('domain_error(not_less_than_zero)', deref(goal.args[1], env));
1765
+ }
1766
+ if (length != null && length < 0n) {
1767
+ throw new PrologError('domain_error(not_less_than_zero)', deref(goal.args[2], env));
1768
+ }
1769
+ if (after != null && after < 0n) {
1770
+ throw new PrologError('domain_error(not_less_than_zero)', deref(goal.args[3], env));
1771
+ }
1733
1772
  const chars = characters(source.name);
1734
1773
  for (let start = 0; start <= chars.length; start++) {
1735
1774
  if (before != null && before !== BigInt(start)) continue;
@@ -1770,9 +1809,14 @@ function validCharacterCode(value, solver = null) {
1770
1809
  }
1771
1810
 
1772
1811
  function listToAtomInput(list, env, kind, solver = null) {
1812
+ const whole = deref(list, env);
1773
1813
  const { items, tail } = listElements(list, env);
1774
- if (tail.type === VAR || items.some((item) => item.type === VAR)) throw new PrologError('instantiation_error');
1775
- if (tail.type !== ATOM || tail.name !== '[]') throw new PrologError('type_error(list)', list);
1814
+ // Corrigendum 2 distinguishes a partial list from an improper one: only
1815
+ // the former precedes the complete-list type check. Variables in a proper
1816
+ // list prefix are checked after that list-shape diagnostic.
1817
+ if (tail.type === VAR) throw new PrologError('instantiation_error');
1818
+ if (tail.type !== ATOM || tail.name !== '[]') throw new PrologError('type_error(list)', whole);
1819
+ if (items.some((item) => item.type === VAR)) throw new PrologError('instantiation_error');
1776
1820
  if (kind === 'chars') {
1777
1821
  const invalid = items.find((item) => !oneChar(item));
1778
1822
  if (invalid) throw new PrologError('type_error(character)', invalid);
@@ -1968,7 +2012,14 @@ function numberListText(list, env, kind, valueIsBound, solver = null) {
1968
2012
  const whole = deref(list, env);
1969
2013
  const { items, tail } = listElements(list, env);
1970
2014
  const proper = tail.type === ATOM && tail.name === '[]';
2015
+ // Corrigendum 2 orders conversion-mode instantiation errors specially:
2016
+ // a partial list is diagnosed before its fixed prefix, while an improper
2017
+ // non-list is diagnosed before a variable element in that prefix.
2018
+ if (!valueIsBound && tail.type === VAR) throw new PrologError('instantiation_error');
1971
2019
  if (tail.type !== VAR && !proper) throw new PrologError('type_error(list)', whole);
2020
+ if (!valueIsBound && items.some((item) => item.type === VAR)) {
2021
+ throw new PrologError('instantiation_error');
2022
+ }
1972
2023
 
1973
2024
  const invalid = items.find((item) => item.type !== VAR &&
1974
2025
  (kind === 'chars' ? !oneChar(item) : !validCharacterCode(item, solver)));
@@ -1981,7 +2032,6 @@ function numberListText(list, env, kind, valueIsBound, solver = null) {
1981
2032
  }
1982
2033
 
1983
2034
  const hasVariable = tail.type === VAR || items.some((item) => item.type === VAR);
1984
- if (!valueIsBound && hasVariable) throw new PrologError('instantiation_error');
1985
2035
  if (hasVariable) return null;
1986
2036
  return items.map((item) => kind === 'chars'
1987
2037
  ? item.name
@@ -33,15 +33,15 @@ error-ordering alternative to an individual executable assertion.
33
33
  | Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete vendored WG17 syntax matrix, `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, quoted-layout/escape error cases, writer/read-back regressions, and strict ASCII PCS/collation boundary tests. The implementation-defined 6.5/6.6 character-model decisions are now closed; wider shall-by-shall lexical mapping remains open. |
34
34
  | 7.1-7.3 — term types, term order, unification | audit | Standard-order, identity, finite-tree and occurs-check suites, Corrigendum 2 term predicates, plus strict checks for the required `variable < float < integer < atom < compound` type order, PCS-based atom collation, and the finite `max_arity` representation boundary used by term construction. |
35
35
  | 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Preparation-time `char_conversion/2` affects later unquoted source text and respects `char_conversion=off`. Strict preparation now enforces declaration-before-clause ordering for `dynamic/1`, `multifile/1`, and `discontiguous/1`, cross-text `multifile/1`, discontiguous clause grouping, empty declared procedures, include textual-replacement behavior, and one-time initialization per prepared program. The remaining shall-by-shall mapping is still open. |
36
- | 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access; focused strict checks now cover `current_predicate/1`, `clause/2`, `asserta/1`, `assertz/1`, `retract/1`, Corrigendum 2 `retractall/1`, and `abolish/1` errors plus empty-procedure lifetime. The remaining clause-conversion shall-by-shall mapping is still open. |
36
+ | 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access; focused strict checks now cover `current_predicate/1`, `clause/2`, `asserta/1`, `assertz/1`, `retract/1`, Corrigendum 2 `retractall/1`, and `abolish/1` errors plus empty-procedure lifetime, including runtime protection of the solver-native conjunction control construct `','/2`. The remaining clause-conversion shall-by-shall mapping is still open. |
37
37
  | 7.7 — execution and backtracking | audit | Control/search suites. Strict mode disables EyeProlog automatic tabling, cycle guards, and recursive numeric shortcuts so core execution uses ordinary clause selection/backtracking. |
38
38
  | 7.8 — control constructs and exceptions | audit | call, cut, conjunction, disjunction (including failed branches after callee-local cuts), if-then, catch/throw, renamed-copy tests. |
39
- | 7.9 — expression evaluation | audit | Arithmetic/evaluation/error suites, including Corrigenda. Exceptional-value/error-precedence matrix remains to be exhaustively enumerated. |
40
- | 7.10 — input/output concepts | audit | Stream, character/byte I/O, read/write options, operator-sensitive write-back, and Corrigendum 3 writer cases. The audit now also covers write-mode creation/truncation, append creation, repositioned overwrite, flushing, EOF actions, close/current-stream handling, strict stream-position terms, stream-property validation, stream-term requirements, and text-vs-binary permission errors. The full option/mode cross-product is still being mapped. |
39
+ | 7.9 — expression evaluation | audit | Arithmetic/evaluation/error suites, including Corrigenda. Strict mode now also pins direct-variable precedence, arithmetic operand type errors, float-only rounding conversions, zero-to-negative-power undefined errors, and the Part 1 mixed integer/float comparison conversion rule. Exceptional-value/error-precedence mapping remains to be exhaustively enumerated. |
40
+ | 7.10 — input/output concepts | audit | Stream, character/byte I/O, read/write options, operator-sensitive write-back, and Corrigendum 3 writer cases. The audit now also covers write-mode creation/truncation, append creation, repositioned overwrite, flushing, EOF actions, close/current-stream handling, strict stream-position terms, stream-property validation, stream-term requirements, text-vs-binary permission errors, and the prescribed `alias(A)` culprit for `open/4` alias collisions. The full option/mode cross-product is still being mapped. |
41
41
  | 7.11 — flags | covered | The complete Part 1 flag set, selected defaults, standard value domains, changeability, `current_prolog_flag/2`, and `set_prolog_flag/2` error behavior have dedicated strict tests. EyeProlog selects `bounded=false` and `integer_rounding_function=toward_zero`; valid alternative values of those fixed flags reach `permission_error(modify,flag,...)`, while `max_integer` and `min_integer` have no current value. Strict mode excludes the EyeProlog `occurs_check` extension. |
42
- | 7.12 — errors | audit | ISO `error(Error, Context)` envelope, type/domain/permission/representation/evaluation/syntax families and focused error cases. Exact ISO 8.14.1-8.14.4 error precedence is covered for `read_term/3`, `write_term/3`, `op/3`, and `current_op/3`; the continuing audit has corrected additional 8.11-8.13 stream/character/byte errors, Corrigendum 2 `keysort/2` errors, and arithmetic culprit reporting. A complete one-row-per-prescribed-error ordering map remains open. |
43
- | 8.2-8.17 — built-in predicates | audit | Predicate-family coverage is mapped in ISO-MATRIX.md; Corrigendum 2 additions (`subsumes_term/2`, `term_variables/2`, `call/2..8`, `false/0`) are in the strict registry. The audit now includes corrected `keysort/2` variable/non-pair errors, stricter 8.11-8.13 stream and character/byte modes/errors, the 8.14 option/error-order subfamily, and closed 8.17 flags. The remaining mode/error matrix is not yet one-row-per-standard-row. |
44
- | Clause 9 — evaluable functors | audit | Integer/float/rounding/transcendental/bitwise suites and corrigendum cases. Strict mode excludes the EyeProlog-only evaluable atom `e`, retains Corrigendum 2 arithmetic additions, and reports unknown zero-arity evaluables with the required `F/0` culprit shape. Host floating-point representation choices remain documented implementation-defined behavior; exhaustive exceptional-value precedence remains open. |
42
+ | 7.12 — errors | audit | ISO `error(Error, Context)` envelope, type/domain/permission/representation/evaluation/syntax families and focused error cases. Exact ISO 8.14.1-8.14.4 error precedence is covered for `read_term/3`, `write_term/3`, `op/3`, and `current_op/3`; the continuing audit has corrected additional 8.11-8.13 stream/character/byte errors (including the complete `alias(A)` culprit for `open/4` alias collisions), Corrigendum 2 `keysort/2` errors, atomic-conversion list-shape precedence, `arg/3`, `atom_concat/3`, `sub_atom/5`, `char_conversion/2`, runtime control-construct database protection, and arithmetic culprit/precedence reporting. A complete one-row-per-prescribed-error ordering map remains open. |
43
+ | 8.2-8.17 — built-in predicates | audit | Predicate-family coverage is mapped in ISO-MATRIX.md; Corrigendum 2 additions (`subsumes_term/2`, `term_variables/2`, `call/2..8`, `false/0`) are in the strict registry. The audit now includes corrected `keysort/2` variable/non-pair errors, stricter 8.11-8.13 stream and character/byte modes/errors, the 8.14 option/error-order subfamily, closed 8.17 flags, and additional prescribed precedence for `arg/3`, `atom_concat/3`, `sub_atom/5`, number/atom list conversions, and `char_conversion/2`. The remaining mode/error matrix is not yet one-row-per-standard-row. |
44
+ | Clause 9 — evaluable functors | audit | Integer/float/rounding/transcendental/bitwise suites and corrigendum cases. Strict mode excludes the EyeProlog-only evaluable atom `e`, retains Corrigendum 2 arithmetic additions, reports unknown zero-arity evaluables with the required `F/0` culprit shape, enforces the Part 1 numeric/integer operand errors and float-only rounding modes, and uses the Part 1 integer-to-float conversion rule for mixed arithmetic comparisons. Normal mode retains EyeProlog's exact mixed-type comparison extension. Host floating-point representation choices remain documented implementation-defined behavior; exhaustive exceptional-value precedence remains open. |
45
45
  | Corrigendum 1 | covered | Double-quoted atom/operator-priority corrections have dedicated cases. |
46
46
  | Corrigendum 2 | covered | Added predicates/functors, catch corrections, bar/operator and uninstantiation corrections have dedicated cases. |
47
47
  | Corrigendum 3 | covered | Writer options, `variable_names/1`, canonical list output and negative-power corrections have dedicated cases. |
@@ -119,6 +119,22 @@ argument as the culprit of `type_error(list, List)`, as required by the
119
119
  Corrigendum 2 replacement error clauses. Dedicated strict-core regressions pin
120
120
  both behaviors.
121
121
 
122
+
123
+ The following Part 1 pass tightens more of the still-open prescribed-error and
124
+ arithmetic matrix. `arg/3`, `atom_concat/3`, `sub_atom/5`, `number_chars/2`,
125
+ `number_codes/2`, and `char_conversion/2` now follow the standard ordering when
126
+ multiple error conditions are simultaneously present. Corrigendum 2 partial
127
+ versus improper list distinctions are applied consistently to atomic and
128
+ number conversion. Clause 9 strict evaluation now gives the prescribed
129
+ number/integer operand diagnostics, checks direct-variable instantiation first,
130
+ enforces the float-only input modes of `floor/1`, `truncate/1`, `round/1`, and
131
+ `ceiling/1`, and reports zero raised to a negative power as undefined. Finally,
132
+ strict mixed integer/float arithmetic comparisons perform the Part 1
133
+ integer-to-float conversion (including `float_overflow`); normal EyeProlog keeps
134
+ its exact cross-type comparison as an extension. Dedicated strict regressions
135
+ cover these distinctions without changing the normal-profile arithmetic error
136
+ contract.
137
+
122
138
  ## Strict-core boundary
123
139
 
124
140
  `isoStrict: true` is intentionally a **Part 1 + Corrigenda 1-3** mode. It does
@@ -11,14 +11,14 @@ compliance audit and the remaining work before a full conformance claim.
11
11
  | Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments, strict ASCII PCS/collation | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, `wg17_syntax_high_risk`, `wg17_invalid_octal_escape`, `wg17_unterminated_quoted_token`, `wg17_literal_newline_in_quote`, `wg17_non_iso_escape`, strict PCS/collation tests in `run-iso-strict.mjs`, syntax error cases |
12
12
  | Clause 7 term order and unification | finite-tree unification, identity, standard order, errors | `unification_control_information`, `swipl_occurs_check`, `term_modes_and_ordering`, `logtalk_compare_standard_order` |
13
13
  | Clause 7 control and exceptions | call, cut, conjunction, disjunction, if-then-else, catch and throw | `cut_control`, `control_and_terms`, `exceptions_and_flags`, `corrigenda_catch_callability`, `throw_copies_ball` |
14
- | 8.2-8.5 term predicates | unification, Corrigendum 2 tests, comparison, sorting, creation and decomposition | `corrigenda_term_predicates`, `corrigenda_sort_keysort`, `logtalk_arg_unification`, `logtalk_univ`, associated error cases, Corrigendum 2 `keysort/2` variable/non-pair errors, and strict `functor/3` / `=../2` error-order plus max-arity checks |
14
+ | 8.2-8.5 term predicates | unification, Corrigendum 2 tests, comparison, sorting, creation and decomposition | `corrigenda_term_predicates`, `corrigenda_sort_keysort`, `logtalk_arg_unification`, `logtalk_univ`, associated error cases, Corrigendum 2 `keysort/2` variable/non-pair errors, and strict `arg/3`, `functor/3` / `=../2` error-order plus max-arity checks |
15
15
  | 8.6-8.7 arithmetic predicates | `is/2` and all six arithmetic comparisons | `arithmetic`, comparison cases, isolated evaluation errors |
16
- | 8.8-8.10 database and solutions | logical update view, dynamic mutation, all-solutions grouping | `dynamic_database`, `trealla_logical_update_view`, `corrigenda_retractall`, `grouped_solutions_and_clauses`, plus strict clause/database error and all-solutions precedence checks |
17
- | 8.11-8.14 streams and term I/O | text/binary streams, properties, units, read/write options and operators | `streams_and_term_io`, `operators`, Corrigendum 3 option cases, stream error cases, strict 8.14.1-8.14.4 option/error-order tests, Corrigendum 3 variable metadata/write naming checks, and issue #65 checks for stream-term/property validation, text-vs-binary permissions, EOF/flush/position, and open/close semantics |
16
+ | 8.8-8.10 database and solutions | logical update view, dynamic mutation, all-solutions grouping | `dynamic_database`, `trealla_logical_update_view`, `corrigenda_retractall`, `grouped_solutions_and_clauses`, plus strict clause/database error and all-solutions precedence checks, including runtime static/private protection for the solver-native conjunction control construct |
17
+ | 8.11-8.14 streams and term I/O | text/binary streams, properties, units, read/write options and operators | `streams_and_term_io`, `operators`, Corrigendum 3 option cases, stream error cases, strict 8.14.1-8.14.4 option/error-order tests, Corrigendum 3 variable metadata/write naming checks, and issue #65 checks for stream-term/property validation, text-vs-binary permissions, EOF/flush/position, open/close semantics, and the exact `alias(A)` collision culprit |
18
18
  | 8.15 logic and control | negation, once, repeat, `call/2` through `call/8`, `false/0` | `logtalk_once`, `corrigenda_call_closure`, `false_builtin`, plus strict Corrigendum 2 resulting-goal `max_arity` error coverage |
19
- | 8.16 atomic processing | atoms, characters, codes and number conversion with prescribed errors | `atomic_term_processing`, focused forward/reverse cases, parenthesized-number rejection, Logtalk-derived error cases, and strict Corrigendum 2 improper-list culprit checks for `atom_chars/2` and `atom_codes/2` |
20
- | 8.17 flags and hooks | complete required Part 1 flag set, selected defaults, standard value domains/changeability, halt and character conversion | `exceptions_and_flags`, `remaining_builtins_and_directives`, flag error cases, and the strict complete-flag audit in `run-iso-strict.mjs` |
21
- | Clause 9 evaluable functors | integer, float, rounding, transcendental and bitwise operations | `arithmetic`, `corrigenda_arithmetic`, `corrigenda_atan2_zero`, `corrigenda_integer_negative_power`, strict exclusion of the normal-mode `e` evaluable extension and zero-arity unknown-evaluable error checks |
19
+ | 8.16 atomic processing | atoms, characters, codes and number conversion with prescribed errors | `atomic_term_processing`, focused forward/reverse cases, parenthesized-number rejection, Logtalk-derived error cases, strict Corrigendum 2 partial/improper-list precedence for atom/number conversions, and prescribed `atom_concat/3` / `sub_atom/5` ordering checks |
20
+ | 8.17 flags and hooks | complete required Part 1 flag set, selected defaults, standard value domains/changeability, halt and character conversion | `exceptions_and_flags`, `remaining_builtins_and_directives`, flag error cases, strict `char_conversion/2` precedence, and the complete strict flag audit in `run-iso-strict.mjs` |
21
+ | Clause 9 evaluable functors | integer, float, rounding, transcendental and bitwise operations | `arithmetic`, `corrigenda_arithmetic`, `corrigenda_atan2_zero`, `corrigenda_integer_negative_power`, strict exclusion of the normal-mode `e` evaluable extension, zero-arity unknown-evaluable checks, prescribed operand/type and negative-power errors, float-only rounding modes, and Part 1 mixed integer/float comparison conversion |
22
22
  | Module compatibility profile (related to ISO/IEC 13211-2 and later WG17 amendment work) | module declarations, exports, imports, qualification, meta-predicate context | `modules/qualified_call`, `modules/selective_library_import`, `dcg_module_nonterminal_indicator` |
23
23
  | Part 3-oriented DCG compatibility profile | `-->`, terminal and partial sequences, grammar control constructs, semicontexts, nonterminal indicators, modules, `phrase/2-3`, steadfastness and errors | `dcg_terminals_and_remainder`, `dcg_control_constructs`, `dcg_partial_sequences`, `dcg_phrase_steadfastness`, `dcg_dynamic_nonterminal_indicator`, `logtalk_dcg_phrase_identity`, `logtalk_dcg_semicontexts`, DCG error and precedence cases |
24
24
 
@@ -183,6 +183,15 @@ export function runIsoStrict(reporter = new TestReporter()) {
183
183
  includes(ignoredNonVariable.stdout, 'x', 'non-variable right side is permitted and ignored');
184
184
  });
185
185
 
186
+ reporter.test('reports the complete alias option in open/4 alias collisions', () => {
187
+ const error = capture(() => run('', {
188
+ isoStrict: true,
189
+ goal: 'open(dummy,write,_,[alias(user_input)])',
190
+ }));
191
+ equal(error.formal, 'permission_error(open, source_sink)', 'formal error');
192
+ includes(error.message, 'alias(user_input)', 'alias option culprit');
193
+ });
194
+
186
195
  reporter.test('follows the ISO 8.14.1.3 read_term/3 error order', () => {
187
196
  equal(capture(() => run('', { isoStrict: true, goal: 'read_term(f(a),_,[X])' })).formal,
188
197
  'instantiation_error', 'partial/variable option before stream domain');
@@ -243,6 +252,60 @@ export function runIsoStrict(reporter = new TestReporter()) {
243
252
  'type_error(atom)', 'operator type');
244
253
  });
245
254
 
255
+ reporter.test('follows remaining Part 1 built-in error precedence', () => {
256
+ const cases = [
257
+ ['arg(a,X,_)', 'instantiation_error', 'arg/3 second-argument instantiation before index type'],
258
+ ['atom_concat(1,X,Y)', 'instantiation_error', 'atom_concat/3 second/whole under-instantiation before first type'],
259
+ ['atom_concat(X,1,Y)', 'instantiation_error', 'atom_concat/3 first/whole under-instantiation before second type'],
260
+ ['sub_atom(a,-1,bad,_,_)', 'type_error(integer)', 'sub_atom/5 integer type before non-negative domain'],
261
+ ['number_chars(N,[1|T])', 'instantiation_error', 'number_chars/2 partial list before element type'],
262
+ ['number_chars(N,[X,1])', 'instantiation_error', 'number_chars/2 variable element before later element type'],
263
+ ['number_codes(N,[foo|T])', 'instantiation_error', 'number_codes/2 partial list before element type'],
264
+ ['number_codes(N,[X,foo])', 'instantiation_error', 'number_codes/2 variable element before later element type'],
265
+ ['atom_chars(A,[X|foo])', 'type_error(list)', 'atom_chars/2 improper list before prefix variable'],
266
+ ['atom_codes(A,[X|foo])', 'type_error(list)', 'atom_codes/2 improper list before prefix variable'],
267
+ ['char_conversion(foo,X)', 'instantiation_error', 'char_conversion/2 output instantiation before input representation'],
268
+ ];
269
+ for (const [goal, formal, label] of cases) {
270
+ equal(capture(() => run('', { isoStrict: true, goal })).formal, formal, label);
271
+ }
272
+ });
273
+
274
+ reporter.test('follows Part 1 arithmetic type and exceptional errors', () => {
275
+ const cases = [
276
+ ["X is '+'(foo,77)", 'type_error(number)', 'simple arithmetic atomic operand'],
277
+ ['X is mod(foo,77)', 'type_error(number)', 'integer arithmetic non-number operand'],
278
+ ['X is mod(7.5,2)', 'type_error(integer)', 'integer arithmetic numeric type'],
279
+ ['X is truncate(foo)', 'type_error(number)', 'rounding non-number operand'],
280
+ ['X is sin(foo)', 'type_error(number)', 'transcendental non-number operand'],
281
+ ['X is foo+Y', 'instantiation_error', 'direct variable before another operand error'],
282
+ ['X is floor(7)', 'type_error(float)', 'floor integer operand'],
283
+ ['X is truncate(7)', 'type_error(float)', 'truncate integer operand'],
284
+ ['X is round(7)', 'type_error(float)', 'round integer operand'],
285
+ ['X is ceiling(7)', 'type_error(float)', 'ceiling integer operand'],
286
+ ['X is 0 ** -1', 'evaluation_error(undefined)', 'power zero negative exponent'],
287
+ ['X is 0.0 ^ -1', 'evaluation_error(undefined)', 'Corrigendum 2 power zero negative exponent'],
288
+ ];
289
+ for (const [goal, formal, label] of cases) {
290
+ equal(capture(() => run('', { isoStrict: true, goal })).formal, formal, label);
291
+ }
292
+ });
293
+
294
+ reporter.test('uses the Part 1 mixed arithmetic comparison operations', () => {
295
+ // 8.7 converts the integer operand to float in mixed comparisons.
296
+ equal(run('', { isoStrict: true, goal: '18014398509481985 =:= 18014398509481984.0' }).stats.completed_goal_lists,
297
+ 1, 'mixed equality after float conversion');
298
+ equal(run('', { isoStrict: true, goal: '\\+ (18014398509481985 > 18014398509481984.0)' }).stats.completed_goal_lists,
299
+ 1, 'mixed ordering after float conversion');
300
+ const huge = `1${'0'.repeat(400)}`;
301
+ equal(capture(() => run('', { isoStrict: true, goal: `${huge} > 1.0` })).formal,
302
+ 'evaluation_error(float_overflow)', 'mixed integer-to-float overflow');
303
+
304
+ // The normal profile retains EyeProlog's exact cross-type extension.
305
+ equal(run('', { goal: '9007199254740993 > 9007199254740992.0' }).stats.completed_goal_lists,
306
+ 1, 'normal exact mixed ordering extension');
307
+ });
308
+
246
309
  reporter.test('follows ISO term-construction error types and precedence', () => {
247
310
  equal(capture(() => run('', { isoStrict: true, goal: 'functor(_,foo(a),1)' })).formal,
248
311
  'type_error(atomic)', 'functor compound name');
@@ -322,6 +385,21 @@ export function runIsoStrict(reporter = new TestReporter()) {
322
385
  equal(answers.length, 1, 'answer count');
323
386
  });
324
387
 
388
+ reporter.test('protects conjunction as an ISO static/private control construct at runtime', () => {
389
+ for (const goal of [
390
+ "asserta(','(a,b))",
391
+ "assertz(','(a,b))",
392
+ "retract(','(a,b))",
393
+ "retractall(','(a,b))",
394
+ "abolish('/'(',',2))",
395
+ ]) {
396
+ equal(capture(() => run('', { isoStrict: true, goal })).formal,
397
+ 'permission_error(modify, static_procedure)', goal);
398
+ }
399
+ equal(capture(() => run('', { isoStrict: true, goal: "clause(','(a,b),_)" })).formal,
400
+ 'permission_error(access, private_procedure)', 'clause/2 conjunction access');
401
+ });
402
+
325
403
  reporter.test('covers ISO database predicate errors and empty-procedure lifetime', () => {
326
404
  const errors = [
327
405
  ['current_predicate(4)', '', 'type_error(predicate_indicator)'],