eyeprolog 1.2.42 → 1.2.44

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.2.42",
6
+ "version": "1.2.44",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -1198,6 +1198,15 @@ function scopeReadTerm(term) {
1198
1198
 
1199
1199
  function parseReadTermText(text, solver) {
1200
1200
  const converted = convertedTermText(text, solver);
1201
+ // A standalone number needs no general term parser or operator tables.
1202
+ // Reuse the same bounded numeric scanner as number_chars/2 so stream reads
1203
+ // and number conversion agree on every accepted numeric token. The stream
1204
+ // scanner has already established that the final full stop is a candidate
1205
+ // terminator; trim only ISO layout immediately before that terminator.
1206
+ const numericText = converted.slice(0, -1).replace(/[\u0009-\u000d\u0020]+$/, '');
1207
+ const numericTerm = parseIsoNumber(numericText);
1208
+ if (numericTerm != null) return numericTerm;
1209
+
1201
1210
  const operatorState = createParserOperatorState(solver.program.operators.values(), false);
1202
1211
  const clauses = parseClauses(converted, {
1203
1212
  sourceMetadata: false,
@@ -2,9 +2,10 @@
2
2
  // The sources are registered here so library(Name) works in Node and browsers;
3
3
  // unlike the former autoloader, no clauses are added unless use_module/1 or
4
4
  // use_module/2 requests their module.
5
- import { createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
5
+ import { PrologError, createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
6
6
  import { clpzBuiltins } from './clpz.js';
7
- import { fs, isNode } from './platform.js';
7
+ import { fs, isNode, memoryStatistics } from './platform.js';
8
+ import { ATOM, VAR, atom, deref, numberTerm, unify } from './term.js';
8
9
 
9
10
  const moduleFiles = Object.freeze({
10
11
  aggregate: 'aggregate.pl',
@@ -73,8 +74,44 @@ export const eyePrologLibraryIndicators = Object.freeze([
73
74
  ...eyePrologNativeLibraryIndicators,
74
75
  ]);
75
76
 
77
+ function runtimeStatistics(solver) {
78
+ return { ...solver.stats, ...memoryStatistics() };
79
+ }
80
+
81
+ function* statisticsBuiltin({ solver, env }) {
82
+ const stream = solver.io.resolve(solver.io.currentOutput);
83
+ if (stream?.type !== 'text') throw new PrologError('permission_error(output, binary_stream)');
84
+ solver.io.writeUnit(stream, 'eyeprolog stats:\n');
85
+ for (const [key, value] of Object.entries(runtimeStatistics(solver))) {
86
+ solver.io.writeUnit(stream, ` ${key}: ${value}\n`);
87
+ }
88
+ yield env;
89
+ }
90
+
91
+ function* statisticsValueBuiltin({ solver, goal, env }) {
92
+ const snapshot = runtimeStatistics(solver);
93
+ const key = deref(goal.args[0], env);
94
+ const entries = key.type === VAR
95
+ ? Object.entries(snapshot)
96
+ : key.type === ATOM && Object.hasOwn(snapshot, key.name)
97
+ ? [[key.name, snapshot[key.name]]]
98
+ : null;
99
+
100
+ if (entries == null) {
101
+ if (key.type !== ATOM) throw new PrologError('type_error(atom)', key);
102
+ return;
103
+ }
104
+
105
+ for (const [name, value] of entries) {
106
+ const next = env.clone();
107
+ if (unify(goal.args[0], atom(name), next) && unify(goal.args[1], numberTerm(value), next)) yield next;
108
+ }
109
+ }
110
+
76
111
  export function createEyePrologRegistry() {
77
112
  const registry = createDefaultRegistry();
113
+ registry.add('statistics', 0, statisticsBuiltin, { deterministic: true });
114
+ registry.add('statistics', 2, statisticsValueBuiltin);
78
115
  eyePrologLibraryBuiltins.register(registry);
79
116
  clpzBuiltins.register(registry);
80
117
  registry.eyePrologLibrary = true;
@@ -692,6 +692,108 @@ c4 ?- call((!;1)).
692
692
  }
693
693
  },
694
694
  },
695
+ {
696
+ name: 'read/2 and number_chars/2 agree on bounded numeric syntax (issue #29)',
697
+ run: () => {
698
+ const registry = createDefaultRegistry();
699
+ const numberChars = registry.get('number_chars', 2).handler;
700
+ const read = registry.get('read', 2).handler;
701
+ const solver = new Solver(Program.parse(''), { registry, ioOptions: { input: '' } });
702
+ const stream = solver.io.resolve(0);
703
+ const streamTerm = compound('$stream', [numberTerm('0')]);
704
+ const alphabet = ['0', '1', '2', '7', '8', '9', 'a', 'f', 'x', 'e', 'E', '+', '-', '.', "'", '\\', ' '];
705
+ let checked = 0;
706
+ let accepted = 0;
707
+
708
+ const visit = (prefix, remaining) => {
709
+ if (remaining === 0) {
710
+ checked++;
711
+ const converted = variable('Converted');
712
+ const conversionGoal = compound('number_chars', [
713
+ converted,
714
+ listFromItems(Array.from(prefix, atom)),
715
+ ]);
716
+ let conversion;
717
+ try {
718
+ conversion = numberChars({ goal: conversionGoal, env: new Env() }).next();
719
+ } catch (_) {
720
+ return;
721
+ }
722
+ if (conversion.done) return;
723
+ accepted++;
724
+ const expected = copyResolved(converted, conversion.value);
725
+
726
+ stream.content = `${prefix}. `;
727
+ stream.position = 0;
728
+ stream.pastEnd = false;
729
+ const readValue = variable('ReadValue');
730
+ const readGoal = compound('read', [streamTerm, readValue]);
731
+ const answer = read({ solver, goal: readGoal, env: new Env() }).next();
732
+ if (answer.done) throw new Error(`read/2 rejected number_chars/2 spelling ${JSON.stringify(prefix)}`);
733
+ const actual = copyResolved(readValue, answer.value);
734
+ assertEqual(actual.type, 'number', `read/2 type for ${JSON.stringify(prefix)}`);
735
+ assertEqual(actual.name, expected.name, `read/2 value for ${JSON.stringify(prefix)}`);
736
+ return;
737
+ }
738
+ for (const character of alphabet) visit(prefix + character, remaining - 1);
739
+ };
740
+
741
+ for (let length = 1; length <= 4; length++) visit('', length);
742
+ assertEqual(checked, 88740, 'bounded numeric spellings checked');
743
+ if (accepted < 1000) throw new Error(`unexpectedly small accepted numeric corpus: ${accepted}`);
744
+ },
745
+ },
746
+ {
747
+ name: 'number_chars/2 to read/2 cross-check stays bounded under a small heap (issue #29)',
748
+ run: () => {
749
+ const engineUrl = new URL('../src/index.js', import.meta.url).href;
750
+ const script = `
751
+ import {
752
+ Program, Solver, Env, atom, compound, variable, listFromItems,
753
+ numberTerm, copyResolved, createDefaultRegistry,
754
+ } from ${JSON.stringify(engineUrl)};
755
+ const registry = createDefaultRegistry();
756
+ const numberChars = registry.get('number_chars', 2).handler;
757
+ const read = registry.get('read', 2).handler;
758
+ const solver = new Solver(Program.parse(''), { registry, ioOptions: { input: '' } });
759
+ const stream = solver.io.resolve(0);
760
+ const streamTerm = compound('$stream', [numberTerm('0')]);
761
+ for (let i = 0; i < 250000; i++) {
762
+ const text = String(i);
763
+ const converted = variable('Converted');
764
+ const conversionGoal = compound('number_chars', [
765
+ converted,
766
+ listFromItems(Array.from(text, atom)),
767
+ ]);
768
+ const conversion = numberChars({ goal: conversionGoal, env: new Env() }).next();
769
+ if (conversion.done) throw new Error('number_chars/2 failed at ' + i);
770
+ const expected = copyResolved(converted, conversion.value);
771
+
772
+ stream.content = text + '. ';
773
+ stream.position = 0;
774
+ stream.pastEnd = false;
775
+ const readValue = variable('ReadValue');
776
+ const readGoal = compound('read', [streamTerm, readValue]);
777
+ const answer = read({ solver, goal: readGoal, env: new Env() }).next();
778
+ if (answer.done) throw new Error('read/2 failed at ' + i);
779
+ const actual = copyResolved(readValue, answer.value);
780
+ if (actual.type !== 'number' || actual.name !== expected.name) {
781
+ throw new Error('number mismatch at ' + i + ': ' + actual.name + ' != ' + expected.name);
782
+ }
783
+ }
784
+ process.stdout.write('250000');
785
+ `;
786
+ const result = spawnSync(process.execPath, [
787
+ '--max-old-space-size=32',
788
+ '--input-type=module',
789
+ '--eval',
790
+ script,
791
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
792
+ if (result.error) throw result.error;
793
+ assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
794
+ assertEqual(result.stdout, '250000', 'number_chars/read cross-check count');
795
+ },
796
+ },
695
797
  {
696
798
  name: 'number syntax and number_chars normalize floating-point negative zero',
697
799
  run: () => {
@@ -1975,6 +2077,36 @@ c4 ?- call((!;1)).
1975
2077
  assertIncludes(result.stderr, ' solve_goals_calls:', 'stderr');
1976
2078
  },
1977
2079
  },
2080
+ {
2081
+ name: 'statistics/0 prints snapshots during execution',
2082
+ run: () => {
2083
+ const input = '%% goal: live\nlive :- statistics, statistics.\n';
2084
+ const result = runCli(['-'], { input });
2085
+ assertEqual(result.status, 0, 'exit status');
2086
+ assertEqual((result.stdout.match(/eyeprolog stats:/g) ?? []).length, 2, 'in-run snapshot count');
2087
+ assertIncludes(result.stdout, ' memory_guard_used_bytes:', 'stdout');
2088
+ assertIncludes(result.stdout, 'live.\n', 'stdout');
2089
+ assertEqual(result.stderr, '', 'stderr');
2090
+ },
2091
+ },
2092
+ {
2093
+ name: 'statistics/2 exposes current counters and memory values',
2094
+ run: () => {
2095
+ const input = '%% goal: live(Used)\nlive(Used) :- statistics(memory_guard_used_bytes, Used).\n';
2096
+ const result = runCli(['-'], { input });
2097
+ assertEqual(result.status, 0, 'exit status');
2098
+ assertEqual(/^live\(\d+\)\.\n$/.test(result.stdout), true, 'numeric memory statistic');
2099
+ assertEqual(result.stderr, '', 'stderr');
2100
+ },
2101
+ },
2102
+ {
2103
+ name: 'statistics predicates are excluded from strict ISO mode',
2104
+ run: () => {
2105
+ const result = runCli(['--iso-strict', '-'], { input: '%% goal: statistics\n' });
2106
+ assertEqual(result.status, 1, 'exit status');
2107
+ assertIncludes(result.stderr, 'existence_error(procedure)', 'stderr');
2108
+ },
2109
+ },
1978
2110
  {
1979
2111
  name: '--warnings prints unstratified negation diagnostics without failing',
1980
2112
  run: () => {
@@ -3205,9 +3337,13 @@ open(X) :- candidate(X), \\+ closed(X).
3205
3337
  assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
3206
3338
  assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
3207
3339
  assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
3208
- assertEqual(library.defs.size, 153, 'EyeProlog registry contains ISO definitions and private library adapters');
3340
+ assertEqual(library.defs.size, 155, 'EyeProlog registry contains ISO definitions, observability extensions, and private library adapters');
3209
3341
  assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
3210
3342
  assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
3343
+ assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
3344
+ assertEqual(registry.get('statistics', 2), null, 'statistics/2 is absent from the ISO registry');
3345
+ assertEqual(Boolean(library.get('statistics', 0)), true, 'statistics/0 is an EyeProlog observability extension');
3346
+ assertEqual(Boolean(library.get('statistics', 2)), true, 'statistics/2 is an EyeProlog observability extension');
3211
3347
  assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
3212
3348
  assertEqual(eyePrologPortableLibraryIndicators.length, 59, 'portable Prolog library count');
3213
3349
  assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
@@ -4635,9 +4635,20 @@ may indicate a generator that should be constrained. Many table hits may show
4635
4635
  effective reuse; many distinct table entries may reveal an argument that
4636
4636
  prevents calls from sharing. On the Node CLI it also reports current heap use,
4637
4637
  non-young/old-generation use, the amount currently compared with the memory
4638
- guard, resident-set size, and the soft and hard memory ceilings in bytes. These memory figures are printed even when execution ends by
4639
- raising a Prolog error, which makes `--stats` useful when reproducing bounded
4640
- memory failures such as long-running conversion stress tests.
4638
+ guard, resident-set size, and the soft and hard memory ceilings in bytes. These
4639
+ memory figures are printed even when execution ends by raising a Prolog error.
4640
+
4641
+ `--stats` is an end-of-run summary. For a deliberately non-terminating or very
4642
+ long computation, call the EyeProlog extension `statistics/0` at the points
4643
+ where a live snapshot is useful. For example, a long-running loop can include
4644
+ `statistics` as one of its goals, as in `loop :- work, statistics, loop.`
4645
+
4646
+ `statistics/0` writes the current solver counters and memory figures
4647
+ immediately to the current output stream. `statistics/2` makes an individual
4648
+ value available to the program, for example
4649
+ `statistics(memory_guard_used_bytes, Used)`. With an unbound first argument it
4650
+ enumerates the available statistic keys and values. These predicates are
4651
+ EyeProlog observability extensions and are not available under `--iso-strict`.
4641
4652
 
4642
4653
  Compare statistics only between runs with the same query, data, and observable
4643
4654
  answer contract. A faster program that silently loses answers is not an
@@ -6410,7 +6421,7 @@ make the observed question explicit.
6410
6421
  | `-p`, `--proof` | Print `why/2` explanations |
6411
6422
  | `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
6412
6423
  | `--iso-strict` | Restrict parsing and execution to ISO/IEC 13211-1:1995 + Corrigenda 1–3; reject EyeProlog language extensions and disable automatic tabling |
6413
- | `-s`, `--stats` | Print solver counters to stderr |
6424
+ | `-s`, `--stats` | Print final solver and memory statistics to stderr after execution |
6414
6425
  | `-v`, `--version` | Print the package version |
6415
6426
  | `-w`, `--warnings` | Print non-fatal portability warnings |
6416
6427
  | `-g`, `--goal Goal` | Solve a callable goal; may be repeated; overrides `%% goal:` comments |