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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.4.7",
6
+ "version": "1.4.9",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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 accept = emit ?? ((clause) => clauses.push(clause));
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 emitFastBinaryRange = (text, start, end) => {
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 parsed = new Parser(text, { ...options, sourceMetadata: false }).parseProgram();
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) accept(simple);
1579
- else {
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);
@@ -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) });
@@ -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: () => {