eyeprolog 1.5.34 → 1.5.35
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/parser.js +8 -5
- package/src/program-analysis.js +35 -2
- package/src/solver.js +14 -12
- package/src/write.js +1 -2
- package/test/run-neumerkel-tests.mjs +5 -2
- package/test/run-regression.mjs +13 -9
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -811,11 +811,14 @@ class Parser {
|
|
|
811
811
|
let left = this.parsePrefixTerm(minPrecedence, allowBar, allowOperatorAtom);
|
|
812
812
|
const leftIsBareOperatorAtom = initialWasCurrentOperator &&
|
|
813
813
|
left.type === ATOM && left.name === initialOperatorName;
|
|
814
|
-
// Neumerkel syntax #379
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
814
|
+
// Neumerkel syntax #379: the DCG rule operator `-->` is processor-defined
|
|
815
|
+
// and not an ISO predicate-indicator name. A bare `-->` (not parenthesized)
|
|
816
|
+
// followed by `/` would be an invalid strict-ISO predicate indicator.
|
|
817
|
+
// However, `(-->)/2` is valid: the parentheses make `-->` an ordinary atom
|
|
818
|
+
// argument, and ISO 7.10.3 does not restrict which atoms may appear as the
|
|
819
|
+
// name in a Name/Arity indicator — only the write-term rules govern spelling.
|
|
820
|
+
// Only reject the bare-operator form so (-->)/2 succeeds per #379.
|
|
821
|
+
if (this.strictIso && leftIsBareOperatorAtom && left.name === '-->' &&
|
|
819
822
|
this.operatorTokenName() === '/') {
|
|
820
823
|
throw new Error(`parse line ${this.token.line}: operator atom --> is not permitted in a strict ISO predicate indicator`);
|
|
821
824
|
}
|
package/src/program-analysis.js
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export function componentHasNegativeEdge(start, deps, negativeEdges) {
|
|
10
10
|
const forward = reachableIndexes(start, deps);
|
|
11
|
-
|
|
11
|
+
// A node is in the same SCC as `start` iff it can also reach `start`.
|
|
12
|
+
// Rather than calling reachableIndexes() per-node (O(n^2)), compute the
|
|
13
|
+
// reverse-reachability set from `start` over the transposed graph once.
|
|
14
|
+
const backward = reachableIndexesTransposed(start, deps, forward);
|
|
15
|
+
const component = new Set([...forward].filter((index) => backward.has(index)));
|
|
12
16
|
return negativeEdges.some(([from, to]) => component.has(from) && component.has(to));
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -27,7 +31,8 @@ export function clauseIsDirectRecursive(clause, group) {
|
|
|
27
31
|
|
|
28
32
|
export function componentHasCut(start, deps, groups) {
|
|
29
33
|
const forward = reachableIndexes(start, deps);
|
|
30
|
-
const
|
|
34
|
+
const backward = reachableIndexesTransposed(start, deps, forward);
|
|
35
|
+
const component = [...forward].filter((index) => backward.has(index));
|
|
31
36
|
return component.some((index) => {
|
|
32
37
|
const group = groups[index];
|
|
33
38
|
const directRecursive = group.clauses.some((clause) => clauseIsDirectRecursive(clause, group));
|
|
@@ -50,6 +55,34 @@ export function reachableIndexes(start, deps) {
|
|
|
50
55
|
return seen;
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
// Returns the set of nodes in `candidates` that can reach `target` by
|
|
59
|
+
// traversing `deps` in reverse. This is equivalent to asking which nodes
|
|
60
|
+
// in the forward-reachable set from `target` also have `target` in their
|
|
61
|
+
// own forward-reachable set, but computed in a single BFS over the
|
|
62
|
+
// transposed graph rather than one BFS per candidate node.
|
|
63
|
+
function reachableIndexesTransposed(target, deps, candidates) {
|
|
64
|
+
// Build a transposed adjacency list restricted to the candidate set.
|
|
65
|
+
const reverse = new Map();
|
|
66
|
+
for (const from of candidates) {
|
|
67
|
+
if (!reverse.has(from)) reverse.set(from, []);
|
|
68
|
+
for (const to of deps[from]) {
|
|
69
|
+
if (!candidates.has(to)) continue;
|
|
70
|
+
let bucket = reverse.get(to);
|
|
71
|
+
if (bucket == null) { bucket = []; reverse.set(to, bucket); }
|
|
72
|
+
bucket.push(from);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const stack = [target];
|
|
77
|
+
while (stack.length) {
|
|
78
|
+
const current = stack.pop();
|
|
79
|
+
if (seen.has(current)) continue;
|
|
80
|
+
seen.add(current);
|
|
81
|
+
for (const prev of reverse.get(current) ?? []) if (!seen.has(prev)) stack.push(prev);
|
|
82
|
+
}
|
|
83
|
+
return seen;
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
|
|
54
87
|
export function isFiniteDatalogArgument(term) {
|
|
55
88
|
return term?.type === VAR || term?.type === ATOM || term?.type === 'string' || term?.type === 'number';
|
package/src/solver.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Most semantic decisions still flow through unification; optimizations only select candidates earlier.
|
|
3
3
|
import {
|
|
4
4
|
ATOM, COMPOUND, NUMBER, STRING, VAR, Env, Term, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
|
|
5
|
-
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
5
|
+
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList, isScalar,
|
|
6
6
|
numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
8
8
|
import { numberValueKey, sameNumberValue } from './number-value.js';
|
|
@@ -2692,11 +2692,14 @@ function matchScalarFact(goal, head, env) {
|
|
|
2692
2692
|
|
|
2693
2693
|
function derefScalarMatch(term, env, names, values) {
|
|
2694
2694
|
let current = term;
|
|
2695
|
-
|
|
2695
|
+
const seen = new Set();
|
|
2696
|
+
while (current?.type === 'var') {
|
|
2697
|
+
if (seen.has(current.name)) break;
|
|
2698
|
+
seen.add(current.name);
|
|
2696
2699
|
const localIndex = names.indexOf(current.name);
|
|
2697
|
-
if (localIndex >= 0) current = values[localIndex];
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
+
if (localIndex >= 0) { current = values[localIndex]; continue; }
|
|
2701
|
+
if (env.has(current.name)) { current = env.get(current.name); continue; }
|
|
2702
|
+
break;
|
|
2700
2703
|
}
|
|
2701
2704
|
return current;
|
|
2702
2705
|
}
|
|
@@ -3092,12 +3095,11 @@ function matchGroundBinaryClause(goal, clause) {
|
|
|
3092
3095
|
return { nextGoal: compound(bodyGoal.name, bodyArgs) };
|
|
3093
3096
|
}
|
|
3094
3097
|
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
}
|
|
3098
|
+
// isScalar is imported from term.js; use it as the canonical scalar test.
|
|
3099
|
+
const isScalarTerm = isScalar;
|
|
3098
3100
|
|
|
3099
3101
|
function sameScalarTerm(left, right) {
|
|
3100
|
-
return
|
|
3102
|
+
return isScalar(left) && isScalar(right) && left.type === right.type &&
|
|
3101
3103
|
(left.type === 'number' ? sameNumberValue(left.name, right.name) : left.name === right.name);
|
|
3102
3104
|
}
|
|
3103
3105
|
|
|
@@ -3131,9 +3133,9 @@ function sameResolvedGroundTerm(left, right, env) {
|
|
|
3131
3133
|
|
|
3132
3134
|
function groundChainKey(term) {
|
|
3133
3135
|
if (term?.type === COMPOUND) {
|
|
3134
|
-
|
|
3135
|
-
for (let i = 0; i < term.arity; i++)
|
|
3136
|
-
return
|
|
3136
|
+
const parts = [`${term.name}/${term.arity}`];
|
|
3137
|
+
for (let i = 0; i < term.arity; i++) parts.push(groundChainKey(term.args[i]));
|
|
3138
|
+
return parts.join('');
|
|
3137
3139
|
}
|
|
3138
3140
|
return `${term?.type ?? ''}:${term?.name ?? ''}`;
|
|
3139
3141
|
}
|
package/src/write.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
} from './term.js';
|
|
6
6
|
|
|
7
7
|
const graphicAtomCharacters = new Set('!#$&*+-./<=>?@^~\\'.split(''));
|
|
8
|
-
const dottedGraphicAtomCharacters = graphicAtomCharacters;
|
|
9
8
|
const compactInfixOperators = new Set([':', '..']);
|
|
10
9
|
|
|
11
10
|
function quotedControlEscape(ch) {
|
|
@@ -60,7 +59,7 @@ function writeAtom(name) {
|
|
|
60
59
|
|
|
61
60
|
function isDottedGraphicAtom(name) {
|
|
62
61
|
return name.includes('.') && [...name].some((ch) => ch !== '.') && !name.startsWith('/*') &&
|
|
63
|
-
[...name].every((ch) =>
|
|
62
|
+
[...name].every((ch) => graphicAtomCharacters.has(ch));
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
function compactBoundaryNeedsSpace(left, right) {
|
|
@@ -59,13 +59,16 @@ export function runNeumerkelHarnessTests(reporter = new TestReporter()) {
|
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
reporter.test('latest strict syntax
|
|
62
|
+
reporter.test('latest strict syntax allows Neumerkel #379 parenthesized --> atom', () => {
|
|
63
|
+
// Upstream Codex expectation for #379: writeq((-->)/2). -> (-->)/2
|
|
64
|
+
// (-->)/2 is valid: parentheses make --> an ordinary atom; only bare
|
|
65
|
+
// --> followed by / in a predicate indicator is processor-defined syntax.
|
|
63
66
|
const item = {
|
|
64
67
|
id: 379,
|
|
65
68
|
query: 'writeq((-->)/2).',
|
|
66
69
|
input: 'writeq((-->)/2).',
|
|
67
70
|
readCount: 1,
|
|
68
|
-
expected: '
|
|
71
|
+
expected: '(-->)/2',
|
|
69
72
|
};
|
|
70
73
|
const actual = executeWg17Item(item);
|
|
71
74
|
if (!matchesUpstreamExpectation(item.expected, actual, item)) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -1956,8 +1956,10 @@ c4 ?- call((!;1)).
|
|
|
1956
1956
|
},
|
|
1957
1957
|
},
|
|
1958
1958
|
{
|
|
1959
|
-
name: 'ISO predicate indicators require parentheses around operator atoms',
|
|
1959
|
+
name: 'ISO predicate indicators require parentheses around bare operator atoms',
|
|
1960
1960
|
run: () => {
|
|
1961
|
+
// Bare --> /2 (without parens) is still rejected in all modes because
|
|
1962
|
+
// --> is an infix/prefix operator atom that cannot appear as a bare operand.
|
|
1961
1963
|
let caught = null;
|
|
1962
1964
|
try {
|
|
1963
1965
|
parseGoalText('writeq(--> /2)');
|
|
@@ -1966,20 +1968,22 @@ c4 ?- call((!;1)).
|
|
|
1966
1968
|
}
|
|
1967
1969
|
if (!caught) throw new Error('bare operator predicate indicator unexpectedly parsed');
|
|
1968
1970
|
assertIncludes(caught.message, 'operator atom', 'bare indicator syntax rejection');
|
|
1971
|
+
// (-->)/2 with parentheses is legal in both normal and strict ISO modes.
|
|
1972
|
+
// Neumerkel #379: upstream Codex expectation is (-->)/2 -> (-->)/2 (success).
|
|
1973
|
+
// Parentheses make --> an ordinary atom argument; ISO 7.10.3 does not
|
|
1974
|
+
// restrict which atoms may appear as the name in a Name/Arity indicator.
|
|
1969
1975
|
parseGoalText('writeq((-->)/2)');
|
|
1970
1976
|
assertEqual(
|
|
1971
1977
|
run('', { goal: 'writeq((-->)/2)' }).stdout,
|
|
1972
1978
|
'(-->)/2writeq((-->) / 2).\n',
|
|
1973
1979
|
'parenthesized operator indicator stays legal outside strict ISO',
|
|
1974
1980
|
);
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
if (!caught) throw new Error('Neumerkel #379 strict indicator unexpectedly parsed');
|
|
1982
|
-
assertIncludes(caught.message, 'strict ISO predicate indicator', 'Neumerkel #379 strict syntax rejection');
|
|
1981
|
+
parseGoalText('writeq((-->)/2)', { isoStrict: true });
|
|
1982
|
+
assertEqual(
|
|
1983
|
+
run('', { isoStrict: true, goal: 'writeq((-->)/2)' }).stdout,
|
|
1984
|
+
'(-->)/2writeq((-->) / 2).\n',
|
|
1985
|
+
'Neumerkel #379: parenthesized --> is legal in strict ISO',
|
|
1986
|
+
);
|
|
1983
1987
|
},
|
|
1984
1988
|
},
|
|
1985
1989
|
{
|