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 +3 -2
- package/src/parser.js +10 -3
- package/src/program.js +55 -11
- package/src/solver.js +17 -5
- package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +3 -1
- package/test/conformance/ISO-PART2-AMENDMENT-2013.md +36 -0
- package/test/conformance/ISO-PART2-PART3-SCOPE.md +15 -7
- package/test/conformance/README.md +5 -2
- package/test/run-all.mjs +2 -0
- package/test/run-conformance-all.mjs +2 -0
- package/test/run-iso-part2-amendment.mjs +228 -0
- package/the-art-of-eyeprolog.md +18 -9
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.5.
|
|
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
|
-
|
|
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
|
|
308
|
+
const modes = [];
|
|
307
309
|
for (let index = 0; index < template.args.length; index++) {
|
|
308
310
|
const spec = template.args[index];
|
|
309
|
-
if (
|
|
310
|
-
|
|
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}`,
|
|
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.
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1228
|
-
const argument = goal.args[index];
|
|
1229
|
-
if (argument
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
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-
|
|
20
|
-
|
|
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);
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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`.
|
|
2712
|
-
|
|
2713
|
-
|
|
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.
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
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
|
-
|
|
5714
|
-
|
|
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 `|`;
|