eyeprolog 1.5.36 → 1.5.38

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/README.md CHANGED
@@ -86,7 +86,7 @@ The checked [Symbiotic Knowledge Graphs example](examples/symbiotic-knowledge-gr
86
86
  The same RDF → Prolog → RDF boundary is exercised by five additional checked scenarios: [cross-organization data sharing](https://eyereasoner.github.io/eyeprolog/examples/deck/cross-organization-data-sharing), [explainable EV-depot configuration](https://eyereasoner.github.io/eyeprolog/examples/deck/explainable-ev-depot-configuration), [operational incident response](https://eyereasoner.github.io/eyeprolog/examples/deck/operational-incident-response), [software supply-chain vulnerability response](https://eyereasoner.github.io/eyeprolog/examples/deck/sbom-vulnerability-response), and a [scientific evidence graph](https://eyereasoner.github.io/eyeprolog/examples/deck/scientific-evidence-graph). Together they cover policy decisions, reversible configuration reasoning, dependency-graph diagnosis, transitive SBOM exposure, and evidence aggregation with explicit disagreement.
87
87
 
88
88
  ## Benchmarks
89
- EyeProlog has 21 checksum-protected wall-clock benchmarks spanning recursion/indexing, constraints, tabling/WFS, DCGs, Eyelet, search, term I/O, attributes, rewriting, and the classic Prolog naive-reverse workload. Short workloads are adaptively batched before timing so millisecond-scale noise is not mistaken for a regression. Run `npm run benchmark`; create a machine-local comparison point with `npm run benchmark:baseline`; use `npm run test:benchmark` for harness checks. The checked [`examples/bench.pl`](examples/bench.pl) preserves the classic Quintus 1984 `nrev/2` workload on a 30-element list. For a comparable LIPS number, run `npm run benchmark:lips`: it executes the classic failure-driven `dobench/1` and `dodummy/1` loops in Prolog, subtracts dummy-loop CPU time, and applies the historical 496 procedure calls per reversal. The generic benchmark table still shows a quick wall-clock LIPS estimate for `classic-nrev`, but `benchmark:lips` is the canonical engine-speed measurement. LIPS is a historical basic-engine-speed indicator, not a whole-system performance score. Details are in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
89
+ EyeProlog has 19 checksum-protected wall-clock benchmarks spanning recursion/indexing, constraints, tabling/WFS, DCGs, Eyelet, search, term I/O, attributes, rewriting, and the classic Prolog naive-reverse workload. Short workloads are adaptively batched before timing so millisecond-scale noise is not mistaken for a regression. Run `npm run benchmark`; create a machine-local comparison point with `npm run benchmark:baseline`; use `npm run test:benchmark` for harness checks. The checked [`examples/bench.pl`](examples/bench.pl) preserves the classic Quintus 1984 `nrev/2` workload on a 30-element list. For a comparable LIPS number, run `npm run benchmark:lips`: it executes the classic failure-driven `dobench/1` and `dodummy/1` loops in Prolog, subtracts dummy-loop CPU time, and applies the historical 496 procedure calls per reversal. The generic benchmark table still shows a quick wall-clock LIPS estimate for `classic-nrev`, but `benchmark:lips` is the canonical engine-speed measurement. LIPS is a historical basic-engine-speed indicator, not a whole-system performance score. Details are in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
90
90
  For the project policy on post-ISO-standard and WG17 compatibility features such as digit separators, see [ISO/WG17 compatibility extensions](test/conformance/ISO-WG17-EXTENSIONS.md).
91
91
  ## Development
92
92
  ```sh
@@ -0,0 +1,42 @@
1
+ % Bulk character-by-character writes to a non-console text stream.
2
+ %
3
+ % put_char/2 writes one character at a time to an open file stream, and each
4
+ % write logically appends at the current stream position (ISO 7.10.2.8). This
5
+ % is the common pattern for hand-rolled serializers that emit one character or
6
+ % code point per call rather than a single bulk write/1 call. The example
7
+ % writes a repeating a-z run to a temporary file, closes it, then reopens and
8
+ % reads the file back one character at a time with get_char/2 to confirm every
9
+ % character round-trips. The path is under /tmp so the source tree is
10
+ % unchanged.
11
+
12
+ %% goal: bulk_write_result(X0, X1)
13
+
14
+ bulk_write_chars(_, 0) :- !.
15
+ bulk_write_chars(Stream, N) :-
16
+ N > 0,
17
+ Code is 0'a + (N mod 26),
18
+ char_code(Char, Code),
19
+ put_char(Stream, Char),
20
+ N1 is N - 1,
21
+ bulk_write_chars(Stream, N1).
22
+
23
+ count_chars(Stream, Acc, Count) :-
24
+ get_char(Stream, Char),
25
+ count_chars_step(Char, Stream, Acc, Count).
26
+
27
+ count_chars_step(end_of_file, _, Count, Count) :- !.
28
+ count_chars_step(_, Stream, Acc, Count) :-
29
+ Acc1 is Acc + 1,
30
+ count_chars(Stream, Acc1, Count).
31
+
32
+ bulk_write_path('/tmp/eyeprolog-bulk-stream-write-example.txt').
33
+
34
+ bulk_write_result(Requested, Counted) :-
35
+ Requested = 5000,
36
+ bulk_write_path(Path),
37
+ open(Path, write, Out, [type(text)]),
38
+ bulk_write_chars(Out, Requested),
39
+ close(Out),
40
+ open(Path, read, In, [type(text)]),
41
+ count_chars(In, 0, Counted),
42
+ close(In).
@@ -0,0 +1 @@
1
+ bulk_write_result(5000, 5000).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.36",
6
+ "version": "1.5.38",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/playground.html CHANGED
@@ -459,6 +459,7 @@
459
459
  "bmi",
460
460
  "braking-safety-worlds",
461
461
  "buck-converter-design",
462
+ "bulk-stream-write",
462
463
  "cache-performance",
463
464
  "canary-release",
464
465
  "cat-koko",
package/src/explain.js CHANGED
@@ -218,7 +218,12 @@ function builtinIsUsedForGoal(def, solver, goal, env) {
218
218
  function selectReadyDeterministicBuiltin(goals, env, registry) {
219
219
  for (let i = 0; i < goals.length; i++) {
220
220
  const goal = goals[i];
221
- if (goal.type !== COMPOUND) continue;
221
+ // Match solver.js's goal-type check: a 0-arity builtin (ATOM) is just as
222
+ // eligible for this fast path as a COMPOUND one. No bundled builtin
223
+ // currently pairs arity 0 with a custom ready() gate, so this has no
224
+ // observable effect today, but it keeps the proof-explanation path from
225
+ // silently diverging from actual execution if one is added later.
226
+ if (goal.type !== COMPOUND && goal.type !== ATOM) continue;
222
227
  const def = registry.get(goal.name, goal.arity);
223
228
  if (!def?.deterministic || typeof def.ready !== 'function') continue;
224
229
  if (typeof def.shouldUse === 'function') continue;
package/src/io.js CHANGED
@@ -186,6 +186,21 @@ export class StreamManager {
186
186
  }
187
187
  const text = String(value);
188
188
  const content = String(stream.content);
189
+ // The overwhelming majority of text-stream writes append at the current
190
+ // end of the sink: bulk write/1 calls and repeated put_char/put_code
191
+ // loops alike. Handling that case as a plain concatenation, separate
192
+ // from the general reposition rebuild below, keeps the two ISO-distinct
193
+ // cases self-documenting instead of folding them into one three-part
194
+ // slice expression that always pays for the overwrite case's shape.
195
+ // This split is for clarity, not raw throughput: measured end to end,
196
+ // the general expression already performs comparably for this pattern.
197
+ if (stream.position === content.length) {
198
+ stream.content = content + text;
199
+ stream.position += text.length;
200
+ return;
201
+ }
202
+ // 7.10.2.8: output after repositioning overwrites the existing sink
203
+ // contents at the selected stream position rather than always appending.
189
204
  const end = Math.min(content.length, stream.position + text.length);
190
205
  stream.content = `${content.slice(0, stream.position)}${text}${content.slice(end)}`;
191
206
  stream.position += text.length;
package/src/lib/dates.pl CHANGED
@@ -66,14 +66,13 @@ dates__date_not_before(EY, _, _, SY, _, _) :- EY > SY.
66
66
  dates__date_not_before(Y, EM, _, Y, SM, _) :- EM > SM.
67
67
  dates__date_not_before(Y, M, ED, Y, M, SD) :- ED >= SD.
68
68
 
69
- dates__borrow_days(EY, EM, ED, SD, EY, EM, ED) :- ED >= SD.
69
+ dates__borrow_days(EY, EM, ED, SD, EY, EM, ED) :- ED >= SD, !.
70
70
  dates__borrow_days(EY0, EM0, ED0, SD, EY, EM, ED) :-
71
71
  ED0 < SD,
72
72
  dates__previous_month(EY0, EM0, PY, PM),
73
73
  dates__days_in_month(PY, PM, Days),
74
- ED is ED0 + Days,
75
- EY = PY,
76
- EM = PM.
74
+ ED1 is ED0 + Days,
75
+ dates__borrow_days(PY, PM, ED1, SD, EY, EM, ED).
77
76
 
78
77
  dates__borrow_months(EY, EM, SM, EY, EM) :- EM >= SM.
79
78
  dates__borrow_months(EY0, EM0, SM, EY, EM) :-
@@ -72,15 +72,6 @@
72
72
  ],
73
73
  "expectedSha256": "b4a153234f3daf1e2ba6c26843349a1bc1919ebd9f9e98559835eb5ef915716b"
74
74
  },
75
- {
76
- "name": "dcg-command",
77
- "group": "dcg",
78
- "file": "examples/dcg-command-parser.pl",
79
- "goals": [
80
- "dcg_example(X0, X1)"
81
- ],
82
- "expectedSha256": "6f640fb0d327eb9139d3568cf7a86ad55a52afdbb0208ca35a70419be2bf1b9c"
83
- },
84
75
  {
85
76
  "name": "dcg-expression",
86
77
  "group": "dcg",
@@ -106,24 +97,6 @@
106
97
  ],
107
98
  "expectedSha256": "a7c33872b86aa7877b9c38ec41c447fc96a328d40f512428993d662f28a097ad"
108
99
  },
109
- {
110
- "name": "clpz-register-allocation",
111
- "group": "clpz",
112
- "file": "examples/register-allocation.pl",
113
- "goals": [
114
- "registerAnswer(X0, X1)"
115
- ],
116
- "expectedSha256": "1ed0b4a90deff2908463774d0025210d58b89366efb5d82b94ff5b51daca046b"
117
- },
118
- {
119
- "name": "clpb-feature-model",
120
- "group": "clpb",
121
- "file": "examples/clpb-feature-model.pl",
122
- "goals": [
123
- "feature_plan(X0)"
124
- ],
125
- "expectedSha256": "47abfd8a87efb595ffb11c6c8cbda3cb5855e95124bc574e3cae387c12318f33"
126
- },
127
100
  {
128
101
  "name": "attributed-variables",
129
102
  "group": "attributes",
@@ -190,5 +163,14 @@
190
163
  "type_answer(X0, X1)"
191
164
  ],
192
165
  "expectedSha256": "acad9ddb995d9ddef091e17cc91659d36ed7f9014d6714fd40100d85a53ae203"
166
+ },
167
+ {
168
+ "name": "bulk-stream-write",
169
+ "group": "term-io",
170
+ "file": "examples/bulk-stream-write.pl",
171
+ "goals": [
172
+ "bulk_write_result(X0, X1)"
173
+ ],
174
+ "expectedSha256": "8471da130a4a3a92dd31ee6cf285b1e129886177d7be367d51650a48b8920324"
193
175
  }
194
176
  ]
@@ -1,2 +1,2 @@
1
1
  answer(duration, 'P4Y1D').
2
- answer(month_borrow, 'P1M-1D').
2
+ answer(month_borrow, 'P30D').
@@ -42,7 +42,7 @@ function runWorker(item) {
42
42
  ]);
43
43
  }
44
44
 
45
- ok(Array.isArray(manifest) && manifest.length === 21, 'benchmark manifest should contain exactly 21 representative workloads');
45
+ ok(Array.isArray(manifest) && manifest.length === 19, 'benchmark manifest should contain exactly 19 representative workloads');
46
46
  ok(new Set(manifest.map((item) => item.name)).size === manifest.length, 'benchmark names should be unique');
47
47
  const classicNrev = manifest.find((item) => item.name === 'classic-nrev');
48
48
  ok(classicNrev?.logicalInferences === 496, 'classic nrev should retain the traditional 496-call LIPS accounting');
@@ -61,7 +61,7 @@ for (const item of manifest) {
61
61
 
62
62
  const adaptive = await spawnJson([
63
63
  path.join(root, 'test', 'benchmark.mjs'),
64
- '--filter', 'dcg-command',
64
+ '--filter', 'dcg-expression',
65
65
  '--runs', '1',
66
66
  '--warmup', '0',
67
67
  '--target-ms', '50',
@@ -69,7 +69,7 @@ const adaptive = await spawnJson([
69
69
  ]);
70
70
  ok(adaptive.results.length === 1, 'adaptive benchmark smoke test should select one workload');
71
71
  ok(adaptive.results[0].batchSize > 1, 'adaptive benchmark smoke test should batch a short workload');
72
- ok(adaptive.results[0].sha256 === manifest.find((item) => item.name === 'dcg-command').expectedSha256,
72
+ ok(adaptive.results[0].sha256 === manifest.find((item) => item.name === 'dcg-expression').expectedSha256,
73
73
  'adaptive batching should preserve the semantic checksum');
74
74
 
75
75
  const nrev = await spawnJson([
@@ -5867,9 +5867,9 @@ and reports the portable ISO `type_error(list)` error term.
5867
5867
  #### A bidirectional expression grammar
5868
5868
 
5869
5869
  DCGs become more useful when the grammar produces a structured term rather than
5870
- merely accepting a token list. The checked
5870
+ merely accepting a token list. The checked
5871
5871
  [`dcg-expression-language.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dcg-expression-language.pl)
5872
- example implements a small arithmetic language in both directions. Its parser
5872
+ example implements a small arithmetic language in both directions. Its parser
5873
5873
  respects precedence and left associativity while constructing an abstract syntax
5874
5874
  tree:
5875
5875
 
@@ -5887,17 +5887,17 @@ additive_tail(AST, AST) --> [].
5887
5887
 
5888
5888
  The accumulator removes left recursion without moving parsing into JavaScript.
5889
5889
  A second DCG walks the AST in the other direction and emits only the parentheses
5890
- needed to preserve its structure. The example therefore exercises parsing,
5890
+ needed to preserve its structure. The example therefore exercises parsing,
5891
5891
  semantic actions, nonterminal-to-nonterminal state hand-off, generation,
5892
5892
  backtracking, `phrase/3` remainder handling, and AST-to-token-to-AST
5893
- round-tripping. The checked answers are in
5893
+ round-tripping. The checked answers are in
5894
5894
  [`examples/output/dcg-expression-language.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dcg-expression-language.pl).
5895
5895
 
5896
5896
  #### Deep sequence hand-off
5897
5897
 
5898
5898
  `library(iso_ext)` provides the common `... //0` helper, which describes an
5899
- arbitrary number of input elements. It is not part of ISO Part 3, but it is a
5900
- useful interoperability and stress-test relation. A compact hand-off test is:
5899
+ arbitrary number of input elements. It is not part of ISO Part 3, but it is a
5900
+ useful interoperability and stress-test relation. A compact hand-off test is:
5901
5901
 
5902
5902
  ```text
5903
5903
  a --> ..., epsilon.
@@ -5905,11 +5905,11 @@ epsilon --> [].
5905
5905
  ```
5906
5906
 
5907
5907
  Here the remaining sequence is repeatedly passed from `... //0` to another
5908
- nonterminal. For a finite compact list, EyeProlog can scan the arbitrary
5908
+ nonterminal. For a finite compact list, EyeProlog can scan the arbitrary
5909
5909
  sequence iteratively instead of consuming one ordinary solver depth level per
5910
- list cell. If the continuation is structurally proven to be a zero-width
5910
+ list cell. If the continuation is structurally proven to be a zero-width
5911
5911
  identity grammar such as `epsilon//0`, the hand-off can be continued without
5912
- constructing a fresh general clause-resolution frame at every suffix. The list
5912
+ constructing a fresh general clause-resolution frame at every suffix. The list
5913
5913
  spine is still traversed; this is a control/allocation optimization rather than
5914
5914
  an O(1) semantic shortcut.
5915
5915
 
@@ -6062,7 +6062,8 @@ contains 129 name/arity entries across 100 names.
6062
6062
  `;/2` recognizes an `->/2` term on its left and implements the ISO
6063
6063
  if-then-else commitment described above. Cuts and committed conditions are
6064
6064
  operational controls; use ordinary relations when all alternatives should
6065
- remain observable.
6065
+ remain observable.
6066
+
6066
6067
  #### Definite clause grammar processing
6067
6068
 
6068
6069
  - **`phrase(+Body,?Sequence)`** — Parses or generates `Sequence` with a Part 3 grammar body and requires complete consumption.
@@ -9743,7 +9744,7 @@ Review questions:
9743
9744
  </figure>
9744
9745
 
9745
9746
  The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
9746
- top-level directory contains **225 self-contained runnable programs**. Every
9747
+ top-level directory contains **226 self-contained runnable programs**. Every
9747
9748
  source program has an exact answer file under
9748
9749
  [examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
9749
9750
  explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic lists link every top-level program and open the program
@@ -10176,7 +10177,7 @@ npm run benchmark:baseline
10176
10177
  npm run benchmark:lips
10177
10178
  ```
10178
10179
 
10179
- The benchmark suite contains 21 representative workloads and stores their
10180
+ The benchmark suite contains 19 representative workloads and stores their
10180
10181
  semantic output digests in the repository, while wall-clock baselines remain
10181
10182
  machine-local under `.benchmarks/` because absolute timings are machine-specific.
10182
10183
  Each benchmark runs in its own fresh Node worker. Inside that worker, one untimed
@@ -10368,7 +10369,7 @@ specifications.
10368
10369
 
10369
10370
  - Kurt Gödel,
10370
10371
  [“Über formal unentscheidbare Sätze der *Principia Mathematica* und
10371
- verwandter Systeme I”](https://doi.org/10.1007/BF01700686),
10372
+ verwandter Systeme I”](https://doi.org/10.1007/BF01700692),
10372
10373
  *Monatshefte für Mathematik und Physik* 38, 1931, pp. 173–198. The
10373
10374
  incompleteness theorems establish intrinsic limits for sufficiently
10374
10375
  expressive effectively axiomatized formal systems. Chapter 30 treats such