eyelang 1.1.12 → 1.1.14
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/playground.html +460 -91
- package/src/solver.js +162 -34
- package/test/run-regression.mjs +53 -2
package/package.json
CHANGED
package/playground.html
CHANGED
|
@@ -16,15 +16,24 @@
|
|
|
16
16
|
--accent: #0969da;
|
|
17
17
|
--accent-strong: #0757b8;
|
|
18
18
|
--danger: #b42318;
|
|
19
|
+
--editor-bg: #ffffff;
|
|
20
|
+
--editor-text: #172033;
|
|
21
|
+
--editor-caret: #111827;
|
|
22
|
+
--editor-selection: rgba(9, 105, 218, 0.18);
|
|
23
|
+
--editor-error-line: #fee2e2;
|
|
24
|
+
--editor-error-border: #dc2626;
|
|
25
|
+
--line-number-bg: #f8fafc;
|
|
26
|
+
--line-number-text: #64748b;
|
|
27
|
+
--line-number-border: #e2e8f0;
|
|
19
28
|
--code-bg: #0f172a;
|
|
20
29
|
--code-text: #dbeafe;
|
|
21
|
-
--comment: #
|
|
22
|
-
--string: #
|
|
23
|
-
--number: #
|
|
24
|
-
--variable: #
|
|
25
|
-
--keyword: #
|
|
26
|
-
--predicate: #
|
|
27
|
-
--punctuation: #
|
|
30
|
+
--comment: #64748b;
|
|
31
|
+
--string: #047857;
|
|
32
|
+
--number: #b45309;
|
|
33
|
+
--variable: #1d4ed8;
|
|
34
|
+
--keyword: #7c3aed;
|
|
35
|
+
--predicate: #be123c;
|
|
36
|
+
--punctuation: #475569;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
@media (prefers-color-scheme: dark) {
|
|
@@ -50,15 +59,15 @@
|
|
|
50
59
|
line-height: 1.45;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
header
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
top: 0;
|
|
59
|
-
z-index: 5;
|
|
62
|
+
header,
|
|
63
|
+
main,
|
|
64
|
+
footer {
|
|
65
|
+
width: min(100% - 2rem, 72rem);
|
|
66
|
+
margin: 0 auto;
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
header { padding: 1.25rem 0 0.9rem; }
|
|
70
|
+
|
|
62
71
|
h1 {
|
|
63
72
|
margin: 0;
|
|
64
73
|
font-size: clamp(1.35rem, 4vw, 2rem);
|
|
@@ -67,21 +76,17 @@
|
|
|
67
76
|
header p { margin: 0.35rem 0 0; color: var(--muted); }
|
|
68
77
|
|
|
69
78
|
main {
|
|
70
|
-
display:
|
|
71
|
-
|
|
72
|
-
gap: 1rem;
|
|
73
|
-
padding: 1rem clamp(1rem, 4vw, 2rem) 2rem;
|
|
74
|
-
max-width: 1500px;
|
|
75
|
-
margin: 0 auto;
|
|
79
|
+
display: block;
|
|
80
|
+
padding: 0 0 2rem;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
section
|
|
79
|
-
aside {
|
|
83
|
+
section {
|
|
80
84
|
background: var(--panel);
|
|
81
85
|
border: 1px solid var(--border);
|
|
82
86
|
border-radius: 0.9rem;
|
|
83
87
|
box-shadow: 0 8px 28px rgba(15, 23, 42, 0.06);
|
|
84
88
|
overflow: hidden;
|
|
89
|
+
margin: 1rem 0;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
.panel-body { padding: 1rem; }
|
|
@@ -89,7 +94,8 @@
|
|
|
89
94
|
.toolbar,
|
|
90
95
|
.example-row,
|
|
91
96
|
.button-row,
|
|
92
|
-
.option-row
|
|
97
|
+
.option-row,
|
|
98
|
+
.background-row {
|
|
93
99
|
display: flex;
|
|
94
100
|
flex-wrap: wrap;
|
|
95
101
|
gap: 0.6rem;
|
|
@@ -102,6 +108,18 @@
|
|
|
102
108
|
background: var(--panel-soft);
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
.toolbar:last-child { border-bottom: 0; }
|
|
112
|
+
|
|
113
|
+
details summary {
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
font-weight: 700;
|
|
116
|
+
padding: 0.85rem 1rem;
|
|
117
|
+
background: var(--panel-soft);
|
|
118
|
+
border-bottom: 1px solid var(--border);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
details:not([open]) summary { border-bottom: 0; }
|
|
122
|
+
|
|
105
123
|
label { font-weight: 650; }
|
|
106
124
|
|
|
107
125
|
select,
|
|
@@ -147,7 +165,8 @@
|
|
|
147
165
|
button.primary:hover { background: var(--accent-strong); }
|
|
148
166
|
button:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
149
167
|
|
|
150
|
-
.option-row label
|
|
168
|
+
.option-row label,
|
|
169
|
+
.background-row label {
|
|
151
170
|
display: inline-flex;
|
|
152
171
|
gap: 0.35rem;
|
|
153
172
|
align-items: center;
|
|
@@ -168,17 +187,56 @@
|
|
|
168
187
|
|
|
169
188
|
.editor {
|
|
170
189
|
position: relative;
|
|
171
|
-
min-height:
|
|
172
|
-
background: var(--
|
|
190
|
+
min-height: 56vh;
|
|
191
|
+
background: var(--editor-bg);
|
|
173
192
|
overflow: hidden;
|
|
174
193
|
}
|
|
175
194
|
|
|
195
|
+
.line-numbers,
|
|
196
|
+
.editor pre,
|
|
197
|
+
.editor textarea {
|
|
198
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
199
|
+
font-size: 0.95rem;
|
|
200
|
+
line-height: 1.55;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.line-numbers {
|
|
204
|
+
position: absolute;
|
|
205
|
+
inset: 0 auto 0 0;
|
|
206
|
+
width: 3.6rem;
|
|
207
|
+
padding: 1rem 0.65rem 1rem 0.35rem;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
text-align: right;
|
|
210
|
+
color: var(--line-number-text);
|
|
211
|
+
background: var(--line-number-bg);
|
|
212
|
+
border-right: 1px solid var(--line-number-border);
|
|
213
|
+
user-select: none;
|
|
214
|
+
pointer-events: none;
|
|
215
|
+
z-index: 3;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.line-numbers-inner { will-change: transform; }
|
|
219
|
+
|
|
220
|
+
.line-number {
|
|
221
|
+
display: block;
|
|
222
|
+
min-height: 1.55em;
|
|
223
|
+
padding-right: 0.1rem;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.line-number.error {
|
|
227
|
+
color: var(--danger);
|
|
228
|
+
background: var(--editor-error-line);
|
|
229
|
+
font-weight: 700;
|
|
230
|
+
margin: 0 -0.65rem 0 -0.35rem;
|
|
231
|
+
padding: 0 0.65rem 0 0.35rem;
|
|
232
|
+
}
|
|
233
|
+
|
|
176
234
|
.editor pre,
|
|
177
235
|
.editor textarea {
|
|
178
236
|
position: absolute;
|
|
179
237
|
inset: 0;
|
|
180
238
|
margin: 0;
|
|
181
|
-
padding: 1rem;
|
|
239
|
+
padding: 1rem 1rem 1rem 4.35rem;
|
|
182
240
|
border: 0;
|
|
183
241
|
width: 100%;
|
|
184
242
|
height: 100%;
|
|
@@ -186,25 +244,36 @@
|
|
|
186
244
|
overflow: auto;
|
|
187
245
|
white-space: pre;
|
|
188
246
|
tab-size: 2;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.error-line-marker {
|
|
250
|
+
position: absolute;
|
|
251
|
+
left: 0;
|
|
252
|
+
right: 0;
|
|
253
|
+
top: 0;
|
|
254
|
+
display: none;
|
|
255
|
+
pointer-events: none;
|
|
256
|
+
background: var(--editor-error-line);
|
|
257
|
+
border-left: 0.28rem solid var(--editor-error-border);
|
|
258
|
+
z-index: 0;
|
|
192
259
|
}
|
|
193
260
|
|
|
194
261
|
.editor pre {
|
|
195
262
|
pointer-events: none;
|
|
196
|
-
color: var(--
|
|
263
|
+
color: var(--editor-text);
|
|
264
|
+
z-index: 1;
|
|
197
265
|
}
|
|
198
266
|
|
|
199
267
|
.editor textarea {
|
|
200
268
|
background: transparent;
|
|
201
|
-
color: rgba(
|
|
202
|
-
caret-color:
|
|
269
|
+
color: rgba(17, 24, 39, 0.025);
|
|
270
|
+
caret-color: var(--editor-caret);
|
|
203
271
|
outline: none;
|
|
204
|
-
-
|
|
272
|
+
z-index: 2;
|
|
273
|
+
-webkit-text-fill-color: rgba(17, 24, 39, 0.025);
|
|
205
274
|
}
|
|
206
275
|
|
|
207
|
-
.editor textarea::selection { background:
|
|
276
|
+
.editor textarea::selection { background: var(--editor-selection); }
|
|
208
277
|
|
|
209
278
|
.tok-comment { color: var(--comment); font-style: italic; }
|
|
210
279
|
.tok-string { color: var(--string); }
|
|
@@ -250,31 +319,34 @@
|
|
|
250
319
|
|
|
251
320
|
footer {
|
|
252
321
|
color: var(--muted);
|
|
253
|
-
padding: 0
|
|
322
|
+
padding: 0 0 1.5rem;
|
|
254
323
|
text-align: center;
|
|
255
324
|
font-size: 0.95rem;
|
|
256
325
|
}
|
|
257
326
|
|
|
258
327
|
footer a { color: var(--accent); }
|
|
259
328
|
|
|
260
|
-
@media (max-width: 900px) {
|
|
261
|
-
header { position: static; }
|
|
262
|
-
main { grid-template-columns: 1fr; }
|
|
263
|
-
.editor { min-height: 54vh; }
|
|
264
|
-
.output { max-height: 42vh; }
|
|
265
|
-
}
|
|
266
|
-
|
|
267
329
|
@media (max-width: 560px) {
|
|
330
|
+
header,
|
|
331
|
+
main,
|
|
332
|
+
footer { width: min(100% - 1rem, 72rem); }
|
|
268
333
|
.toolbar,
|
|
269
334
|
.panel-body,
|
|
270
|
-
.editor-label
|
|
335
|
+
.editor-label,
|
|
336
|
+
details summary { padding: 0.8rem; }
|
|
271
337
|
button,
|
|
272
338
|
select,
|
|
273
339
|
input[type="url"],
|
|
274
340
|
input[type="search"] { width: 100%; }
|
|
275
341
|
.button-row > button { flex: 1 1 8rem; }
|
|
342
|
+
.editor { min-height: 58vh; }
|
|
343
|
+
.line-numbers,
|
|
276
344
|
.editor pre,
|
|
277
|
-
.editor textarea { font-size: 0.9rem;
|
|
345
|
+
.editor textarea { font-size: 0.9rem; }
|
|
346
|
+
.line-numbers { width: 3.2rem; padding: 0.8rem 0.55rem 0.8rem 0.25rem; }
|
|
347
|
+
.line-number.error { margin: 0 -0.55rem 0 -0.25rem; padding: 0 0.55rem 0 0.25rem; }
|
|
348
|
+
.editor pre,
|
|
349
|
+
.editor textarea { padding: 0.8rem 0.8rem 0.8rem 3.85rem; }
|
|
278
350
|
}
|
|
279
351
|
</style>
|
|
280
352
|
</head>
|
|
@@ -285,45 +357,62 @@
|
|
|
285
357
|
</header>
|
|
286
358
|
|
|
287
359
|
<main>
|
|
288
|
-
<section aria-labelledby="
|
|
360
|
+
<section aria-labelledby="examples-heading">
|
|
289
361
|
<div class="toolbar" aria-label="Example loader">
|
|
290
|
-
<label for="example-search">Load example</label>
|
|
362
|
+
<label id="examples-heading" for="example-search">Load an example</label>
|
|
291
363
|
<input id="example-search" type="search" placeholder="Filter examples" autocomplete="off">
|
|
292
364
|
<select id="examples" aria-label="Examples"></select>
|
|
293
|
-
<button id="load-example" type="button">Load</button>
|
|
294
|
-
</div>
|
|
295
|
-
|
|
296
|
-
<div class="toolbar" aria-label="URL loader">
|
|
297
|
-
<label for="source-url">Load from URL</label>
|
|
298
|
-
<input id="source-url" type="url" placeholder="https://example.org/program.pl">
|
|
299
|
-
<button id="load-url" type="button">Load URL</button>
|
|
365
|
+
<button id="load-example" type="button">Load example</button>
|
|
300
366
|
</div>
|
|
367
|
+
</section>
|
|
301
368
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
<
|
|
305
|
-
|
|
369
|
+
<section aria-labelledby="advanced-heading">
|
|
370
|
+
<details>
|
|
371
|
+
<summary id="advanced-heading">⚙ Advanced configuration</summary>
|
|
372
|
+
<div class="toolbar" aria-label="URL loader">
|
|
373
|
+
<label for="source-url">Load Eyelang from URL</label>
|
|
374
|
+
<input id="source-url" type="url" placeholder="https://example.org/program.pl">
|
|
375
|
+
<button id="load-url" type="button">Load from URL</button>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="toolbar background-row">
|
|
378
|
+
<label><input id="load-background" type="checkbox"> Load into background knowledge instead of overwriting the editor</label>
|
|
379
|
+
<span id="background-status" class="hint">No background knowledge loaded.</span>
|
|
380
|
+
<button id="clear-background" type="button">Clear background</button>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="toolbar option-row" aria-label="Run options">
|
|
383
|
+
<label><input id="proof" type="checkbox"> Show proof explanations</label>
|
|
384
|
+
<label><input id="stats" type="checkbox"> Show stats</label>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="panel-body">
|
|
387
|
+
<p class="hint">Enter any Eyelang file URL and click “Load from URL”. Background knowledge is prepended when the program runs.</p>
|
|
388
|
+
</div>
|
|
389
|
+
</details>
|
|
390
|
+
</section>
|
|
306
391
|
|
|
392
|
+
<section aria-labelledby="editor-heading">
|
|
307
393
|
<div class="editor-shell">
|
|
308
394
|
<div class="editor-label">
|
|
309
395
|
<strong id="editor-heading">Editable Eyelang program</strong>
|
|
310
396
|
<span id="source-name" class="hint">custom program</span>
|
|
311
397
|
</div>
|
|
312
398
|
<div class="editor">
|
|
399
|
+
<div id="line-numbers" class="line-numbers" aria-hidden="true"><div class="line-numbers-inner"></div></div>
|
|
400
|
+
<div id="error-line-marker" class="error-line-marker" aria-hidden="true"></div>
|
|
313
401
|
<pre id="highlight" aria-hidden="true"><code></code></pre>
|
|
314
402
|
<textarea id="source" spellcheck="false" autocapitalize="off" autocomplete="off" aria-label="Editable Eyelang source"></textarea>
|
|
315
403
|
</div>
|
|
316
404
|
</div>
|
|
317
|
-
</section>
|
|
318
|
-
|
|
319
|
-
<aside aria-labelledby="output-heading">
|
|
320
405
|
<div class="toolbar button-row">
|
|
321
|
-
<button id="run" type="button" class="primary">Run</button>
|
|
406
|
+
<button id="run" type="button" class="primary">Run reasoning</button>
|
|
322
407
|
<button id="stop" type="button" disabled>Stop</button>
|
|
323
408
|
<button id="copy-source" type="button">Copy source</button>
|
|
324
409
|
<button id="copy-output" type="button">Copy output</button>
|
|
325
410
|
<button id="share" type="button">Copy share link</button>
|
|
411
|
+
<button id="create-gist" type="button">Create Gist share</button>
|
|
326
412
|
</div>
|
|
413
|
+
</section>
|
|
414
|
+
|
|
415
|
+
<section aria-labelledby="output-heading">
|
|
327
416
|
<div class="editor-label">
|
|
328
417
|
<strong id="output-heading">Output</strong>
|
|
329
418
|
<span id="elapsed" class="hint">Idle.</span>
|
|
@@ -336,7 +425,7 @@
|
|
|
336
425
|
<div class="panel-body">
|
|
337
426
|
<p class="hint">For local use, serve the checkout with a small HTTP server first; browser modules and example loading usually do not work from <code>file://</code>.</p>
|
|
338
427
|
</div>
|
|
339
|
-
</
|
|
428
|
+
</section>
|
|
340
429
|
</main>
|
|
341
430
|
|
|
342
431
|
<footer>
|
|
@@ -477,6 +566,9 @@
|
|
|
477
566
|
const FALLBACK_SOURCE = `materialize(answer, 1).
|
|
478
567
|
answer(ok) :- eq(ok, ok).
|
|
479
568
|
`;
|
|
569
|
+
const HIGHLIGHT_LIMIT = 200000;
|
|
570
|
+
const MAX_SHARE_URL_LENGTH = 1900;
|
|
571
|
+
const GIST_STATE_FILENAME = 'eyelang-playground-state.json';
|
|
480
572
|
const KEYWORDS = new Set(['materialize', 'memoize']);
|
|
481
573
|
const BUILTINS = new Set([
|
|
482
574
|
"abs",
|
|
@@ -561,33 +653,52 @@ answer(ok) :- eq(ok, ok).
|
|
|
561
653
|
|
|
562
654
|
const source = document.querySelector('#source');
|
|
563
655
|
const highlight = document.querySelector('#highlight');
|
|
656
|
+
const lineNumbers = document.querySelector('#line-numbers');
|
|
657
|
+
const lineNumbersInner = lineNumbers.firstElementChild;
|
|
658
|
+
const errorLineMarker = document.querySelector('#error-line-marker');
|
|
564
659
|
const exampleSelect = document.querySelector('#examples');
|
|
565
660
|
const exampleSearch = document.querySelector('#example-search');
|
|
566
661
|
const output = document.querySelector('#output');
|
|
567
662
|
const status = document.querySelector('#status');
|
|
568
663
|
const elapsed = document.querySelector('#elapsed');
|
|
569
664
|
const sourceName = document.querySelector('#source-name');
|
|
665
|
+
const backgroundStatus = document.querySelector('#background-status');
|
|
666
|
+
const loadBackground = document.querySelector('#load-background');
|
|
570
667
|
const proof = document.querySelector('#proof');
|
|
571
668
|
const stats = document.querySelector('#stats');
|
|
572
669
|
const runButton = document.querySelector('#run');
|
|
573
670
|
const stopButton = document.querySelector('#stop');
|
|
671
|
+
let backgroundSource = '';
|
|
672
|
+
let backgroundName = '';
|
|
574
673
|
let activeWorker = null;
|
|
575
674
|
let activeWorkerUrl = null;
|
|
675
|
+
let renderToken = 0;
|
|
676
|
+
let syntaxErrorLine = null;
|
|
677
|
+
let sourceReference = { kind: 'custom' };
|
|
678
|
+
let sourceDirty = false;
|
|
679
|
+
let backgroundReference = null;
|
|
576
680
|
|
|
577
681
|
populateExamples(EXAMPLES);
|
|
578
|
-
|
|
682
|
+
await restoreFromHashOrLoadDefault();
|
|
579
683
|
loadVersion();
|
|
580
684
|
|
|
581
|
-
source.addEventListener('input',
|
|
685
|
+
source.addEventListener('input', () => {
|
|
686
|
+
sourceDirty = true;
|
|
687
|
+
sourceReference = { kind: 'custom' };
|
|
688
|
+
clearSyntaxError();
|
|
689
|
+
render();
|
|
690
|
+
});
|
|
582
691
|
source.addEventListener('scroll', syncScroll);
|
|
583
692
|
exampleSearch.addEventListener('input', () => populateExamples(filterExamples(exampleSearch.value)));
|
|
584
693
|
document.querySelector('#load-example').addEventListener('click', () => loadExample(exampleSelect.value));
|
|
585
694
|
document.querySelector('#load-url').addEventListener('click', loadUrl);
|
|
695
|
+
document.querySelector('#clear-background').addEventListener('click', clearBackground);
|
|
586
696
|
document.querySelector('#run').addEventListener('click', runProgram);
|
|
587
697
|
document.querySelector('#stop').addEventListener('click', stopProgram);
|
|
588
698
|
document.querySelector('#copy-source').addEventListener('click', () => copyText(source.value, 'Source copied.'));
|
|
589
699
|
document.querySelector('#copy-output').addEventListener('click', () => copyText(output.textContent, 'Output copied.'));
|
|
590
700
|
document.querySelector('#share').addEventListener('click', copyShareLink);
|
|
701
|
+
document.querySelector('#create-gist').addEventListener('click', createGistShare);
|
|
591
702
|
|
|
592
703
|
function populateExamples(names) {
|
|
593
704
|
const current = exampleSelect.value;
|
|
@@ -614,7 +725,7 @@ answer(ok) :- eq(ok, ok).
|
|
|
614
725
|
const exampleUrl = new URL(`./examples/${name}.pl`, location.href);
|
|
615
726
|
const response = await fetch(exampleUrl, { cache: 'no-store' });
|
|
616
727
|
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
617
|
-
setSource(await response.text(), `examples/${name}.pl
|
|
728
|
+
setSource(await response.text(), `examples/${name}.pl`, { kind: 'example', name });
|
|
618
729
|
setStatus(`Loaded examples/${name}.pl.`);
|
|
619
730
|
} catch (error) {
|
|
620
731
|
setSource(FALLBACK_SOURCE, 'fallback program');
|
|
@@ -629,27 +740,75 @@ answer(ok) :- eq(ok, ok).
|
|
|
629
740
|
setStatus(`Loading ${url}…`);
|
|
630
741
|
const response = await fetch(url);
|
|
631
742
|
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
632
|
-
|
|
633
|
-
|
|
743
|
+
const text = await response.text();
|
|
744
|
+
if (loadBackground.checked) {
|
|
745
|
+
backgroundSource = text;
|
|
746
|
+
backgroundName = url;
|
|
747
|
+
backgroundReference = { kind: 'url', url };
|
|
748
|
+
updateBackgroundStatus();
|
|
749
|
+
setStatus(`Loaded background knowledge from ${url}.`);
|
|
750
|
+
} else {
|
|
751
|
+
setSource(text, url, { kind: 'url', url });
|
|
752
|
+
setStatus(`Loaded ${url}.`);
|
|
753
|
+
}
|
|
634
754
|
} catch (error) {
|
|
635
755
|
setStatus(`Could not load URL: ${formatError(error)}`, true);
|
|
636
756
|
}
|
|
637
757
|
}
|
|
638
758
|
|
|
639
|
-
function
|
|
759
|
+
function clearBackground() {
|
|
760
|
+
backgroundSource = '';
|
|
761
|
+
backgroundName = '';
|
|
762
|
+
backgroundReference = null;
|
|
763
|
+
updateBackgroundStatus();
|
|
764
|
+
setStatus('Background knowledge cleared.');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function updateBackgroundStatus() {
|
|
768
|
+
backgroundStatus.textContent = backgroundSource
|
|
769
|
+
? `Background loaded from ${backgroundName} (${backgroundSource.length.toLocaleString()} chars).`
|
|
770
|
+
: 'No background knowledge loaded.';
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function setSource(text, name, reference = { kind: 'custom' }) {
|
|
640
774
|
source.value = text;
|
|
641
775
|
sourceName.textContent = name;
|
|
776
|
+
sourceReference = reference;
|
|
777
|
+
sourceDirty = false;
|
|
778
|
+
clearSyntaxError();
|
|
642
779
|
render();
|
|
643
780
|
}
|
|
644
781
|
|
|
645
782
|
function render() {
|
|
646
|
-
|
|
647
|
-
|
|
783
|
+
const token = ++renderToken;
|
|
784
|
+
const text = source.value;
|
|
785
|
+
requestAnimationFrame(() => {
|
|
786
|
+
if (token !== renderToken) return;
|
|
787
|
+
highlight.firstElementChild.innerHTML = renderHighlightedSource(text);
|
|
788
|
+
updateLineNumbers(text);
|
|
789
|
+
syncScroll();
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function renderHighlightedSource(text) {
|
|
794
|
+
const rendered = text.length > HIGHLIGHT_LIMIT ? escapeHtml(text) : colorize(text);
|
|
795
|
+
return rendered + (text.endsWith('\n') ? ' ' : '');
|
|
648
796
|
}
|
|
649
797
|
|
|
650
798
|
function syncScroll() {
|
|
651
799
|
highlight.scrollTop = source.scrollTop;
|
|
652
800
|
highlight.scrollLeft = source.scrollLeft;
|
|
801
|
+
lineNumbersInner.style.transform = `translateY(${-source.scrollTop}px)`;
|
|
802
|
+
updateErrorLineMarker();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function updateLineNumbers(text = source.value) {
|
|
806
|
+
const count = Math.max(1, text.split('\n').length);
|
|
807
|
+
lineNumbersInner.innerHTML = Array.from({ length: count }, (_, index) => {
|
|
808
|
+
const line = index + 1;
|
|
809
|
+
const className = line === syntaxErrorLine ? 'line-number error' : 'line-number';
|
|
810
|
+
return `<span class="${className}">${line}</span>`;
|
|
811
|
+
}).join('');
|
|
653
812
|
}
|
|
654
813
|
|
|
655
814
|
function colorize(text) {
|
|
@@ -708,6 +867,7 @@ answer(ok) :- eq(ok, ok).
|
|
|
708
867
|
|
|
709
868
|
function runProgram() {
|
|
710
869
|
stopProgram(false);
|
|
870
|
+
clearSyntaxError();
|
|
711
871
|
output.textContent = '';
|
|
712
872
|
elapsed.textContent = 'Running…';
|
|
713
873
|
setStatus('Running Eyelang…');
|
|
@@ -737,20 +897,83 @@ answer(ok) :- eq(ok, ok).
|
|
|
737
897
|
activeWorker.onmessage = (event) => finishRun(event.data);
|
|
738
898
|
activeWorker.onerror = (event) => finishRun({ ok: false, error: event.message || 'worker error' });
|
|
739
899
|
activeWorker.postMessage({
|
|
740
|
-
source:
|
|
900
|
+
source: combinedSource(),
|
|
741
901
|
options: { proof: proof.checked, stats: stats.checked },
|
|
742
902
|
});
|
|
743
903
|
}
|
|
744
904
|
|
|
905
|
+
function combinedSource() {
|
|
906
|
+
if (!backgroundSource.trim()) return source.value;
|
|
907
|
+
return `${backgroundSource}\n${source.value}`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function extractParseErrorLine(text) {
|
|
911
|
+
const match = String(text).match(/parse line (\d+)/i);
|
|
912
|
+
return match ? Number(match[1]) : null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function editorLineForParseLine(parseLine) {
|
|
916
|
+
if (!Number.isFinite(parseLine) || parseLine < 1) return null;
|
|
917
|
+
if (!backgroundSource.trim()) return parseLine;
|
|
918
|
+
const backgroundLines = backgroundSource.split('\n').length;
|
|
919
|
+
const editorLine = parseLine - backgroundLines;
|
|
920
|
+
return editorLine >= 1 ? editorLine : null;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function markSyntaxErrorLine(line) {
|
|
924
|
+
syntaxErrorLine = line;
|
|
925
|
+
updateLineNumbers();
|
|
926
|
+
revealLine(line);
|
|
927
|
+
updateErrorLineMarker();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function clearSyntaxError() {
|
|
931
|
+
syntaxErrorLine = null;
|
|
932
|
+
updateLineNumbers();
|
|
933
|
+
updateErrorLineMarker();
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function revealLine(line) {
|
|
937
|
+
const metrics = editorMetrics();
|
|
938
|
+
const y = metrics.paddingTop + (line - 1) * metrics.lineHeight;
|
|
939
|
+
const top = source.scrollTop;
|
|
940
|
+
const bottom = top + source.clientHeight - metrics.lineHeight;
|
|
941
|
+
if (y < top || y > bottom) {
|
|
942
|
+
source.scrollTop = Math.max(0, y - source.clientHeight / 3);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function updateErrorLineMarker() {
|
|
947
|
+
if (syntaxErrorLine == null) {
|
|
948
|
+
errorLineMarker.style.display = 'none';
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const metrics = editorMetrics();
|
|
952
|
+
errorLineMarker.style.display = 'block';
|
|
953
|
+
errorLineMarker.style.height = `${metrics.lineHeight}px`;
|
|
954
|
+
errorLineMarker.style.top = `${metrics.paddingTop + (syntaxErrorLine - 1) * metrics.lineHeight - source.scrollTop}px`;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function editorMetrics() {
|
|
958
|
+
const style = getComputedStyle(source);
|
|
959
|
+
const fontSize = parseFloat(style.fontSize) || 15;
|
|
960
|
+
const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.55;
|
|
961
|
+
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
962
|
+
return { lineHeight, paddingTop };
|
|
963
|
+
}
|
|
964
|
+
|
|
745
965
|
function finishRun(message) {
|
|
746
|
-
const worker = activeWorker;
|
|
747
966
|
cleanupWorker();
|
|
748
967
|
runButton.disabled = false;
|
|
749
968
|
stopButton.disabled = true;
|
|
750
969
|
if (!message.ok) {
|
|
751
|
-
|
|
970
|
+
const errorText = message.error || 'Unknown error.';
|
|
971
|
+
const parseLine = extractParseErrorLine(errorText);
|
|
972
|
+
const editorLine = editorLineForParseLine(parseLine);
|
|
973
|
+
if (editorLine != null) markSyntaxErrorLine(editorLine);
|
|
974
|
+
output.textContent = errorText;
|
|
752
975
|
elapsed.textContent = 'Failed.';
|
|
753
|
-
setStatus('Run failed.'
|
|
976
|
+
setStatus(editorLine == null ? 'Run failed.' : `Syntax error on line ${editorLine}.`, true);
|
|
754
977
|
return;
|
|
755
978
|
}
|
|
756
979
|
let text = message.stdout || '(no materialized output)\n';
|
|
@@ -758,7 +981,7 @@ answer(ok) :- eq(ok, ok).
|
|
|
758
981
|
output.textContent = text;
|
|
759
982
|
const seconds = (message.elapsedMs / 1000).toFixed(3);
|
|
760
983
|
elapsed.textContent = `${seconds} sec`;
|
|
761
|
-
setStatus(
|
|
984
|
+
setStatus('Run complete.');
|
|
762
985
|
}
|
|
763
986
|
|
|
764
987
|
function stopProgram(report = true) {
|
|
@@ -791,30 +1014,176 @@ answer(ok) :- eq(ok, ok).
|
|
|
791
1014
|
}
|
|
792
1015
|
|
|
793
1016
|
async function copyShareLink() {
|
|
794
|
-
const
|
|
795
|
-
|
|
1017
|
+
const link = buildShareLink();
|
|
1018
|
+
if (link == null) {
|
|
1019
|
+
setStatus('This program is too large for a reliable URL. Use “Create Gist share” instead.', true);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
await copyText(link, 'Share link copied.');
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function buildShareLink() {
|
|
1026
|
+
const referenced = buildReferenceShareLink();
|
|
1027
|
+
if (referenced && referenced.length <= MAX_SHARE_URL_LENGTH) return referenced;
|
|
1028
|
+
const embedded = `${basePlaygroundUrl()}#state=${encodeState(currentShareState())}`;
|
|
1029
|
+
return embedded.length <= MAX_SHARE_URL_LENGTH ? embedded : null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function buildReferenceShareLink() {
|
|
1033
|
+
const params = new URLSearchParams();
|
|
1034
|
+
if (proof.checked) params.set('proof', '1');
|
|
1035
|
+
if (stats.checked) params.set('stats', '1');
|
|
1036
|
+
if (backgroundSource.trim()) {
|
|
1037
|
+
if (backgroundReference?.kind !== 'url') return null;
|
|
1038
|
+
params.set('background-url', backgroundReference.url);
|
|
1039
|
+
}
|
|
1040
|
+
if (!sourceDirty && sourceReference.kind === 'example') params.set('example', sourceReference.name);
|
|
1041
|
+
else if (!sourceDirty && sourceReference.kind === 'url') params.set('url', sourceReference.url);
|
|
1042
|
+
else return null;
|
|
1043
|
+
return `${basePlaygroundUrl()}#${params.toString()}`;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function createGistShare() {
|
|
1047
|
+
const stateText = JSON.stringify(currentShareState(), null, 2);
|
|
1048
|
+
const token = prompt('Optional GitHub token with gist scope. It is only sent to api.github.com and is not stored. Leave blank to copy Gist-ready state instead.');
|
|
1049
|
+
if (token === null) return;
|
|
1050
|
+
if (!token.trim()) {
|
|
1051
|
+
await copyText(stateText, `Gist-ready state copied. Create a Gist file named ${GIST_STATE_FILENAME}, paste it, then share its raw URL with #state-url=...`);
|
|
1052
|
+
window.open('https://gist.github.com/', '_blank', 'noopener');
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
try {
|
|
1057
|
+
setStatus('Creating GitHub Gist…');
|
|
1058
|
+
const files = {
|
|
1059
|
+
[GIST_STATE_FILENAME]: { content: stateText },
|
|
1060
|
+
'program.pl': { content: source.value },
|
|
1061
|
+
};
|
|
1062
|
+
if (backgroundSource.trim()) files['background.pl'] = { content: backgroundSource };
|
|
1063
|
+
const response = await fetch('https://api.github.com/gists', {
|
|
1064
|
+
method: 'POST',
|
|
1065
|
+
headers: {
|
|
1066
|
+
Accept: 'application/vnd.github+json',
|
|
1067
|
+
Authorization: `Bearer ${token.trim()}`,
|
|
1068
|
+
'Content-Type': 'application/json',
|
|
1069
|
+
},
|
|
1070
|
+
body: JSON.stringify({
|
|
1071
|
+
description: 'Eyelang playground share',
|
|
1072
|
+
public: false,
|
|
1073
|
+
files,
|
|
1074
|
+
}),
|
|
1075
|
+
});
|
|
1076
|
+
const gist = await response.json().catch(() => ({}));
|
|
1077
|
+
if (!response.ok) throw new Error(gist.message || `${response.status} ${response.statusText}`);
|
|
1078
|
+
const rawUrl = gist.files?.[GIST_STATE_FILENAME]?.raw_url;
|
|
1079
|
+
if (!rawUrl) throw new Error('Gist response did not include a raw state URL.');
|
|
1080
|
+
await copyText(`${basePlaygroundUrl()}#state-url=${encodeURIComponent(rawUrl)}`, 'Gist share link copied.');
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
await copyText(stateText, `Could not create Gist: ${formatError(error)}. Gist-ready state copied instead.`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function currentShareState() {
|
|
1087
|
+
return {
|
|
1088
|
+
source: source.value,
|
|
1089
|
+
proof: proof.checked,
|
|
1090
|
+
stats: stats.checked,
|
|
1091
|
+
backgroundSource,
|
|
1092
|
+
backgroundName,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function encodeState(payload) {
|
|
1097
|
+
const data = new TextEncoder().encode(JSON.stringify(payload));
|
|
796
1098
|
let binary = '';
|
|
797
1099
|
for (const byte of data) binary += String.fromCharCode(byte);
|
|
798
|
-
|
|
799
|
-
|
|
1100
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function decodeState(encoded) {
|
|
1104
|
+
const base64 = encoded.replaceAll('-', '+').replaceAll('_', '/');
|
|
1105
|
+
const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
|
|
1106
|
+
const bytes = Uint8Array.from(atob(padded), (ch) => ch.charCodeAt(0));
|
|
1107
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function basePlaygroundUrl() {
|
|
1111
|
+
return location.href.split('#')[0];
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function restoreFromHashOrLoadDefault() {
|
|
1115
|
+
if (!(await restoreFromHash())) await loadExample('ancestor');
|
|
800
1116
|
}
|
|
801
1117
|
|
|
802
|
-
function restoreFromHash() {
|
|
803
|
-
if (!location.hash.
|
|
1118
|
+
async function restoreFromHash() {
|
|
1119
|
+
if (!location.hash || location.hash === '#') return false;
|
|
1120
|
+
const params = new URLSearchParams(location.hash.slice(1));
|
|
804
1121
|
try {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1122
|
+
if (params.has('state')) {
|
|
1123
|
+
applySharedState(decodeState(params.get('state')), 'shared program');
|
|
1124
|
+
setStatus('Loaded shared program.');
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
if (params.has('state-url')) {
|
|
1128
|
+
const stateUrl = params.get('state-url');
|
|
1129
|
+
const response = await fetch(stateUrl);
|
|
1130
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
1131
|
+
applySharedState(await response.json(), `shared state from ${stateUrl}`);
|
|
1132
|
+
setStatus('Loaded shared Gist state.');
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
await restoreBackgroundUrl(params);
|
|
1136
|
+
applyOptionParams(params);
|
|
1137
|
+
if (params.has('example')) {
|
|
1138
|
+
await loadExample(params.get('example'));
|
|
1139
|
+
applyOptionParams(params);
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
if (params.has('url')) {
|
|
1143
|
+
await loadSourceFromUrl(params.get('url'), false);
|
|
1144
|
+
applyOptionParams(params);
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
814
1147
|
} catch (error) {
|
|
815
1148
|
setStatus(`Could not read shared state: ${formatError(error)}`, true);
|
|
816
1149
|
return false;
|
|
817
1150
|
}
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function restoreBackgroundUrl(params) {
|
|
1155
|
+
const url = params.get('background-url');
|
|
1156
|
+
if (!url) return;
|
|
1157
|
+
await loadSourceFromUrl(url, true);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async function loadSourceFromUrl(url, asBackground) {
|
|
1161
|
+
const response = await fetch(url);
|
|
1162
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
1163
|
+
const text = await response.text();
|
|
1164
|
+
if (asBackground) {
|
|
1165
|
+
backgroundSource = text;
|
|
1166
|
+
backgroundName = url;
|
|
1167
|
+
backgroundReference = { kind: 'url', url };
|
|
1168
|
+
updateBackgroundStatus();
|
|
1169
|
+
} else {
|
|
1170
|
+
setSource(text, url, { kind: 'url', url });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function applySharedState(payload, label) {
|
|
1175
|
+
setSource(String(payload.source || ''), label, { kind: 'custom' });
|
|
1176
|
+
proof.checked = Boolean(payload.proof);
|
|
1177
|
+
stats.checked = Boolean(payload.stats);
|
|
1178
|
+
backgroundSource = String(payload.backgroundSource || '');
|
|
1179
|
+
backgroundName = String(payload.backgroundName || 'shared background');
|
|
1180
|
+
backgroundReference = null;
|
|
1181
|
+
updateBackgroundStatus();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function applyOptionParams(params) {
|
|
1185
|
+
proof.checked = params.get('proof') === '1' || params.get('proof') === 'true';
|
|
1186
|
+
stats.checked = params.get('stats') === '1' || params.get('stats') === 'true';
|
|
818
1187
|
}
|
|
819
1188
|
|
|
820
1189
|
async function loadVersion() {
|
package/src/solver.js
CHANGED
|
@@ -38,50 +38,122 @@ export class Solver {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
*solve(goals, env = new Env(), depth = 0) {
|
|
41
|
-
this.stats.solve_goals_calls++;
|
|
42
|
-
this.stats.max_depth = Math.max(this.stats.max_depth, depth);
|
|
43
|
-
this.stats.max_goal_count = Math.max(this.stats.max_goal_count, goals.length);
|
|
44
|
-
if (depth > this.maxDepth || this.solutionsSeen >= this.solutionLimit) return;
|
|
45
41
|
if (!Array.isArray(goals)) goals = [goals];
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
const savedActive = this.active;
|
|
44
|
+
try {
|
|
45
|
+
const stack = [{ kind: 'goals', goals, env, depth, active: savedActive.slice() }];
|
|
46
|
+
while (stack.length) {
|
|
47
|
+
const frame = stack.pop();
|
|
48
|
+
if (frame.kind === 'completeMemo') {
|
|
49
|
+
frame.entry.computing = false;
|
|
50
|
+
frame.entry.complete = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const goal = goals[selectedIndex];
|
|
59
|
-
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
60
|
-
if (goal.type === COMPOUND && goal.name === ',' && goal.arity === 2) {
|
|
61
|
-
yield* this.solve([...flattenConjunction(goal), ...rest], env, depth + 1);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
54
|
+
goals = frame.goals;
|
|
55
|
+
env = frame.env;
|
|
56
|
+
depth = frame.depth;
|
|
57
|
+
let active = frame.active;
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
59
|
+
while (true) {
|
|
60
|
+
this.stats.solve_goals_calls++;
|
|
61
|
+
this.stats.max_depth = Math.max(this.stats.max_depth, depth);
|
|
62
|
+
this.stats.max_goal_count = Math.max(this.stats.max_goal_count, goals.length);
|
|
63
|
+
if (depth > this.maxDepth || this.solutionsSeen >= this.solutionLimit) break;
|
|
64
|
+
|
|
65
|
+
if (goals.length === 0) {
|
|
66
|
+
this.solutionsSeen++;
|
|
67
|
+
this.stats.completed_goal_lists++;
|
|
68
|
+
this.active = active;
|
|
69
|
+
yield env;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const first = goals[0];
|
|
74
|
+
if (first?.kind === 'releaseActive') {
|
|
75
|
+
active = active.slice(0, -1);
|
|
76
|
+
goals = goals.slice(1);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (first?.kind === 'memoStore') {
|
|
80
|
+
rememberMemoAnswer(first.entry, first.goal, env);
|
|
81
|
+
goals = goals.slice(1);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// eyelang normally solves left-to-right, but ready deterministic builtins can
|
|
86
|
+
// be run early as pure filters. Stop at internal sentinels so rule-body
|
|
87
|
+
// active guards are released before the caller's remaining goals are seen.
|
|
88
|
+
const selectedIndex = selectReadyDeterministicBuiltin(goals, env, this.registry);
|
|
89
|
+
const goal = goals[selectedIndex];
|
|
90
|
+
const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
|
|
91
|
+
if (goal.type === COMPOUND && goal.name === ',' && goal.arity === 2) {
|
|
92
|
+
goals = [...flattenConjunction(goal), ...rest];
|
|
93
|
+
depth++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const def = goal.type === COMPOUND ? this.registry.get(goal.name, goal.arity) : null;
|
|
98
|
+
this.active = active;
|
|
99
|
+
if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
|
|
100
|
+
const nextEnvs = [];
|
|
101
|
+
for (const next of def.handler({ solver: this, goal, env })) nextEnvs.push(next);
|
|
102
|
+
if (def.deterministic) {
|
|
103
|
+
if (nextEnvs.length) this.stats.deterministic_builtin_successes++;
|
|
104
|
+
else this.stats.deterministic_builtin_failures++;
|
|
105
|
+
}
|
|
106
|
+
if (nextEnvs.length === 0) break;
|
|
107
|
+
if (nextEnvs.length === 1) {
|
|
108
|
+
goals = rest;
|
|
109
|
+
env = nextEnvs[0];
|
|
110
|
+
depth++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (let i = nextEnvs.length - 1; i >= 0; i--) {
|
|
114
|
+
stack.push({ kind: 'goals', goals: rest, env: nextEnvs[i], depth: depth + 1, active });
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.stats.solve_one_goal_calls++;
|
|
120
|
+
if (goal.type !== COMPOUND) break;
|
|
121
|
+
const group = this.program.findGroup(goal.name, goal.arity);
|
|
122
|
+
if (!group) break;
|
|
123
|
+
|
|
124
|
+
if (group.memoized) {
|
|
125
|
+
const key = memoKey(goal, env);
|
|
126
|
+
if (key.hasBound) {
|
|
127
|
+
const mapKey = `${goal.name}/${goal.arity}:${key.text}`;
|
|
128
|
+
let entry = this.memo.get(mapKey);
|
|
129
|
+
if (!entry) {
|
|
130
|
+
entry = { computing: false, complete: false, answers: [], answerKeys: new Set() };
|
|
131
|
+
this.memo.set(mapKey, entry);
|
|
132
|
+
}
|
|
133
|
+
if (entry.complete) {
|
|
134
|
+
pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, this);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if (!entry.computing) {
|
|
138
|
+
entry.computing = true;
|
|
139
|
+
stack.push({ kind: 'completeMemo', entry });
|
|
140
|
+
pushUserGoalUncachedFrames(stack, this, group, goal, [{ kind: 'memoStore', entry, goal }, ...rest], env, depth, active);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pushUserGoalUncachedFrames(stack, this, group, goal, rest, env, depth, active);
|
|
147
|
+
break;
|
|
72
148
|
}
|
|
73
|
-
if (def.deterministic) {
|
|
74
|
-
if (produced) this.stats.deterministic_builtin_successes++;
|
|
75
|
-
else this.stats.deterministic_builtin_failures++;
|
|
76
149
|
}
|
|
77
|
-
|
|
150
|
+
} finally {
|
|
151
|
+
this.active = savedActive;
|
|
78
152
|
}
|
|
79
|
-
|
|
80
|
-
yield* this.solveUserGoal(goal, rest, env, depth);
|
|
81
153
|
}
|
|
82
154
|
|
|
83
155
|
activeVariant(goal, env) {
|
|
84
|
-
return
|
|
156
|
+
return activeVariantIn(goal, env, this.active);
|
|
85
157
|
}
|
|
86
158
|
|
|
87
159
|
*solveUserGoal(goal, rest, env, depth) {
|
|
@@ -180,6 +252,61 @@ export class Solver {
|
|
|
180
252
|
}
|
|
181
253
|
|
|
182
254
|
|
|
255
|
+
function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solver) {
|
|
256
|
+
for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) {
|
|
257
|
+
const answerArgs = entry.answers[answerIndex];
|
|
258
|
+
const next = env.clone();
|
|
259
|
+
let ok = true;
|
|
260
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
261
|
+
solver.stats.unify_calls++;
|
|
262
|
+
if (!unify(goal.args[i], answerArgs[i], next)) { ok = false; break; }
|
|
263
|
+
}
|
|
264
|
+
if (ok) stack.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
269
|
+
if (activeVariantIn(goal, env, active)) return;
|
|
270
|
+
const candidates = selectClauseCandidates(group, goal, env);
|
|
271
|
+
const frames = [];
|
|
272
|
+
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
273
|
+
for (const clause of pass) {
|
|
274
|
+
if (headCannotMatch(goal, clause.head, env)) continue;
|
|
275
|
+
const id = nextFreshId();
|
|
276
|
+
const freshHead = freshTerm(clause.head, id);
|
|
277
|
+
const freshBody = clause.body.map((term) => freshTerm(term, id));
|
|
278
|
+
const next = env.clone();
|
|
279
|
+
solver.stats.unify_calls++;
|
|
280
|
+
if (!unify(goal, freshHead, next)) continue;
|
|
281
|
+
if (freshBody.length === 0) {
|
|
282
|
+
frames.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
|
|
283
|
+
} else {
|
|
284
|
+
frames.push({
|
|
285
|
+
kind: 'goals',
|
|
286
|
+
goals: [...freshBody, { kind: 'releaseActive' }, ...rest],
|
|
287
|
+
env: next,
|
|
288
|
+
depth: depth + 1,
|
|
289
|
+
active: [...active, { goal, env }],
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function rememberMemoAnswer(entry, goal, env) {
|
|
298
|
+
const answerArgs = goal.args.map((arg) => importResolved(arg, env));
|
|
299
|
+
const key = answerArgs.map((arg) => termToString(arg, new Env(), true)).join('\x1f');
|
|
300
|
+
if (entry.answerKeys.has(key)) return;
|
|
301
|
+
entry.answerKeys.add(key);
|
|
302
|
+
entry.answers.push(answerArgs);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function activeVariantIn(goal, env, active) {
|
|
306
|
+
return active.some((entry) => variantTerms(goal, env, entry.goal, entry.env));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
183
310
|
function builtinIsReadyOrAuthoritative(def, solver, goal, env) {
|
|
184
311
|
if (typeof def.shouldUse === 'function' && !def.shouldUse({ solver, goal, env })) return false;
|
|
185
312
|
if (typeof def.ready !== 'function') return true;
|
|
@@ -190,6 +317,7 @@ function builtinIsReadyOrAuthoritative(def, solver, goal, env) {
|
|
|
190
317
|
function selectReadyDeterministicBuiltin(goals, env, registry) {
|
|
191
318
|
for (let i = 0; i < goals.length; i++) {
|
|
192
319
|
const goal = goals[i];
|
|
320
|
+
if (goal?.kind === 'releaseActive' || goal?.kind === 'memoStore') return 0;
|
|
193
321
|
if (goal.type !== COMPOUND) continue;
|
|
194
322
|
const def = registry.get(goal.name, goal.arity);
|
|
195
323
|
if (!def?.deterministic || typeof def.ready !== 'function') continue;
|
package/test/run-regression.mjs
CHANGED
|
@@ -360,6 +360,16 @@ function apiCases() {
|
|
|
360
360
|
assertEqual(result.stdout, 'q(a, b).\n', 'stdout');
|
|
361
361
|
},
|
|
362
362
|
},
|
|
363
|
+
{
|
|
364
|
+
name: 'run keeps recursive materializations independent in one solver',
|
|
365
|
+
run: () => {
|
|
366
|
+
const text = fs.readFileSync(path.join(packageRoot, 'examples', 'alignment-demo.pl'), 'utf8');
|
|
367
|
+
const program = Program.parseSources([{ text, filename: 'alignment-demo.pl' }]);
|
|
368
|
+
const result = run(program);
|
|
369
|
+
assertIncludes(result.stdout, 'broaderTransitive(anpr_passenger_car, ref_car).\n', 'stdout');
|
|
370
|
+
assertIncludes(result.stdout, 'narrowerOrEqualOf(anpr_passenger_car, ref_car).\n', 'stdout');
|
|
371
|
+
},
|
|
372
|
+
},
|
|
363
373
|
{
|
|
364
374
|
name: 'makeProgram creates indexed programs',
|
|
365
375
|
run: () => {
|
|
@@ -525,6 +535,20 @@ function whiteBoxCases() {
|
|
|
525
535
|
assertEqual(group.recursive, true, 'collatz/2 recursive');
|
|
526
536
|
},
|
|
527
537
|
},
|
|
538
|
+
{
|
|
539
|
+
name: 'collatz example remains stack-safe for browser-sized stacks',
|
|
540
|
+
run: () => {
|
|
541
|
+
// Use a deliberately tiny stack to catch browser-worker recursion regressions.
|
|
542
|
+
const result = spawnSync(process.execPath, ['--stack-size=100', bin, 'examples/collatz-1000.pl'], {
|
|
543
|
+
cwd: packageRoot,
|
|
544
|
+
encoding: 'utf8',
|
|
545
|
+
});
|
|
546
|
+
assertEqual(result.status, 0, `exit status${result.stderr ? `\nstderr: ${result.stderr}` : ''}`);
|
|
547
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
548
|
+
assertIncludes(result.stdout, 'collatzTrajectory(1000, [1000, 500, 250, 125', 'stdout');
|
|
549
|
+
assertIncludes(result.stdout, 'collatzTrajectory(1, [1]).\n', 'stdout');
|
|
550
|
+
},
|
|
551
|
+
},
|
|
528
552
|
];
|
|
529
553
|
}
|
|
530
554
|
|
|
@@ -625,17 +649,44 @@ function playgroundStaticIssues() {
|
|
|
625
649
|
if (!pkg.files?.includes('playground.html')) issues.push('package files must include playground.html');
|
|
626
650
|
if (!readme.includes('[Playground](https://eyereasoner.github.io/eyelang/playground)')) issues.push('README must link to the GitHub Pages playground URL');
|
|
627
651
|
if (!html.includes('<meta name="viewport" content="width=device-width, initial-scale=1">')) issues.push('missing mobile viewport meta');
|
|
628
|
-
if (!html.includes('
|
|
629
|
-
issues.push('playground must
|
|
652
|
+
if (!html.includes('main {') || !html.includes('display: block;')) {
|
|
653
|
+
issues.push('playground must use a simple vertical layout');
|
|
630
654
|
}
|
|
631
655
|
if (!html.includes('@media (max-width: 560px)') || !html.includes('button,') || !html.includes('width: 100%')) {
|
|
632
656
|
issues.push('playground must make controls usable at phone widths');
|
|
633
657
|
}
|
|
658
|
+
if (!html.includes('<summary id="advanced-heading">⚙ Advanced configuration</summary>')) {
|
|
659
|
+
issues.push('playground must keep URL/proof controls inside advanced configuration');
|
|
660
|
+
}
|
|
661
|
+
if (!html.includes('id="load-background"') || !html.includes('backgroundSource') || !html.includes('combinedSource()')) {
|
|
662
|
+
issues.push('playground must support loading URL content as background knowledge');
|
|
663
|
+
}
|
|
664
|
+
if (!html.includes('HIGHLIGHT_LIMIT') || !html.includes('text.length > HIGHLIGHT_LIMIT')) {
|
|
665
|
+
issues.push('playground must avoid full syntax coloring for very large examples');
|
|
666
|
+
}
|
|
634
667
|
if (!html.includes('<script type="module">')) issues.push('playground script must be an ES module');
|
|
635
668
|
if (!html.includes("new URL('./src/index.js', location.href)")) issues.push('playground must import the public browser API');
|
|
636
669
|
if (!html.includes('class="editor"') || !html.includes('id="highlight"') || !html.includes('id="source"')) {
|
|
637
670
|
issues.push('playground must include layered syntax-colored editor');
|
|
638
671
|
}
|
|
672
|
+
if (!html.includes('--editor-bg: #ffffff') || !html.includes('background: var(--editor-bg)')) {
|
|
673
|
+
issues.push('playground editor must use a light editor background');
|
|
674
|
+
}
|
|
675
|
+
if (!html.includes('id="error-line-marker"') || !html.includes('extractParseErrorLine') || !html.includes('markSyntaxErrorLine') || !html.includes('--editor-error-line')) {
|
|
676
|
+
issues.push('playground must highlight syntax-error lines in the editor');
|
|
677
|
+
}
|
|
678
|
+
if (!html.includes('id="line-numbers"') || !html.includes('updateLineNumbers') || !html.includes('lineNumbersInner.style.transform') || !html.includes('--line-number-bg')) {
|
|
679
|
+
issues.push('playground editor must include synced line numbers');
|
|
680
|
+
}
|
|
681
|
+
if (!html.includes('MAX_SHARE_URL_LENGTH') || !html.includes('buildReferenceShareLink') || !html.includes("params.set('example'") || !html.includes("params.set('url'")) {
|
|
682
|
+
issues.push('playground share links must avoid embedding large example or URL-loaded sources');
|
|
683
|
+
}
|
|
684
|
+
if (!html.includes('id="create-gist"') || !html.includes('createGistShare') || !html.includes('GIST_STATE_FILENAME') || !html.includes("fetch('https://api.github.com/gists'")) {
|
|
685
|
+
issues.push('playground must support Gist-backed sharing for large programs');
|
|
686
|
+
}
|
|
687
|
+
if (!html.includes("params.has('state-url')") || !html.includes('#state-url=')) {
|
|
688
|
+
issues.push('playground must restore state from raw Gist state URLs');
|
|
689
|
+
}
|
|
639
690
|
if (!html.includes('id="example-search"') || !html.includes('id="examples"')) issues.push('playground must include searchable examples');
|
|
640
691
|
const scriptMatch = html.match(new RegExp('<script type="module">\\n([\\s\\S]*?)\\n <\\/script>'));
|
|
641
692
|
if (scriptMatch == null) {
|