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 +1 -1
- package/src/iso.js +9 -0
- package/src/standard-library.js +39 -2
- package/test/run-regression.mjs +137 -1
- package/the-art-of-eyeprolog.md +15 -4
package/package.json
CHANGED
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,
|
package/src/standard-library.js
CHANGED
|
@@ -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;
|
package/test/run-regression.mjs
CHANGED
|
@@ -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,
|
|
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');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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
|
|
4639
|
-
|
|
4640
|
-
|
|
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
|
|
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 |
|