eyeprolog 1.5.40 → 1.5.41

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.5.40",
6
+ "version": "1.5.41",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -75,6 +75,7 @@
75
75
  "conformance:update:neumerkel": "node test/run-neumerkel.mjs --update-report",
76
76
  "conformance:check:neumerkel": "node test/run-neumerkel.mjs --cached --verify-report",
77
77
  "conformance:sync:neumerkel": "node test/run-neumerkel.mjs --cached --update-report",
78
- "test:http-json": "node test/run-http-json.mjs"
78
+ "test:http-json": "node test/run-http-json.mjs",
79
+ "test:iso-part2-amendment": "node test/run-iso-part2-amendment.mjs"
79
80
  }
80
81
  }
package/src/parser.js CHANGED
@@ -112,7 +112,6 @@ const INFIX_OPERATORS = new Map([
112
112
  ['=<', { precedence: 501, associativity: 'none' }],
113
113
  ['>', { precedence: 501, associativity: 'none' }],
114
114
  ['>=', { precedence: 501, associativity: 'none' }],
115
- [':', { precedence: 601, associativity: 'right' }],
116
115
  ['+', { precedence: 701, associativity: 'left' }],
117
116
  ['-', { precedence: 701, associativity: 'left' }],
118
117
  ['/\\', { precedence: 701, associativity: 'left' }],
@@ -142,13 +141,21 @@ export const ISO_OPERATOR_DEFINITIONS = [
142
141
  [900, 'fy', '\\+'],
143
142
  ...['=', '=..', '\\=', '==', '\\==', '@<', '@=<', '@>', '@>=', 'is',
144
143
  '=:=', '=\\=', '<', '=<', '>', '>='].map((name) => [700, 'xfx', name]),
145
- [600, 'xfy', ':'],
146
144
  ...['+', '-', '/\\', '\\/'].map((name) => [500, 'yfx', name]),
147
145
  ...['*', '/', '//', 'div', 'mod', 'rem', '<<', '>>'].map((name) => [400, 'yfx', name]),
148
146
  [200, 'xfx', '**'], [200, 'xfy', '^'],
149
147
  [200, 'fy', '+'], [200, 'fy', '-'], [200, 'fy', '\\'],
150
148
  ];
151
149
 
150
+ // ISO/IEC 13211-2 adds module qualification to the Part 1 operator table.
151
+ // The 2013 amendment also writes meta_predicate/1 in directive operator form,
152
+ // so normal module mode accepts that standard spelling while strict Part 1
153
+ // keeps both additions out of its initial operator table.
154
+ export const PART2_OPERATOR_DEFINITIONS = [
155
+ [600, 'xfy', ':'],
156
+ [1150, 'fx', 'meta_predicate'],
157
+ ];
158
+
152
159
  // The alternative operator belongs to the Part 3 grammar-rule profile. Part 1
153
160
  // reserves `|` as list punctuation but permits a program to declare it as an
154
161
  // infix operator at priority 1001 or greater (Corrigendum 2).
@@ -226,7 +233,7 @@ export function createParserOperatorState(definitions = [], includeDefaults = tr
226
233
  postfixOperators: new Map(),
227
234
  };
228
235
  if (includeDefaults && options.isoStrict !== true) {
229
- for (const [priority, specifier, name] of [...PART3_OPERATOR_DEFINITIONS, ...QUAD_OPERATOR_DEFINITIONS]) {
236
+ for (const [priority, specifier, name] of [...PART2_OPERATOR_DEFINITIONS, ...PART3_OPERATOR_DEFINITIONS, ...QUAD_OPERATOR_DEFINITIONS]) {
230
237
  defineParserOperator(state, priority, specifier, name);
231
238
  }
232
239
  }
package/src/program.js CHANGED
@@ -4,6 +4,7 @@ import { ATOM, COMPOUND, NUMBER, STRING, VAR, Env, atom, compound, deref, flatte
4
4
  import { formatTermForWrite } from './write.js';
5
5
  import {
6
6
  ISO_OPERATOR_DEFINITIONS,
7
+ PART2_OPERATOR_DEFINITIONS,
7
8
  PART3_OPERATOR_DEFINITIONS,
8
9
  QUAD_OPERATOR_DEFINITIONS,
9
10
  createParserOperatorState,
@@ -41,6 +42,7 @@ const PROGRAM_BUILD_BATCH_SIZE = 16384;
41
42
  const preparedBundledLibraryCache = new Map();
42
43
  const NORMAL_OPERATOR_DEFINITIONS = [
43
44
  ...ISO_OPERATOR_DEFINITIONS,
45
+ ...PART2_OPERATOR_DEFINITIONS,
44
46
  ...PART3_OPERATOR_DEFINITIONS,
45
47
  ...QUAD_OPERATOR_DEFINITIONS,
46
48
  ];
@@ -206,7 +208,7 @@ export class Program {
206
208
  name,
207
209
  arity,
208
210
  module,
209
- metaArgumentPositions: this.moduleMetaPredicates.get(module)?.get(`${name}/${arity}`) ?? [],
211
+ metaArgumentModes: this.moduleMetaPredicates.get(module)?.get(`${name}/${arity}`) ?? [],
210
212
  clauses: [],
211
213
  argIndexes: Array.from({ length: arity }, makeArgumentIndex),
212
214
  demandIndexes: new Map(),
@@ -303,17 +305,23 @@ export class Program {
303
305
  }
304
306
  defineMetaPredicate(module, template) {
305
307
  if (template.type !== COMPOUND) return;
306
- const positions = [];
308
+ const modes = [];
307
309
  for (let index = 0; index < template.args.length; index++) {
308
310
  const spec = template.args[index];
309
- if ((spec.type === 'number' && RE_DECIMAL_INT.test(spec.name)) ||
310
- (spec.type === ATOM && spec.name === ':')) positions.push(index);
311
+ if (spec.type === ATOM && spec.name === ':') {
312
+ modes.push({ index, kind: 'context' });
313
+ } else if (spec.type === NUMBER && RE_DECIMAL_INT.test(spec.name)) {
314
+ // Numeric closure modes are a widely implemented compatibility
315
+ // extension. Keep their existing hidden lexical qualification while
316
+ // reserving explicit Module:Goal wrapping for ISO Part 2 ':' modes.
317
+ modes.push({ index, kind: 'closure' });
318
+ }
311
319
  }
312
320
  const definitions = this.moduleMetaPredicates.get(module) ?? new Map();
313
- definitions.set(`${template.name}/${template.arity}`, positions);
321
+ definitions.set(`${template.name}/${template.arity}`, modes);
314
322
  this.moduleMetaPredicates.set(module, definitions);
315
323
  const group = this.groups.get(modulePredicateKey(module, template.name, template.arity));
316
- if (group) group.metaArgumentPositions = positions;
324
+ if (group) group.metaArgumentModes = modes;
317
325
  }
318
326
  ensureDynamicGroup(name, arity, module = 'user') {
319
327
  assertPredicateIsDefinable(name, arity, this.strictIso);
@@ -1401,6 +1409,12 @@ function sourcePath(options) {
1401
1409
  function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules, fast, context) {
1402
1410
  const batch = [];
1403
1411
  const deferredGoalExpansions = [];
1412
+ // ISO/IEC 13211-2 amendment (2013), 6.2.5: a module body is one
1413
+ // Prolog text whose first term is module/2 and whose body continues to the
1414
+ // end of that text. Track prepared source terms per load so a later module/2
1415
+ // cannot silently switch the owning module mid-text. A recursive load gets
1416
+ // its own counter, so a distinct Prolog text may begin a distinct module.
1417
+ let sourceTermCount = 0;
1404
1418
  const preparedCacheKey = preparedBundledLibraryCacheKey(builder.program, options);
1405
1419
  const preparedForCache = preparedCacheKey == null ? null : [];
1406
1420
  const flush = () => {
@@ -1436,6 +1450,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
1436
1450
  };
1437
1451
 
1438
1452
  const acceptPreparedClause = (clause, lexicalModule, record = true) => {
1453
+ const sourceTermIndex = sourceTermCount++;
1439
1454
  const remember = () => {
1440
1455
  if (record && preparedForCache != null) {
1441
1456
  preparedForCache.push({ clause: cachedClauseCopy(clause), lexicalModule });
@@ -1444,6 +1459,9 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
1444
1459
 
1445
1460
  const moduleDeclaration = moduleDirective(clause);
1446
1461
  if (moduleDeclaration) {
1462
+ if (sourceTermIndex !== 0) {
1463
+ throw new PrologError('permission_error(redefine, module)', atom(moduleDeclaration.name));
1464
+ }
1447
1465
  flush();
1448
1466
  clause.module = moduleDeclaration.name;
1449
1467
  clause.textUnit = context.textUnit;
@@ -1507,7 +1525,27 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
1507
1525
  builder.lastPredicateByText.set(context.textUnit ?? '<input>', '@directive');
1508
1526
  }
1509
1527
  const child = readIncludedSource(include, options, ensured);
1510
- if (!child) return;
1528
+ if (include.name === 'ensure_loaded') {
1529
+ const declaration = sourceModuleDeclaration(child.text, child.options);
1530
+ if (declaration) {
1531
+ // ISO/IEC 13211-2 amendment (2013), 6.2.5.6 notes that the public
1532
+ // predicates of a module loaded with ensure_loaded/1 are imported in
1533
+ // the same sense as use_module/1. Loading remains idempotent, but each
1534
+ // importing module still receives its own import mapping.
1535
+ if (!loadedModules.has(declaration.name)) {
1536
+ const childContext = {
1537
+ module: declaration.name,
1538
+ textUnit: sourcePath(child.options) ?? context.textUnit,
1539
+ };
1540
+ if (!loadSourceIntoBuilder(builder, child.text, child.options, ensured, loadedModules, fast, childContext)) {
1541
+ throw FAST_PARSE_ABORT;
1542
+ }
1543
+ }
1544
+ builder.program.importModule(context.module, declaration.name, null);
1545
+ return;
1546
+ }
1547
+ if (child.alreadyLoaded) return;
1548
+ }
1511
1549
  const childContext = include.name === 'include'
1512
1550
  ? context
1513
1551
  : { module: context.module, textUnit: sourcePath(child.options) ?? context.textUnit };
@@ -1530,7 +1568,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
1530
1568
  // while unqualified calls in the body retain the lexical module of the
1531
1569
  // source text. This distinction is essential for conventional hooks such
1532
1570
  // as user:term_expansion/2 defined from library modules.
1533
- clause = normalizeQualifiedClauseHead(clause, lexicalModule);
1571
+ if (!builder.program.strictIso) clause = normalizeQualifiedClauseHead(clause, lexicalModule);
1534
1572
 
1535
1573
  if (!builder.program.strictIso && !isDirectiveClause(clause)) {
1536
1574
  try {
@@ -1674,6 +1712,11 @@ function moduleExportIndicators(term) {
1674
1712
  return indicators;
1675
1713
  }
1676
1714
 
1715
+ function sourceModuleDeclaration(text, options = {}) {
1716
+ const clauses = parseClauses(text, { ...options, sourceMetadata: false });
1717
+ return clauses.length > 0 ? moduleDirective(clauses[0]) : null;
1718
+ }
1719
+
1677
1720
  function readModuleSource(designation, options) {
1678
1721
  if (designation.type === COMPOUND && designation.name === 'library' && designation.arity === 1 &&
1679
1722
  designation.args[0].type === ATOM) {
@@ -1696,7 +1739,7 @@ function readModuleSource(designation, options) {
1696
1739
  } catch (_) {
1697
1740
  throw new PrologError('existence_error(source_sink)', designation);
1698
1741
  }
1699
- const declaration = parseClauses(text, { filename, sourceMetadata: false }).map(moduleDirective).find(Boolean);
1742
+ const declaration = sourceModuleDeclaration(text, { filename });
1700
1743
  if (!declaration) throw new PrologError('existence_error(module)', designation);
1701
1744
  return { name: declaration.name, text, options: { ...options, filename, baseDir: path.dirname(filename) } };
1702
1745
  }
@@ -1727,8 +1770,8 @@ function readIncludedSource(directive, options, ensured) {
1727
1770
  : currentWorkingDirectory()
1728
1771
  );
1729
1772
  const filename = path.resolve(base, designation.name);
1730
- if (directive.name === 'ensure_loaded' && ensured.has(filename)) return null;
1731
- if (directive.name === 'ensure_loaded') ensured.add(filename);
1773
+ const alreadyLoaded = directive.name === 'ensure_loaded' && ensured.has(filename);
1774
+ if (directive.name === 'ensure_loaded' && !alreadyLoaded) ensured.add(filename);
1732
1775
 
1733
1776
  let text;
1734
1777
  try {
@@ -1739,6 +1782,7 @@ function readIncludedSource(directive, options, ensured) {
1739
1782
  return {
1740
1783
  text,
1741
1784
  options: { ...options, filename, baseDir: path.dirname(filename) },
1785
+ alreadyLoaded,
1742
1786
  };
1743
1787
  }
1744
1788
 
package/src/solver.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Depth-first EyeProlog solver with builtin dispatch, memoization, and guarded recursion handling.
2
2
  // Most semantic decisions still flow through unification; optimizations only select candidates earlier.
3
3
  import {
4
- ATOM, COMPOUND, NUMBER, STRING, VAR, Env, Term, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
4
+ ATOM, COMPOUND, NUMBER, STRING, VAR, Env, Term, atom, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
5
5
  flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList, isScalar,
6
6
  numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
7
7
  } from './term.js';
@@ -708,7 +708,7 @@ export class Solver {
708
708
  depth++;
709
709
  continue;
710
710
  }
711
- if (goal.type === COMPOUND && goal.name === ':' && goal.arity === 2) {
711
+ if (!this.isoStrict && goal.type === COMPOUND && goal.name === ':' && goal.arity === 2) {
712
712
  const module = deref(goal.args[0], env);
713
713
  if (module.type === 'var') throw new PrologError('instantiation_error');
714
714
  if (module.type !== 'atom') throw new PrologError('type_error(atom)', module);
@@ -1224,9 +1224,21 @@ function normalizeHostResourceError(error) {
1224
1224
 
1225
1225
  function qualifyMetaArguments(goal, group) {
1226
1226
  const callerModule = goal.module ?? 'user';
1227
- for (const index of group.metaArgumentPositions ?? []) {
1228
- const argument = goal.args[index];
1229
- if (argument && (argument.type === COMPOUND || argument.type === 'atom')) {
1227
+ for (const mode of group.metaArgumentModes ?? []) {
1228
+ const argument = goal.args[mode.index];
1229
+ if (argument == null) continue;
1230
+ if (mode.kind === 'context') {
1231
+ // ISO/IEC 13211-2 amendment (2013), 6.2.5.7: ':' arguments are
1232
+ // visibly prefixed with the current source/calling module. Preserve an
1233
+ // already explicit qualification, otherwise wrap even a variable so a
1234
+ // later binding retains the caller context and Module:Goal inspection
1235
+ // observes the standardized term shape.
1236
+ if (!(argument.type === COMPOUND && argument.name === ':' && argument.arity === 2)) {
1237
+ goal.args[mode.index] = compound(':', [atom(callerModule), argument]);
1238
+ }
1239
+ continue;
1240
+ }
1241
+ if (mode.kind === 'closure' && (argument.type === COMPOUND || argument.type === ATOM)) {
1230
1242
  qualifyTerm(argument, callerModule);
1231
1243
  }
1232
1244
  }
@@ -131,7 +131,9 @@ features overlap standardized Part 2 and Part 3 facilities. They are extensions
131
131
  relative to the Part 1 strict-core boundary and are tested separately; this
132
132
  ledger does not assert complete Part 2 or Part 3 conformance. The concrete
133
133
  compatibility boundary, including the normal-profile `phrase/2-3` terminal-sequence
134
- error choice, is recorded in `ISO-PART2-PART3-SCOPE.md`.
134
+ error choice, is recorded in `ISO-PART2-PART3-SCOPE.md`. The requirements
135
+ clarified by the 2013 Part 2 amendment are tracked separately in the executable
136
+ `ISO-PART2-AMENDMENT-2013.md` ledger.
135
137
 
136
138
  ## Important implementation-dependent behavior (not the 5.4 mandatory table)
137
139
 
@@ -0,0 +1,36 @@
1
+ # ISO/IEC 13211-2 module amendment (2013) coverage
2
+
3
+ This ledger maps EyeProlog's normal module profile to the requirements clarified
4
+ by the attached 2013 ISO/IEC 13211-2 amendment draft (WG17 N251). It is narrowly
5
+ scoped to that amendment. It does **not** turn the broader ISO/IEC 13211-2:2000
6
+ base document into a release-facing certification claim; unchanged Part 2
7
+ facilities outside the amendment still need their own clause-by-clause audit.
8
+
9
+ Executable evidence lives in `test/run-iso-part2-amendment.mjs` and is included
10
+ in both `npm test` and the conformance aggregate.
11
+
12
+ | Amendment clause | Required behavior | EyeProlog behavior / evidence |
13
+ | --- | --- | --- |
14
+ | 6.2.4.1 | A `module(Name, Exports)` directive identifies a named module and its public predicate indicators. | `Program.defineModule()` records the named module and export map. The focused suite checks that public predicates import and private predicates remain module-local. |
15
+ | 6.2.5 | A module body is a Prolog text beginning with its `module/2` directive and extending to the end of that text. | Files designated by `use_module/1-2` are accepted as module sources only when their first read term is `module/2`. A second `module/2` later in the same Prolog text is rejected rather than switching modules mid-text, while a distinct Prolog text may begin a distinct module body. |
16
+ | 6.2.5.5 | `use_module(F, L)` selectively imports the predicates in `L` from the exports of the module defined by `F`. | `Program.importModule()` validates requested indicators against the source module's export map and installs only those imports. The focused suite verifies selective import. |
17
+ | 6.2.5.6 | `use_module(F)` imports all exported predicates; the amendment also aligns the public-predicate effect of `ensure_loaded(F)` for a module source. | `use_module/1` imports the full export map. `ensure_loaded/1` now imports a module's public predicates while retaining idempotent source loading, including when the same module was already loaded for another caller. |
18
+ | 6.2.5.7 | `meta_predicate/1` marks context-sensitive arguments; `:` arguments carry the current source/calling module. | Normal parsing accepts the amendment spelling `:- meta_predicate p(:).`. Colon-mode arguments are represented as an observable `Module:Goal` term, including when the argument is a variable. Numeric closure modes remain a separate compatibility extension. |
19
+ | 6.4.4.3 | Imported metapredicates preserve the caller's module context for their meta-arguments. | The focused suite verifies both visible `Module:Goal` decomposition and execution of a caller-private predicate through a variable meta-argument. Explicit `Module:Goal` calls continue to set their stated module context. |
20
+
21
+ ## Operator boundary
22
+
23
+ ISO/IEC 13211-2 adds `:` as the module-qualification operator relative to the
24
+ Part 1 initial operator table. EyeProlog therefore predeclares `:` only in its
25
+ normal module profile; `--iso-strict` no longer includes it in the Part 1
26
+ initial table. Normal mode also predeclares `meta_predicate` as a directive
27
+ operator so the amendment's source spelling is accepted, while the parenthesized
28
+ `meta_predicate(...)` compatibility spelling remains valid.
29
+
30
+ ## Remaining Part 2 scope
31
+
32
+ The amendment coverage above is executable and release-gated. EyeProlog still
33
+ describes the overall Part 2 surface as a compatibility profile until the
34
+ unchanged portions of ISO/IEC 13211-2:2000 - including its broader module
35
+ interface and re-export model - have a complete processor/semantics ledger.
36
+ See `ISO-PART2-PART3-SCOPE.md` for that release boundary.
@@ -13,12 +13,20 @@ Normal mode implements a procedure-oriented module compatibility layer:
13
13
  qualification, exports/imports, nonterminal indicators, and module-aware meta
14
14
  calls are covered by the regression and conformance corpora.
15
15
 
16
- This overlap is intentionally described as a compatibility profile rather than
17
- a complete Part 2 conformance claim. In particular, the Part 1 strict registry
18
- does not enable module directives, and the project does not infer full Part 2
19
- coverage from interoperability with Scryer, Trealla, or Logtalk. A future Part 2
20
- claim would require a clause-by-clause Part 2 processor and module-semantics
21
- ledger comparable to the existing Part 1 matrices.
16
+ The requirements clarified by the 2013 ISO/IEC 13211-2 module amendment draft
17
+ (WG17 N251) now have a dedicated executable ledger in
18
+ `ISO-PART2-AMENDMENT-2013.md`: module/2 exports, selective and full imports, the
19
+ module-source behavior of ensure_loaded/1, the amendment's meta_predicate
20
+ directive spelling, and visible caller-module qualification of `:`
21
+ meta-arguments. The focused runner is part of the release gate.
22
+
23
+ This amendment coverage is intentionally narrower than a complete Part 2
24
+ conformance claim. In particular, the Part 1 strict registry does not enable
25
+ module directives or the Part 2 `:` operator, and the project does not infer
26
+ full Part 2 coverage from interoperability with Scryer, Trealla, or Logtalk. A
27
+ future complete Part 2 claim would require a clause-by-clause audit of the
28
+ unchanged ISO/IEC 13211-2:2000 module-interface and re-export facilities as well
29
+ as the amendment.
22
30
 
23
31
  ## Part 3 definite clause grammars
24
32
 
@@ -45,7 +53,7 @@ rather than silently changing the compatibility profile.
45
53
  ## Release boundary
46
54
 
47
55
  - Part 1 + Corrigenda 1-3: release-facing strict-core conformance target.
48
- - Part 2: normal-mode compatibility profile, tested but not certified complete.
56
+ - Part 2: 2013 amendment requirements release-gated; broader Part 2 remains a normal-mode compatibility profile, not a complete certification.
49
57
  - Part 3: normal-mode compatibility profile, tested but not certified complete.
50
58
 
51
59
  This separation keeps Part 1 conformance evidence independent of useful module
@@ -16,8 +16,10 @@ closes 7.9/Clause 9, and [ISO-PROCESSOR-REQUIREMENTS.md](ISO-PROCESSOR-REQUIREME
16
16
  decomposes the Clause 5 processor obligations.
17
17
  [ISO-CORRIGENDA-MATRIX.md](ISO-CORRIGENDA-MATRIX.md) gives every published
18
18
  Corrigenda amendment cluster an executable, editorial, or superseded
19
- disposition. [ISO-PART2-PART3-SCOPE.md](ISO-PART2-PART3-SCOPE.md) records the
20
- separate normal-profile module/DCG compatibility boundary and known non-claims. Built-in rows may group closely related conditions only when the
19
+ disposition. [ISO-PART2-AMENDMENT-2013.md](ISO-PART2-AMENDMENT-2013.md) maps the
20
+ 2013 module-amendment requirements to executable evidence, while
21
+ [ISO-PART2-PART3-SCOPE.md](ISO-PART2-PART3-SCOPE.md) records the broader
22
+ normal-profile module/DCG compatibility boundary and known non-claims. Built-in rows may group closely related conditions only when the
21
23
  row names every grouped condition and its executable evidence.
22
24
  The exit checklist is embedded in [ISO-COMPLIANCE.md](ISO-COMPLIANCE.md). [WG17-SYNTAX-STATUS.md](WG17-SYNTAX-STATUS.md) records the
23
25
  complete one-to-one trace for the vendored active upstream WG17 syntax cases.
@@ -92,6 +94,7 @@ npm run test:neumerkel # the seven live upstream suites only
92
94
  npm run test:neumerkel:cached # exact last fetched bytes; reproduction only
93
95
  npm run conformance:check:neumerkel # verify tracked report against last successful live snapshot
94
96
  npm run test:iso # Part 1 + Corrigenda strict-core processor gate
97
+ npm run test:iso-part2-amendment # 2013 Part 2 amendment module requirements
95
98
  npm run test:wg17 # vendored reviewed WG17 syntax regression
96
99
  ```
97
100
 
package/test/run-all.mjs CHANGED
@@ -6,6 +6,7 @@ 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';
9
+ import { runIsoPart2Amendment } from './run-iso-part2-amendment.mjs';
9
10
  import { runPlayground } from './run-playground.mjs';
10
11
  import { runExamples } from './run-examples.mjs';
11
12
  import { runBookExamples } from './run-book-examples.mjs';
@@ -24,6 +25,7 @@ await runStandalone(async (reporter) => {
24
25
  if (!offline) await runNeumerkel(reporter);
25
26
  runConformance(reporter);
26
27
  runIsoStrict(reporter);
28
+ runIsoPart2Amendment(reporter);
27
29
  runWg17(reporter);
28
30
  runOpenRuleBenchChecks(reporter);
29
31
  runArchitecture(reporter);
@@ -4,6 +4,7 @@ import { runStandalone } from './test-style.mjs';
4
4
  import { runNeumerkel } from './run-neumerkel.mjs';
5
5
  import { runConformance } from './run-conformance.mjs';
6
6
  import { runIsoStrict } from './run-iso-strict.mjs';
7
+ import { runIsoPart2Amendment } from './run-iso-part2-amendment.mjs';
7
8
  import { runWg17 } from './run-wg17.mjs';
8
9
 
9
10
  const offline = process.argv.includes('--offline');
@@ -12,5 +13,6 @@ await runStandalone(async (reporter) => {
12
13
  if (!offline) await runNeumerkel(reporter);
13
14
  runConformance(reporter);
14
15
  runIsoStrict(reporter);
16
+ runIsoPart2Amendment(reporter);
15
17
  runWg17(reporter);
16
18
  });
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { run } from '../src/index.js';
6
+ import { Program } from '../src/program.js';
7
+ import { TestReporter, assertEqual, isMainModule, runStandalone } from './test-style.mjs';
8
+
9
+ function withModuleTree(files, test) {
10
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeprolog-part2-amendment-'));
11
+ try {
12
+ for (const [name, source] of Object.entries(files)) {
13
+ fs.writeFileSync(path.join(directory, name), source);
14
+ }
15
+ return test(directory);
16
+ } finally {
17
+ fs.rmSync(directory, { recursive: true, force: true });
18
+ }
19
+ }
20
+
21
+ function programFrom(directory, source, filename = 'main.pl') {
22
+ return Program.parseSources([{ text: source, filename: path.join(directory, filename), baseDir: directory }]);
23
+ }
24
+
25
+ export function runIsoPart2Amendment(reporter = new TestReporter()) {
26
+ reporter.section('ISO/IEC 13211-2 amendment 2013');
27
+
28
+ reporter.test('module/2 exports are imported by use_module/1 while private predicates stay local', () => {
29
+ withModuleTree({
30
+ 'colors.pl': [
31
+ ':- module(colors, [tone/1]).',
32
+ 'tone(blue).',
33
+ 'hidden(private).',
34
+ '',
35
+ ].join('\n'),
36
+ }, (directory) => {
37
+ const program = programFrom(directory, [
38
+ ":- use_module('colors.pl').",
39
+ 'answer(X) :- tone(X).',
40
+ '',
41
+ ].join('\n'));
42
+ assertEqual(run(program, { goal: 'answer(X)' }).stdout, 'answer(blue).\n', 'use_module/1 export import');
43
+ assertEqual(program.findGroup('hidden', 1, 'user'), null, 'private predicate not imported');
44
+ assertEqual(program.findGroup('hidden', 1, 'colors')?.module, 'colors', 'private predicate remains in defining module');
45
+ });
46
+ });
47
+
48
+ reporter.test('use_module/2 selectively imports named exported predicates', () => {
49
+ withModuleTree({
50
+ 'palette.pl': [
51
+ ':- module(palette, [red/0, blue/0]).',
52
+ 'red.',
53
+ 'blue.',
54
+ '',
55
+ ].join('\n'),
56
+ }, (directory) => {
57
+ const program = programFrom(directory, [
58
+ ":- use_module('palette.pl', [red/0]).",
59
+ 'answer :- red.',
60
+ '',
61
+ ].join('\n'));
62
+ assertEqual(run(program, { goal: 'answer' }).stdout, 'answer.\n', 'selective import execution');
63
+ assertEqual(program.findGroup('red', 0, 'user')?.module, 'palette', 'selected export imported');
64
+ assertEqual(program.findGroup('blue', 0, 'user'), null, 'unselected export not imported');
65
+ });
66
+ });
67
+
68
+ reporter.test('ensure_loaded/1 imports public predicates from a module like use_module/1', () => {
69
+ withModuleTree({
70
+ 'colors.pl': [
71
+ ':- module(colors, [tone/1]).',
72
+ 'tone(blue).',
73
+ '',
74
+ ].join('\n'),
75
+ }, (directory) => {
76
+ const program = programFrom(directory, [
77
+ ":- ensure_loaded('colors.pl').",
78
+ 'answer(X) :- tone(X).',
79
+ '',
80
+ ].join('\n'));
81
+ assertEqual(run(program, { goal: 'answer(X)' }).stdout, 'answer(blue).\n', 'ensure_loaded/1 module import');
82
+ });
83
+ });
84
+
85
+ reporter.test('repeated ensure_loaded/1 still imports an already loaded module into each caller', () => {
86
+ withModuleTree({
87
+ 'colors.pl': [
88
+ ':- module(colors, [tone/1]).',
89
+ 'tone(blue).',
90
+ '',
91
+ ].join('\n'),
92
+ 'left.pl': [
93
+ ':- module(left, [answer/1]).',
94
+ ":- ensure_loaded('colors.pl').",
95
+ 'answer(X) :- tone(X).',
96
+ '',
97
+ ].join('\n'),
98
+ 'right.pl': [
99
+ ':- module(right, [answer/1]).',
100
+ ":- ensure_loaded('colors.pl').",
101
+ 'answer(X) :- tone(X).',
102
+ '',
103
+ ].join('\n'),
104
+ }, (directory) => {
105
+ const program = programFrom(directory, [
106
+ ":- use_module('left.pl', [answer/1]).",
107
+ ":- use_module('right.pl', []).",
108
+ 'both(X,Y) :- left:answer(X), right:answer(Y).',
109
+ '',
110
+ ].join('\n'));
111
+ assertEqual(run(program, { goal: 'both(X,Y)' }).stdout, 'both(blue, blue).\n', 'per-caller ensure_loaded import');
112
+ });
113
+ });
114
+
115
+ reporter.test('module source loaded by use_module/1 begins with module/2', () => {
116
+ withModuleTree({
117
+ 'late.pl': [
118
+ 'before_module.',
119
+ ':- module(late, [visible/0]).',
120
+ 'visible.',
121
+ '',
122
+ ].join('\n'),
123
+ }, (directory) => {
124
+ let error = null;
125
+ try {
126
+ programFrom(directory, ":- use_module('late.pl').\n");
127
+ } catch (caught) {
128
+ error = caught;
129
+ }
130
+ assertEqual(error?.formal, 'existence_error(module)', 'late module directive rejection');
131
+ });
132
+ });
133
+
134
+ reporter.test('a second module/2 cannot restart a module body within the same Prolog text', () => {
135
+ let error = null;
136
+ try {
137
+ Program.parse([
138
+ ':- module(first, [one/0]).',
139
+ 'one.',
140
+ ':- module(second, [two/0]).',
141
+ 'two.',
142
+ '',
143
+ ].join('\n'));
144
+ } catch (caught) {
145
+ error = caught;
146
+ }
147
+ assertEqual(Boolean(error), true, 'second module directive rejected');
148
+ });
149
+
150
+ reporter.test('distinct Prolog texts may each begin and end their own module body', () => {
151
+ const program = Program.parseSources([
152
+ { text: ':- module(first, [value/1]).\nvalue(first).\n', filename: '<first>' },
153
+ { text: ':- module(second, [value/1]).\nvalue(second).\n', filename: '<second>' },
154
+ ]);
155
+ assertEqual(run(program, { goal: 'first:value(X)' }).stdout, 'first:value(first).\n', 'first text body');
156
+ assertEqual(run(program, { goal: 'second:value(X)' }).stdout, 'second:value(second).\n', 'second text body');
157
+ });
158
+
159
+ reporter.test('meta_predicate directive accepts the amendment operator spelling', () => {
160
+ const program = Program.parse([
161
+ ':- module(example, [capture/2]).',
162
+ ':- meta_predicate capture(:, *).',
163
+ 'capture(Goal, Module) :- Goal = Module:_G.',
164
+ '',
165
+ ].join('\n'));
166
+ assertEqual(program.findGroup('capture', 2, 'example')?.metaArgumentModes?.[0]?.kind,
167
+ 'context', 'colon meta mode registered');
168
+ });
169
+
170
+ reporter.test('colon meta-arguments are visibly prefixed with the calling source module', () => {
171
+ withModuleTree({
172
+ 'trace.pl': [
173
+ ':- module(trace, [capture/2]).',
174
+ ':- meta_predicate capture(:, *).',
175
+ 'capture(Goal, Module) :- Goal = Module:_G.',
176
+ '',
177
+ ].join('\n'),
178
+ 'foo.pl': [
179
+ ':- module(foo, [seen/1]).',
180
+ ":- use_module('trace.pl').",
181
+ 'seen(Module) :- capture(local_goal, Module).',
182
+ '',
183
+ ].join('\n'),
184
+ }, (directory) => {
185
+ const program = programFrom(directory, ":- use_module('foo.pl').\n");
186
+ assertEqual(run(program, { goal: 'foo:seen(Module)' }).stdout,
187
+ 'foo:seen(foo).\n', 'visible caller module prefix');
188
+ });
189
+ });
190
+
191
+ reporter.test('colon meta-arguments preserve caller context when the argument is a variable', () => {
192
+ withModuleTree({
193
+ 'trace.pl': [
194
+ ':- module(trace, [invoke/1]).',
195
+ ':- meta_predicate invoke(:).',
196
+ 'invoke(Goal) :- call(Goal).',
197
+ '',
198
+ ].join('\n'),
199
+ 'foo.pl': [
200
+ ':- module(foo, [answer/1]).',
201
+ ":- use_module('trace.pl').",
202
+ 'answer(X) :- Goal = private(X), invoke(Goal).',
203
+ 'private(ok).',
204
+ '',
205
+ ].join('\n'),
206
+ }, (directory) => {
207
+ const program = programFrom(directory, ":- use_module('foo.pl').\n");
208
+ assertEqual(run(program, { goal: 'foo:answer(X)' }).stdout,
209
+ 'foo:answer(ok).\n', 'variable meta-argument caller context');
210
+ });
211
+ });
212
+
213
+ reporter.test('strict Part 1 does not predeclare the Part 2 colon operator', () => {
214
+ assertEqual(run('', { isoStrict: true, goal: "current_op(600,xfy,':')" }).stats.completed_goal_lists,
215
+ 0, 'strict Part 1 colon current_op');
216
+ let error = null;
217
+ try {
218
+ Program.parse('p :- user:true.\n', { isoStrict: true });
219
+ } catch (caught) {
220
+ error = caught;
221
+ }
222
+ assertEqual(Boolean(error), true, 'strict Part 1 colon operator syntax rejected');
223
+ });
224
+
225
+ reporter.sectionTotal('ISO/IEC 13211-2 amendment 2013');
226
+ }
227
+
228
+ if (isMainModule(import.meta.url)) await runStandalone(runIsoPart2Amendment);
@@ -2708,9 +2708,12 @@ whose conclusions remain auditable.
2708
2708
 
2709
2709
  EyeProlog supplies the Part 1 control, dynamic-database, operator, and I/O
2710
2710
  facilities together with its normal-profile module forms `module/2`,
2711
- `use_module/1`, `use_module/2`, and `Module:Goal`. These forms are treated as a
2712
- module compatibility surface, not as a claim of complete ISO/IEC 13211-2:2000
2713
- conformance. Definite-clause grammar notation remains
2711
+ `use_module/1`, `use_module/2`, `meta_predicate/1`, and `Module:Goal`. The
2712
+ requirements clarified by the 2013 ISO/IEC 13211-2 module amendment are covered
2713
+ by a dedicated release-gated suite, including public imports through
2714
+ `ensure_loaded/1` and caller-module qualification of `:` meta-arguments. The
2715
+ unchanged remainder of Part 2 is still treated as a compatibility surface, not
2716
+ as a claim of complete ISO/IEC 13211-2:2000 conformance. Definite-clause grammar notation remains
2714
2717
  outside this profile. The examples still prefer explicit domain
2715
2718
  relations, state, and syntax trees where that makes assumptions easier to
2716
2719
  inspect.
@@ -5653,10 +5656,11 @@ semantics, such as #75's conditional power-underflow proposal, strict mode keeps
5653
5656
  the licensed baseline until the change is standardized or explicitly adopted as
5654
5657
  a compatibility extension. Normal EyeProlog additionally provides a practical
5655
5658
  module interface aligned with later WG17 module amendment work and a
5656
- definite-clause-grammar profile following ISO/IEC TS 13211-3. Those
5657
- normal-mode profiles are documented and tested compatibility surfaces; they are
5658
- not currently claimed as complete clause-by-clause certifications of Part 2 or
5659
- Part 3.
5659
+ definite-clause-grammar profile following ISO/IEC TS 13211-3. The requirements
5660
+ clarified by the 2013 Part 2 amendment have a dedicated executable coverage
5661
+ ledger; the unchanged remainder of Part 2 and the Part 3 profile are documented
5662
+ and tested compatibility surfaces rather than complete clause-by-clause
5663
+ certifications.
5660
5664
 
5661
5665
  Normal-mode Prolog source accepted by EyeProlog is UTF-8. `%` starts a line
5662
5666
  comment and `/* ... */` delimits a block comment. Plain atoms begin with a
@@ -5710,8 +5714,9 @@ denotes one literal double quote character.
5710
5714
 
5711
5715
  Graphic tokens use the characters `#$&*+-./<=>?@^~\`; `!` and `;` are solo
5712
5716
  atoms. A colon is the Part 2 module qualification operator in `Module:Goal`;
5713
- quote an atom whose name itself contains a colon. Unquoted angle-bracket IRIs
5714
- are not syntax.
5717
+ normal mode predeclares it, while `--iso-strict` does not include it in the
5718
+ Part 1 initial operator table. Quote an atom whose name itself contains a colon.
5719
+ Unquoted angle-bracket IRIs are not syntax.
5715
5720
 
5716
5721
  A `/*` sequence opens a block comment only when it begins a token; inside a
5717
5722
  maximal graphic token the slash and star remain atom characters. Graphic tokens
@@ -5756,6 +5761,10 @@ lowered to ordinary compound terms:
5756
5761
 
5757
5762
  - prefix: ISO `?-`, `\+`, unary `+`, unary `-`, and `\`;
5758
5763
  - control: `,`, `;`, and `->`;
5764
+ - normal Part 2 module profile: `:` at priority 600 (`xfy`) and
5765
+ `meta_predicate` at priority 1150 (`fx`) so the amendment's directive spelling
5766
+ such as `:- meta_predicate run(:).` parses directly; these are not predeclared
5767
+ by `--iso-strict`;
5759
5768
  - quad syntax extension: `?-` is also a priority-1200 `xfx` operator so a
5760
5769
  label may precede a quad query;
5761
5770
  - grammar rules: `-->` and the Part 3 alternative `|`;