eyeprolog 1.4.7 → 1.4.9
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 +8 -0
- package/src/parser.js +79 -6
- package/src/program.js +1 -1
- package/test/run-examples.mjs +5 -0
- package/test/run-regression.mjs +33 -57
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -154,6 +154,7 @@ export async function main(argv) {
|
|
|
154
154
|
isoStrict: options.isoStrict,
|
|
155
155
|
autoload: options.autoload,
|
|
156
156
|
autoloadGoals: options.goals,
|
|
157
|
+
onWarning: printSourceWarning,
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
const portabilityFailures = program.interopPortabilityWarnings ?? [];
|
|
@@ -271,6 +272,13 @@ function readStdin() {
|
|
|
271
272
|
});
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
|
|
276
|
+
function printSourceWarning(warning) {
|
|
277
|
+
if (warning.kind !== 'singleton') return;
|
|
278
|
+
process.stderr.write(`Warning: singleton: ${warning.name}, near ${warning.filename}:${warning.line}
|
|
279
|
+
`);
|
|
280
|
+
}
|
|
281
|
+
|
|
274
282
|
function printWarnings(program) {
|
|
275
283
|
for (const warning of program.interopPortabilityWarnings ?? []) {
|
|
276
284
|
if (warning.kind === 'library') {
|
package/src/parser.js
CHANGED
|
@@ -246,6 +246,8 @@ class Parser {
|
|
|
246
246
|
this.anonymous = 0;
|
|
247
247
|
this.variables = new Map();
|
|
248
248
|
this.sourceMetadata = options.sourceMetadata !== false;
|
|
249
|
+
this.onWarning = typeof options.onWarning === 'function' ? options.onWarning : null;
|
|
250
|
+
this.currentSourceTermLine = 1;
|
|
249
251
|
this.strictIso = options.isoStrict === true;
|
|
250
252
|
this.parserFlagState = options.parserFlagState ?? {
|
|
251
253
|
doubleQuotes: options.doubleQuotes ?? 'chars',
|
|
@@ -1010,9 +1012,14 @@ class Parser {
|
|
|
1010
1012
|
parseProgram(emit = null) {
|
|
1011
1013
|
const clauses = emit ? null : [];
|
|
1012
1014
|
let clauseNumber = 0;
|
|
1013
|
-
const
|
|
1015
|
+
const rawAccept = emit ?? ((clause) => clauses.push(clause));
|
|
1016
|
+
const accept = (item) => {
|
|
1017
|
+
reportClauseSingletonWarnings(item, this.onWarning, this.filename, this.currentSourceTermLine);
|
|
1018
|
+
rawAccept(item);
|
|
1019
|
+
};
|
|
1014
1020
|
while (this.token.type !== TOK.EOF) {
|
|
1015
1021
|
const line = this.token.line;
|
|
1022
|
+
this.currentSourceTermLine = line;
|
|
1016
1023
|
// Prefix operator notation needs one program-level distinction so
|
|
1017
1024
|
// a comma in `?- A, B.` remains inside the query rather than outside the
|
|
1018
1025
|
// prefix term. Ordinary functional notation is parsed as a term and then
|
|
@@ -1226,6 +1233,29 @@ function listAtomNames(term) {
|
|
|
1226
1233
|
}
|
|
1227
1234
|
|
|
1228
1235
|
|
|
1236
|
+
function collectClauseVariableOccurrences(term, counts) {
|
|
1237
|
+
if (term == null) return;
|
|
1238
|
+
if (term.type === 'var') {
|
|
1239
|
+
counts.set(term.name, (counts.get(term.name) ?? 0) + 1);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (term.type === COMPOUND) {
|
|
1243
|
+
for (const arg of term.args) collectClauseVariableOccurrences(arg, counts);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function reportClauseSingletonWarnings(clause, onWarning, filename, line) {
|
|
1248
|
+
if (typeof onWarning !== 'function' || clause?.kind === 'quad') return;
|
|
1249
|
+
const counts = new Map();
|
|
1250
|
+
collectClauseVariableOccurrences(clause.head, counts);
|
|
1251
|
+
for (const goal of clause.body ?? []) collectClauseVariableOccurrences(goal, counts);
|
|
1252
|
+
for (const [name, count] of counts) {
|
|
1253
|
+
if (count !== 1 || String(name).startsWith('_')) continue;
|
|
1254
|
+
onWarning({ kind: 'singleton', name, filename: filename ?? '<input>', line });
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
|
|
1229
1259
|
export function parseClauses(source, options = {}) {
|
|
1230
1260
|
const ownsParserFlagState = options.parserFlagState == null;
|
|
1231
1261
|
const initialDoubleQuotes = options.doubleQuotes ?? 'chars';
|
|
@@ -1282,6 +1312,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1282
1312
|
const accept = emit ?? ((clause) => clauses.push(clause));
|
|
1283
1313
|
let anonymous = 0;
|
|
1284
1314
|
let chunk = '';
|
|
1315
|
+
let chunkStartLine = 1;
|
|
1285
1316
|
|
|
1286
1317
|
const cached = (cache, key, create) => {
|
|
1287
1318
|
const existing = cache.get(key);
|
|
@@ -1466,12 +1497,42 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1466
1497
|
return -1;
|
|
1467
1498
|
};
|
|
1468
1499
|
|
|
1469
|
-
const
|
|
1500
|
+
const reportFastBinarySingletonWarnings = (head, body, line) => {
|
|
1501
|
+
if (typeof options.onWarning !== 'function') return;
|
|
1502
|
+
const variables = [];
|
|
1503
|
+
const add = (type, name) => {
|
|
1504
|
+
if (type !== 'var' || String(name).startsWith('_')) return;
|
|
1505
|
+
variables.push(name);
|
|
1506
|
+
};
|
|
1507
|
+
add(head.arg0Type, head.arg0Name);
|
|
1508
|
+
add(head.arg1Type, head.arg1Name);
|
|
1509
|
+
if (body != null) {
|
|
1510
|
+
add(body.arg0Type, body.arg0Name);
|
|
1511
|
+
add(body.arg1Type, body.arg1Name);
|
|
1512
|
+
}
|
|
1513
|
+
for (let i = 0; i < variables.length; i++) {
|
|
1514
|
+
let count = 0;
|
|
1515
|
+
for (let j = 0; j < variables.length; j++) {
|
|
1516
|
+
if (variables[i] === variables[j]) count++;
|
|
1517
|
+
}
|
|
1518
|
+
if (count === 1) {
|
|
1519
|
+
options.onWarning({
|
|
1520
|
+
kind: 'singleton',
|
|
1521
|
+
name: variables[i],
|
|
1522
|
+
filename: options.filename ?? '<input>',
|
|
1523
|
+
line,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
const emitFastBinaryRange = (text, start, end, line) => {
|
|
1470
1530
|
if (!emitBinary || start >= end || text.charCodeAt(end - 1) !== 46) return false;
|
|
1471
1531
|
const termEnd = end - 1;
|
|
1472
1532
|
const rule = findRuleInRange(text, start, termEnd);
|
|
1473
1533
|
if (rule < 0) {
|
|
1474
1534
|
if (!parseBinaryRawRange(text, start, termEnd, rawHead)) return false;
|
|
1535
|
+
reportFastBinarySingletonWarnings(rawHead, null, line);
|
|
1475
1536
|
emitBinary(rawHead.name,
|
|
1476
1537
|
rawHead.arg0Type, rawHead.arg0Name, rawHead.arg1Type, rawHead.arg1Name,
|
|
1477
1538
|
null, null, null, null, null);
|
|
@@ -1480,6 +1541,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1480
1541
|
if (findRuleInRange(text, rule + 2, termEnd) >= 0 ||
|
|
1481
1542
|
!parseBinaryRawRange(text, start, rule, rawHead) ||
|
|
1482
1543
|
!parseBinaryRawRange(text, rule + 2, termEnd, rawBody)) return false;
|
|
1544
|
+
reportFastBinarySingletonWarnings(rawHead, rawBody, line);
|
|
1483
1545
|
emitBinary(rawHead.name,
|
|
1484
1546
|
rawHead.arg0Type, rawHead.arg0Name, rawHead.arg1Type, rawHead.arg1Name,
|
|
1485
1547
|
rawBody.name, rawBody.arg0Type, rawBody.arg0Name, rawBody.arg1Type, rawBody.arg1Name);
|
|
@@ -1541,6 +1603,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1541
1603
|
|
|
1542
1604
|
const flush = () => {
|
|
1543
1605
|
const text = chunk.trim();
|
|
1606
|
+
const sourceLine = chunkStartLine;
|
|
1544
1607
|
chunk = '';
|
|
1545
1608
|
if (!text) return true;
|
|
1546
1609
|
// Once a preparation-time char_conversion/2 mapping is active, every
|
|
@@ -1549,11 +1612,15 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1549
1612
|
// here would silently bypass Convc for otherwise simple clauses.
|
|
1550
1613
|
const simple = preparationConversionActive() ? null : parseSimple(text);
|
|
1551
1614
|
if (simple) {
|
|
1615
|
+
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', sourceLine);
|
|
1552
1616
|
accept(simple);
|
|
1553
1617
|
return true;
|
|
1554
1618
|
}
|
|
1555
1619
|
try {
|
|
1556
|
-
const
|
|
1620
|
+
const nestedWarning = typeof options.onWarning === 'function'
|
|
1621
|
+
? (warning) => options.onWarning({ ...warning, line: warning.line + sourceLine - 1 })
|
|
1622
|
+
: null;
|
|
1623
|
+
const parsed = new Parser(text, { ...options, sourceMetadata: false, onWarning: nestedWarning }).parseProgram();
|
|
1557
1624
|
for (const clause of parsed) accept(clause);
|
|
1558
1625
|
return true;
|
|
1559
1626
|
} catch (_) {
|
|
@@ -1562,6 +1629,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1562
1629
|
};
|
|
1563
1630
|
|
|
1564
1631
|
let lineStart = 0;
|
|
1632
|
+
let lineNumber = 1;
|
|
1565
1633
|
while (lineStart <= source.length) {
|
|
1566
1634
|
let lineEnd = source.indexOf('\n', lineStart);
|
|
1567
1635
|
if (lineEnd < 0) lineEnd = source.length;
|
|
@@ -1571,17 +1639,21 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1571
1639
|
[contentStart, contentEnd] = trimRange(source, contentStart, contentEnd);
|
|
1572
1640
|
if (contentStart < contentEnd && source.charCodeAt(contentStart) !== 37) {
|
|
1573
1641
|
if (!chunk && source.charCodeAt(contentEnd - 1) === 46 && !preparationConversionActive()) {
|
|
1574
|
-
if (emitFastBinaryRange(source, contentStart, contentEnd)) {
|
|
1642
|
+
if (emitFastBinaryRange(source, contentStart, contentEnd, lineNumber)) {
|
|
1575
1643
|
// The program builder accepted a compact binary clause directly.
|
|
1576
1644
|
} else {
|
|
1577
1645
|
const simple = parseFastRange(source, contentStart, contentEnd);
|
|
1578
|
-
if (simple)
|
|
1579
|
-
|
|
1646
|
+
if (simple) {
|
|
1647
|
+
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', lineNumber);
|
|
1648
|
+
accept(simple);
|
|
1649
|
+
} else {
|
|
1650
|
+
chunkStartLine = lineNumber;
|
|
1580
1651
|
chunk = source.slice(lineStart, lineEnd) + '\n';
|
|
1581
1652
|
if (!flush()) return null;
|
|
1582
1653
|
}
|
|
1583
1654
|
}
|
|
1584
1655
|
} else {
|
|
1656
|
+
if (!chunk) chunkStartLine = lineNumber;
|
|
1585
1657
|
chunk += source.slice(lineStart, lineEnd) + '\n';
|
|
1586
1658
|
if (source.charCodeAt(contentEnd - 1) === 46) {
|
|
1587
1659
|
if (!flush()) return null;
|
|
@@ -1590,6 +1662,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1590
1662
|
}
|
|
1591
1663
|
if (lineEnd === source.length) break;
|
|
1592
1664
|
lineStart = lineEnd + 1;
|
|
1665
|
+
lineNumber++;
|
|
1593
1666
|
}
|
|
1594
1667
|
if (chunk.trim() && !flush()) return null;
|
|
1595
1668
|
return clauses ?? true;
|
package/src/program.js
CHANGED
|
@@ -1390,7 +1390,7 @@ function readModuleSource(designation, options) {
|
|
|
1390
1390
|
return {
|
|
1391
1391
|
name,
|
|
1392
1392
|
text: registered.source,
|
|
1393
|
-
options: { ...options, filename: registered.filename, baseDir: 'src/lib' },
|
|
1393
|
+
options: { ...options, filename: registered.filename, baseDir: 'src/lib', onWarning: null },
|
|
1394
1394
|
};
|
|
1395
1395
|
}
|
|
1396
1396
|
if (designation.type !== ATOM) throw new PrologError('type_error(source_sink)', designation);
|
package/test/run-examples.mjs
CHANGED
|
@@ -214,6 +214,11 @@ function runProgramExample(programFile, filename, options) {
|
|
|
214
214
|
const expectedExit = text.match(/^%\s*expect-exit:\s*(\d+)\s*$/m);
|
|
215
215
|
const program = Program.parseSources([{ text, filename }], {
|
|
216
216
|
sourceMetadata: options.proof,
|
|
217
|
+
onWarning: (warning) => {
|
|
218
|
+
if (warning.kind === 'singleton') {
|
|
219
|
+
process.stderr.write(`Warning: singleton: ${warning.name}, near ${warning.filename}:${warning.line}\n`);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
217
222
|
});
|
|
218
223
|
try {
|
|
219
224
|
const result = run(program, { ...options, goals: goalsInProgramOrder(program, text) });
|
package/test/run-regression.mjs
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
variantTerms,
|
|
46
46
|
parseProgramText,
|
|
47
47
|
} from '../src/index.js';
|
|
48
|
-
import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText } from '../src/parser.js';
|
|
48
|
+
import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText, tryParseClausesFastInto } from '../src/parser.js';
|
|
49
49
|
import { PrologError, formalErrorTerm } from '../src/iso.js';
|
|
50
50
|
import { compareTerms } from '../src/term.js';
|
|
51
51
|
import { formatTermForWrite } from '../src/write.js';
|
|
@@ -4062,6 +4062,38 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
4062
4062
|
assertIncludes(result.stderr, '/(member, 2)', 'stderr');
|
|
4063
4063
|
},
|
|
4064
4064
|
},
|
|
4065
|
+
{
|
|
4066
|
+
name: 'CLI reports singleton source variables without a corpus-wide warning scan',
|
|
4067
|
+
run: () => {
|
|
4068
|
+
const input = 'p(X, Once, _Ignored, _) :- q(X).\nq(a).\n';
|
|
4069
|
+
const result = runCli(['-'], { input });
|
|
4070
|
+
assertEqual(result.status, 0, 'exit status');
|
|
4071
|
+
assertEqual(result.stdout, '', 'stdout');
|
|
4072
|
+
assertEqual(result.stderr, 'Warning: singleton: Once, near <stdin>:1\n', 'stderr');
|
|
4073
|
+
},
|
|
4074
|
+
},
|
|
4075
|
+
{
|
|
4076
|
+
name: 'singleton warnings preserve the compact binary parser path',
|
|
4077
|
+
run: () => {
|
|
4078
|
+
const clauses = [];
|
|
4079
|
+
const binary = [];
|
|
4080
|
+
const warnings = [];
|
|
4081
|
+
const parsed = tryParseClausesFastInto(
|
|
4082
|
+
'p(X,Y) :- q(X,a).\n',
|
|
4083
|
+
(clause) => clauses.push(clause),
|
|
4084
|
+
(...args) => binary.push(args),
|
|
4085
|
+
{ filename: 'fast.pl', onWarning: (warning) => warnings.push(warning) },
|
|
4086
|
+
);
|
|
4087
|
+
assertEqual(parsed, true, 'fast parse result');
|
|
4088
|
+
assertEqual(clauses.length, 0, 'materialized clause count');
|
|
4089
|
+
assertEqual(binary.length, 1, 'direct binary clause count');
|
|
4090
|
+
assertEqual(warnings.length, 1, 'warning count');
|
|
4091
|
+
assertEqual(warnings[0].kind, 'singleton', 'warning kind');
|
|
4092
|
+
assertEqual(warnings[0].name, 'Y', 'warning variable');
|
|
4093
|
+
assertEqual(warnings[0].filename, 'fast.pl', 'warning filename');
|
|
4094
|
+
assertEqual(warnings[0].line, 1, 'warning line');
|
|
4095
|
+
},
|
|
4096
|
+
},
|
|
4065
4097
|
{
|
|
4066
4098
|
name: '--warnings flags explicit library(prologue) dependencies',
|
|
4067
4099
|
run: () => {
|
|
@@ -4648,64 +4680,8 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
4648
4680
|
|
|
4649
4681
|
|
|
4650
4682
|
|
|
4651
|
-
function topLevelRunnableExampleFiles() {
|
|
4652
|
-
const examplesDir = path.join(packageRoot, 'examples');
|
|
4653
|
-
return fs.readdirSync(examplesDir)
|
|
4654
|
-
.filter((name) => name.endsWith('.pl'))
|
|
4655
|
-
.sort()
|
|
4656
|
-
.map((name) => path.join(examplesDir, name));
|
|
4657
|
-
}
|
|
4658
|
-
|
|
4659
|
-
function visitTermVariables(term, counts, seen = new Set()) {
|
|
4660
|
-
if (term == null) return;
|
|
4661
|
-
if (term.type === 'var') {
|
|
4662
|
-
// The parser preserves identity for repeated source variables. Count every
|
|
4663
|
-
// occurrence so only variables that occur once syntactically are flagged.
|
|
4664
|
-
counts.set(term.name, (counts.get(term.name) ?? 0) + 1);
|
|
4665
|
-
return;
|
|
4666
|
-
}
|
|
4667
|
-
if (seen.has(term)) return;
|
|
4668
|
-
seen.add(term);
|
|
4669
|
-
if (term.type === 'compound') {
|
|
4670
|
-
for (const arg of term.args) visitTermVariables(arg, counts, seen);
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
|
-
|
|
4674
|
-
function singletonVariableWarningsForClause(clause) {
|
|
4675
|
-
const counts = new Map();
|
|
4676
|
-
visitTermVariables(clause.head, counts);
|
|
4677
|
-
for (const goal of clause.body ?? []) visitTermVariables(goal, counts);
|
|
4678
|
-
return [...counts]
|
|
4679
|
-
.filter(([name, count]) => count === 1 && !String(name).startsWith('_'))
|
|
4680
|
-
.map(([name]) => name);
|
|
4681
|
-
}
|
|
4682
|
-
|
|
4683
|
-
function singletonVariableWarningsForRunnableExamples() {
|
|
4684
|
-
const warnings = [];
|
|
4685
|
-
for (const filename of topLevelRunnableExampleFiles()) {
|
|
4686
|
-
const text = fs.readFileSync(filename, 'utf8');
|
|
4687
|
-
const program = Program.parseSources([{ text, filename }], { sourceMetadata: true });
|
|
4688
|
-
for (const clause of program.clauses) {
|
|
4689
|
-
if (path.resolve(clause.source?.filename ?? '') !== path.resolve(filename)) continue;
|
|
4690
|
-
for (const name of singletonVariableWarningsForClause(clause)) {
|
|
4691
|
-
warnings.push(`singleton: ${name}, near ${path.relative(packageRoot, filename)}:${clause.source?.line ?? '?'}`);
|
|
4692
|
-
}
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
|
-
return warnings;
|
|
4696
|
-
}
|
|
4697
|
-
|
|
4698
4683
|
function documentationSyncCases() {
|
|
4699
4684
|
return [
|
|
4700
|
-
{
|
|
4701
|
-
name: 'runnable examples are free of singleton-variable warnings',
|
|
4702
|
-
run: () => {
|
|
4703
|
-
const warnings = singletonVariableWarningsForRunnableExamples();
|
|
4704
|
-
if (warnings.length !== 0) {
|
|
4705
|
-
throw new Error(`example singleton-variable warnings\n${warnings.join('\n')}`);
|
|
4706
|
-
}
|
|
4707
|
-
},
|
|
4708
|
-
},
|
|
4709
4685
|
{
|
|
4710
4686
|
name: 'WG17 syntax status matches its executable-coverage manifest',
|
|
4711
4687
|
run: () => {
|