eyeprolog 1.5.56 → 1.5.57
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 +1 -1
- package/src/cli.js +1 -0
- package/src/parser.js +74 -1
- package/src/program.js +10 -5
- package/test/conformance/ISO-PROLOG-TEXT-EXECUTION-MATRIX.md +1 -1
- package/test/conformance/expected-errors/syntax/extra_missing_head_rejected.txt +1 -1
- package/test/regression/cases-regression.mjs +36 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -335,6 +335,7 @@ Options:
|
|
|
335
335
|
--proof-detail mode Use abstract or expanded proof detail (implies --proof).
|
|
336
336
|
--verify-proof file Verify why/2 proof certificates against the input program.
|
|
337
337
|
-q, --quads Run embedded quad tests and fail if any do not hold.
|
|
338
|
+
Note: -q is quads, not quiet; --quiet has no short form.
|
|
338
339
|
--quiet Suppress answer terms while preserving Prolog output.
|
|
339
340
|
-s, --stats Print solver and memory statistics to stderr after execution.
|
|
340
341
|
--iso-strict Use ISO/IEC 13211-1 core + Corrigenda 1-3 only;
|
package/src/parser.js
CHANGED
|
@@ -1138,7 +1138,7 @@ class Parser {
|
|
|
1138
1138
|
}
|
|
1139
1139
|
const operator = this.applyOperatorDirective(directive, line);
|
|
1140
1140
|
if (!coreDirective && !extensionDirective && !operator) {
|
|
1141
|
-
throw new Error(`parse line ${line}:
|
|
1141
|
+
throw new Error(`parse line ${line}: ${unsupportedDirectiveMessage(directive, this.strictIso)}`);
|
|
1142
1142
|
}
|
|
1143
1143
|
this.expect(TOK.DOT, '.');
|
|
1144
1144
|
this.applyParserFlagDirective(directive, line);
|
|
@@ -1911,3 +1911,76 @@ export function parseGoalText(text, options = {}) {
|
|
|
1911
1911
|
}
|
|
1912
1912
|
return head.args[0];
|
|
1913
1913
|
}
|
|
1914
|
+
|
|
1915
|
+
// Directives are accepted from a fixed list, so an unrecognized one is a
|
|
1916
|
+
// different mistake from unparseable input and deserves a different message.
|
|
1917
|
+
// The three cases below cover what people actually write: a bare goal that
|
|
1918
|
+
// belongs in initialization/1, a misspelt directive name, and a term that is
|
|
1919
|
+
// not a directive at all.
|
|
1920
|
+
const CORE_DIRECTIVE_INDICATORS = [
|
|
1921
|
+
'dynamic/1', 'multifile/1', 'discontiguous/1', 'initialization/1', 'include/1',
|
|
1922
|
+
'ensure_loaded/1', 'char_conversion/2', 'set_prolog_flag/2', 'op/3',
|
|
1923
|
+
];
|
|
1924
|
+
const EXTENSION_DIRECTIVE_INDICATORS = [
|
|
1925
|
+
'use_module/1', 'use_module/2', 'meta_predicate/1', 'attribute/1', 'table/1',
|
|
1926
|
+
'public/1', 'module/2',
|
|
1927
|
+
];
|
|
1928
|
+
|
|
1929
|
+
function directiveNameDistance(a, b) {
|
|
1930
|
+
// Small Levenshtein distance, used only to suggest a near-miss spelling.
|
|
1931
|
+
const rows = a.length + 1;
|
|
1932
|
+
const cols = b.length + 1;
|
|
1933
|
+
let previous = Array.from({ length: cols }, (_, i) => i);
|
|
1934
|
+
for (let i = 1; i < rows; i++) {
|
|
1935
|
+
const current = [i];
|
|
1936
|
+
for (let j = 1; j < cols; j++) {
|
|
1937
|
+
current[j] = Math.min(
|
|
1938
|
+
previous[j] + 1,
|
|
1939
|
+
current[j - 1] + 1,
|
|
1940
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
previous = current;
|
|
1944
|
+
}
|
|
1945
|
+
return previous[cols - 1];
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function nearestDirectiveName(name, available) {
|
|
1949
|
+
let best = null;
|
|
1950
|
+
let bestDistance = Infinity;
|
|
1951
|
+
for (const indicator of available) {
|
|
1952
|
+
const candidate = indicator.slice(0, indicator.lastIndexOf('/'));
|
|
1953
|
+
const distance = directiveNameDistance(name, candidate);
|
|
1954
|
+
if (distance < bestDistance) {
|
|
1955
|
+
bestDistance = distance;
|
|
1956
|
+
best = candidate;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
// Only suggest a genuinely close spelling, not the alphabetically nearest.
|
|
1960
|
+
return bestDistance <= Math.max(1, Math.floor(name.length / 3)) ? best : null;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function unsupportedDirectiveMessage(directive, strictIso) {
|
|
1964
|
+
const available = strictIso
|
|
1965
|
+
? CORE_DIRECTIVE_INDICATORS
|
|
1966
|
+
: [...CORE_DIRECTIVE_INDICATORS, ...EXTENSION_DIRECTIVE_INDICATORS];
|
|
1967
|
+
if (directive.type !== COMPOUND && directive.type !== ATOM) {
|
|
1968
|
+
return 'a directive must be a callable term';
|
|
1969
|
+
}
|
|
1970
|
+
const name = directive.name;
|
|
1971
|
+
const arity = directive.type === COMPOUND ? directive.arity : 0;
|
|
1972
|
+
const indicator = `${name}/${arity}`;
|
|
1973
|
+
const known = available.filter((entry) => entry.slice(0, entry.lastIndexOf('/')) === name);
|
|
1974
|
+
if (known.length > 0) {
|
|
1975
|
+
return `directive ${indicator} has the wrong arity; expected ${known.join(' or ')}`;
|
|
1976
|
+
}
|
|
1977
|
+
const suggestion = nearestDirectiveName(name, available);
|
|
1978
|
+
if (suggestion != null) {
|
|
1979
|
+
return `unknown directive ${indicator}; did you mean ${suggestion}?`;
|
|
1980
|
+
}
|
|
1981
|
+
// A goal such as `:- write(hello), nl.` is well-formed but is not one of the
|
|
1982
|
+
// recognized declarations, so point at initialization/1 rather than reporting
|
|
1983
|
+
// a syntax error.
|
|
1984
|
+
return `unknown directive ${indicator}; to run a goal at load time use ` +
|
|
1985
|
+
':- initialization(Goal).';
|
|
1986
|
+
}
|
package/src/program.js
CHANGED
|
@@ -1439,8 +1439,13 @@ function analyzeInteropPortability(program, extraGoals = []) {
|
|
|
1439
1439
|
// this only affects user code, but within user code it acts at a distance
|
|
1440
1440
|
// across included files.
|
|
1441
1441
|
//
|
|
1442
|
-
// How strictly this is treated depends on how explicit the program was
|
|
1443
|
-
//
|
|
1442
|
+
// How strictly this is treated depends on how explicit the program was. The
|
|
1443
|
+
// first two tiers match what Trealla and Scryer do for the same program
|
|
1444
|
+
// (`:- use_module(library(lists)).` followed by clauses for append/3): both
|
|
1445
|
+
// warn and continue, Trealla with `overwriting user:'append'/3` and Scryer with
|
|
1446
|
+
// `overwriting append/3 because the clauses are discontiguous`. SWI-Prolog
|
|
1447
|
+
// treats imported predicates as weak symbols and warns likewise. No system
|
|
1448
|
+
// raises an error there, so neither does EyeProlog:
|
|
1444
1449
|
//
|
|
1445
1450
|
// implicit autoload -> warning
|
|
1446
1451
|
// use_module(library(L)) -> warning
|
|
@@ -1448,9 +1453,9 @@ function analyzeInteropPortability(program, extraGoals = []) {
|
|
|
1448
1453
|
//
|
|
1449
1454
|
// Only the last case is an outright contradiction: the program asked for that
|
|
1450
1455
|
// exact predicate to be imported and then supplied clauses for it. The first
|
|
1451
|
-
// two stay warnings so that flat, module-free programs
|
|
1452
|
-
// on Scryer and Trealla,
|
|
1453
|
-
//
|
|
1456
|
+
// two stay warnings so that flat, module-free programs keep loading here
|
|
1457
|
+
// exactly as they do on Scryer and Trealla, which was checked directly rather
|
|
1458
|
+
// than inferred. Note that ISO reserves permission_error(modify, static_procedure)
|
|
1454
1459
|
// for built-in predicates (7.5.3); library predicates are not built-ins, so
|
|
1455
1460
|
// this error is raised only where the program's own import list demands it.
|
|
1456
1461
|
function analyzeLibraryShadowing(program) {
|
|
@@ -23,7 +23,7 @@ chosen behavior is also indexed by `ISO-IMPLEMENTATION-DEFINED.md`.
|
|
|
23
23
|
| 7.4.2.7 `include/1` | covered | The included text is prepared at the directive position and shares operator/character/flag preparation state with its parent. |
|
|
24
24
|
| 7.4.2.8 `ensure_loaded/1` | covered | A source is prepared at most once in the current load graph, including repeated references and self/top-level references. |
|
|
25
25
|
| 7.4.2.9 `set_prolog_flag/2` | covered | Preparation-time flag changes affect subsequent text and are replayed into execution state; strict flag names/values/changeability remain governed by the closed 7.11 review. |
|
|
26
|
-
| 7.4.3 source clauses | covered | Source heads/bodies are validated like program clauses, standardized static/control procedures are protected, declarations can create empty procedures, and body conversion follows 7.6.2 while preserving head/body variable identity. The built-in restriction applies in every execution mode, so consulting a clause for a built-in predicate or control construct reports the same `permission_error(modify, static_procedure)` as `assert`ing one; EyeProlog's own library and extension predicates are not standard built-ins, so ISO does not protect them. They stay redefinable when they reach the program implicitly, through autoloading or a whole-module `use_module(library(L))`, but the redefinition is reported as a shadowing warning because it replaces the library predicate for all user code in the program. Naming a predicate in an explicit import list, `use_module(library(L), [Name/Arity])`, and then supplying clauses for it is a contradiction and reports `permission_error(modify, static_procedure)`. Bundled library modules keep resolving their own internal calls, so a user redefinition never changes library behaviour. Corpus: `error/iso/consult_redefines_builtin`. |
|
|
26
|
+
| 7.4.3 source clauses | covered | Source heads/bodies are validated like program clauses, standardized static/control procedures are protected, declarations can create empty procedures, and body conversion follows 7.6.2 while preserving head/body variable identity. The built-in restriction applies in every execution mode, so consulting a clause for a built-in predicate or control construct reports the same `permission_error(modify, static_procedure)` as `assert`ing one; EyeProlog's own library and extension predicates are not standard built-ins, so ISO does not protect them. They stay redefinable when they reach the program implicitly, through autoloading or a whole-module `use_module(library(L))`, but the redefinition is reported as a shadowing warning because it replaces the library predicate for all user code in the program. Naming a predicate in an explicit import list, `use_module(library(L), [Name/Arity])`, and then supplying clauses for it is a contradiction and reports `permission_error(modify, static_procedure)`. Bundled library modules keep resolving their own internal calls, so a user redefinition never changes library behaviour. Trealla and Scryer also warn and continue for the whole-module case rather than raising an error, so the warning tiers match those systems. Corpus: `error/iso/consult_redefines_builtin`. |
|
|
27
27
|
|
|
28
28
|
The strict release test `closes ISO 7.4 Prolog-text preparation and directive
|
|
29
29
|
rows` exercises the cross-text operator/character/flag state, include and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
parse line 1:
|
|
1
|
+
parse line 1: unknown directive q/1; to run a goal at load time use :- initialization(Goal).
|
|
@@ -4427,6 +4427,42 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
|
|
|
4427
4427
|
assertIncludes(result.stdout, 'answer([1, 2, 3])', 'library append/2 still uses the library append/3');
|
|
4428
4428
|
},
|
|
4429
4429
|
},
|
|
4430
|
+
{
|
|
4431
|
+
// Directives come from a fixed list, so an unrecognized one is a
|
|
4432
|
+
// different mistake from unparseable input and gets its own message
|
|
4433
|
+
// instead of the generic parser 'bad term'.
|
|
4434
|
+
name: 'unsupported directives report why they were rejected',
|
|
4435
|
+
run: () => {
|
|
4436
|
+
const goalDirective = runCli(['-'], { input: ':- write(hello), nl.\n' });
|
|
4437
|
+
assertEqual(goalDirective.status, 1, 'goal directive status');
|
|
4438
|
+
assertIncludes(goalDirective.stderr, 'unknown directive write/1', 'goal directive indicator');
|
|
4439
|
+
assertIncludes(goalDirective.stderr, 'initialization(Goal)', 'goal directive remedy');
|
|
4440
|
+
assertNotIncludes(goalDirective.stderr, 'bad term', 'no generic parser message');
|
|
4441
|
+
|
|
4442
|
+
const misspelt = runCli(['-'], { input: ':- dynamc(foo/1).\n' });
|
|
4443
|
+
assertEqual(misspelt.status, 1, 'misspelt status');
|
|
4444
|
+
assertIncludes(misspelt.stderr, 'did you mean dynamic?', 'near-miss suggestion');
|
|
4445
|
+
|
|
4446
|
+
const wrongArity = runCli(['-'], { input: ':- dynamic(foo/1, bar).\n' });
|
|
4447
|
+
assertEqual(wrongArity.status, 1, 'wrong arity status');
|
|
4448
|
+
assertIncludes(wrongArity.stderr, 'expected dynamic/1', 'arity guidance');
|
|
4449
|
+
|
|
4450
|
+
const notCallable = runCli(['-'], { input: ':- 42.\n' });
|
|
4451
|
+
assertEqual(notCallable.status, 1, 'non-callable status');
|
|
4452
|
+
assertIncludes(notCallable.stderr, 'must be a callable term', 'callable guidance');
|
|
4453
|
+
},
|
|
4454
|
+
},
|
|
4455
|
+
{
|
|
4456
|
+
// Genuinely malformed input must keep reporting a syntax error rather
|
|
4457
|
+
// than being misreported as a directive problem.
|
|
4458
|
+
name: 'malformed terms still report a parse error',
|
|
4459
|
+
run: () => {
|
|
4460
|
+
const result = runCli(['-'], { input: 'foo(1). bar(\n' });
|
|
4461
|
+
assertEqual(result.status, 1, 'exit status');
|
|
4462
|
+
assertIncludes(result.stderr, 'parse line', 'parse error');
|
|
4463
|
+
assertNotIncludes(result.stderr, 'unknown directive', 'not a directive message');
|
|
4464
|
+
},
|
|
4465
|
+
},
|
|
4430
4466
|
{
|
|
4431
4467
|
name: 'double dash permits option-shaped file names',
|
|
4432
4468
|
run: () => {
|