eyelang 1.1.13 → 1.1.15

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/docs/guide.md CHANGED
@@ -302,10 +302,10 @@ Use `holds/2` when you want to match the member term directly, for example `name
302
302
 
303
303
  ## Example catalog
304
304
 
305
- The repository includes examples for recursion, graph reachability, finite search, arithmetic, list processing, optimization, policies, puzzles, and applied scientific calculations. Bundled examples use relation-style output.
305
+ Each example has a checked golden output in `examples/output`.
306
306
 
307
- | Input | Short description | Output |
308
- | --- | --- | --- |
307
+ | Example | What it demonstrates | Golden output |
308
+ |---|---|---|
309
309
  | [`access-control-policy.pl`](../examples/access-control-policy.pl) | Evaluates role and condition based access decisions. | [`output/access-control-policy.pl`](../examples/output/access-control-policy.pl) |
310
310
  | [`ackermann.pl`](../examples/ackermann.pl) | Computes Ackermann-style hyperoperation values. | [`output/ackermann.pl`](../examples/output/ackermann.pl) |
311
311
  | [`age.pl`](../examples/age.pl) | Checks whether people meet age thresholds. | [`output/age.pl`](../examples/output/age.pl) |
@@ -391,6 +391,8 @@ The repository includes examples for recursion, graph reachability, finite searc
391
391
  | [`heron-theorem.pl`](../examples/heron-theorem.pl) | Computes triangle area by Heron's theorem. | [`output/heron-theorem.pl`](../examples/output/heron-theorem.pl) |
392
392
  | [`ideal-gas-law.pl`](../examples/ideal-gas-law.pl) | Applies the ideal gas law. | [`output/ideal-gas-law.pl`](../examples/output/ideal-gas-law.pl) |
393
393
  | [`illegitimate-reasoning.pl`](../examples/illegitimate-reasoning.pl) | Detects suspect reasoning patterns. | [`output/illegitimate-reasoning.pl`](../examples/output/illegitimate-reasoning.pl) |
394
+ | [`job-shop-scheduling.pl`](../examples/job-shop-scheduling.pl) | Searches a small job-shop schedule and minimizes makespan. | [`output/job-shop-scheduling.pl`](../examples/output/job-shop-scheduling.pl) |
395
+ | [`knapsack-optimization.pl`](../examples/knapsack-optimization.pl) | Optimizes a finite 0/1 knapsack pack with aggregation. | [`output/knapsack-optimization.pl`](../examples/output/knapsack-optimization.pl) |
394
396
  | [`knowledge-engineering-alignment-flow.pl`](../examples/knowledge-engineering-alignment-flow.pl) | Specializes reusable alignment rules into a target-shaped flow view. | [`output/knowledge-engineering-alignment-flow.pl`](../examples/output/knowledge-engineering-alignment-flow.pl) |
395
397
  | [`law-of-cosines.pl`](../examples/law-of-cosines.pl) | Computes a triangle side by cosine law. | [`output/law-of-cosines.pl`](../examples/output/law-of-cosines.pl) |
396
398
  | [`least-squares-regression.pl`](../examples/least-squares-regression.pl) | Fits a least-squares regression line. | [`output/least-squares-regression.pl`](../examples/output/least-squares-regression.pl) |
@@ -399,6 +401,7 @@ The repository includes examples for recursion, graph reachability, finite searc
399
401
  | [`manufacturing-quality-control.pl`](../examples/manufacturing-quality-control.pl) | Evaluates process capability and quality. | [`output/manufacturing-quality-control.pl`](../examples/output/manufacturing-quality-control.pl) |
400
402
  | [`microgrid-dispatch.pl`](../examples/microgrid-dispatch.pl) | Plans microgrid dispatch and reserve. | [`output/microgrid-dispatch.pl`](../examples/output/microgrid-dispatch.pl) |
401
403
  | [`monkey-bananas.pl`](../examples/monkey-bananas.pl) | Solves the monkey-and-bananas puzzle. | [`output/monkey-bananas.pl`](../examples/output/monkey-bananas.pl) |
404
+ | [`n-queens-8.pl`](../examples/n-queens-8.pl) | Solves the 8-queens search problem with diagonal constraints. | [`output/n-queens-8.pl`](../examples/output/n-queens-8.pl) |
402
405
  | [`network-sla.pl`](../examples/network-sla.pl) | Checks network path SLA compliance. | [`output/network-sla.pl`](../examples/output/network-sla.pl) |
403
406
  | [`newton-raphson.pl`](../examples/newton-raphson.pl) | Finds roots by Newton-Raphson iteration. | [`output/newton-raphson.pl`](../examples/output/newton-raphson.pl) |
404
407
  | [`nixon-diamond.pl`](../examples/nixon-diamond.pl) | Reports the classic Nixon-diamond conflict. | [`output/nixon-diamond.pl`](../examples/output/nixon-diamond.pl) |
@@ -418,12 +421,14 @@ The repository includes examples for recursion, graph reachability, finite searc
418
421
  | [`reusable-builtins.pl`](../examples/reusable-builtins.pl) | Tours reusable numeric, list, and string builtins. | [`output/reusable-builtins.pl`](../examples/output/reusable-builtins.pl) |
419
422
  | [`riemann-hypothesis.pl`](../examples/riemann-hypothesis.pl) | Checks a finite catalogue of non-trivial zeta zeros against the Riemann-hypothesis condition. | [`output/riemann-hypothesis.pl`](../examples/output/riemann-hypothesis.pl) |
420
423
  | [`security-incident-correlation.pl`](../examples/security-incident-correlation.pl) | Correlates security incidents across signals. | [`output/security-incident-correlation.pl`](../examples/output/security-incident-correlation.pl) |
424
+ | [`send-more-money.pl`](../examples/send-more-money.pl) | Solves the SEND + MORE = MONEY cryptarithm. | [`output/send-more-money.pl`](../examples/output/send-more-money.pl) |
421
425
  | [`service-impact.pl`](../examples/service-impact.pl) | Analyzes service impact over cyclic dependencies. | [`output/service-impact.pl`](../examples/output/service-impact.pl) |
422
426
  | [`sieve.pl`](../examples/sieve.pl) | Enumerates primes with a sieve-style program. | [`output/sieve.pl`](../examples/output/sieve.pl) |
423
427
  | [`skolem-functions.pl`](../examples/skolem-functions.pl) | Generates deterministic functional terms. | [`output/skolem-functions.pl`](../examples/output/skolem-functions.pl) |
424
428
  | [`socket-age.pl`](../examples/socket-age.pl) | Shows socket-declared age reasoning inputs and plugs. | [`output/socket-age.pl`](../examples/output/socket-age.pl) |
425
429
  | [`socket-family.pl`](../examples/socket-family.pl) | Shows socket-declared family-source inputs and ancestry rules. | [`output/socket-family.pl`](../examples/output/socket-family.pl) |
426
430
  | [`socrates.pl`](../examples/socrates.pl) | Derives that Socrates is mortal. | [`output/socrates.pl`](../examples/output/socrates.pl) |
431
+ | [`stable-marriage.pl`](../examples/stable-marriage.pl) | Finds stable matchings by excluding blocking pairs. | [`output/stable-marriage.pl`](../examples/output/stable-marriage.pl) |
427
432
  | [`statistics-summary.pl`](../examples/statistics-summary.pl) | Computes population statistics for a sample. | [`output/statistics-summary.pl`](../examples/output/statistics-summary.pl) |
428
433
  | [`superdense-coding.pl`](../examples/superdense-coding.pl) | Models superdense-coding bit transmission. | [`output/superdense-coding.pl`](../examples/output/superdense-coding.pl) |
429
434
  | [`term-tools.pl`](../examples/term-tools.pl) | Inspects, builds, renders, and validates terms with reusable term/control builtins. | [`output/term-tools.pl`](../examples/output/term-tools.pl) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyelang",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "A small Prolog-syntax-subset logic programming language for rules, goals, answers, and proofs.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/playground.html CHANGED
@@ -408,6 +408,7 @@
408
408
  <button id="copy-source" type="button">Copy source</button>
409
409
  <button id="copy-output" type="button">Copy output</button>
410
410
  <button id="share" type="button">Copy share link</button>
411
+ <button id="create-gist" type="button">Create Gist share</button>
411
412
  </div>
412
413
  </section>
413
414
 
@@ -433,139 +434,146 @@
433
434
 
434
435
  <script type="module">
435
436
  const EXAMPLES = [
436
- "access-control-policy",
437
- "ackermann",
438
- "age",
439
- "aliases-and-namespaces",
440
- "alignment-demo",
441
- "allen-interval-calculus",
442
- "ancestor",
443
- "animal",
444
- "annotation",
445
- "auroracare",
446
- "backward",
447
- "basic-monadic",
448
- "bayes-diagnosis",
449
- "bayes-therapy",
450
- "beam-deflection",
451
- "blocks-world-planning",
452
- "bmi",
453
- "braking-safety-worlds",
454
- "buck-converter-design",
455
- "cache-performance",
456
- "canary-release",
457
- "cat-koko",
458
- "clinical-trial-screening",
459
- "collatz-1000",
460
- "combinatorics-findall-sort",
461
- "competitive-enzyme-kinetics",
462
- "complex",
463
- "composition-of-injective-functions-is-injective",
464
- "context-association",
465
- "context-schema-audit",
466
- "control-system",
467
- "cyclic-path",
468
- "d3-group",
469
- "dairy-energy-balance",
470
- "data-negotiation",
471
- "deep-taxonomy-10",
472
- "deep-taxonomy-100",
473
- "deep-taxonomy-1000",
474
- "deep-taxonomy-10000",
475
- "deep-taxonomy-100000",
476
- "delfour",
477
- "deontic-logic",
478
- "derived-backward-rule",
479
- "derived-rule",
480
- "diamond-property",
481
- "dijkstra-findall-sort",
482
- "dijkstra-risk-path",
483
- "dijkstra",
484
- "dining-philosophers",
485
- "dog",
486
- "dpv-odrl-purpose-mapping",
487
- "drone-corridor-planner",
488
- "easter-computus",
489
- "electrical-rc-filter",
490
- "epidemic-policy",
491
- "equivalence-classes-overlap-implies-same-class",
492
- "eulerian-path",
493
- "ev-range-worlds",
494
- "existential-rule",
495
- "exoplanet-validation-worlds",
496
- "expression-eval",
497
- "family-cousins",
498
- "fastpow",
499
- "fft8-numeric",
500
- "fibonacci",
501
- "field-nitrogen-balance",
502
- "flandor",
503
- "floating-point",
504
- "four-color-map",
505
- "fundamental-theorem-arithmetic",
506
- "gd-step-certified",
507
- "gdpr-compliance",
508
- "good-cobbler",
509
- "gps",
510
- "graph-reachability",
511
- "gray-code-counter",
512
- "greatest-lower-bound-uniqueness",
513
- "group-inverse-uniqueness",
514
- "hamiltonian-path",
515
- "hamming-code",
516
- "hanoi",
517
- "heat-loss",
518
- "heron-theorem",
519
- "ideal-gas-law",
520
- "illegitimate-reasoning",
521
- "knowledge-engineering-alignment-flow",
522
- "law-of-cosines",
523
- "least-squares-regression",
524
- "list-collection",
525
- "lldm",
526
- "manufacturing-quality-control",
527
- "microgrid-dispatch",
528
- "monkey-bananas",
529
- "network-sla",
530
- "newton-raphson",
531
- "nixon-diamond",
532
- "observability-log-correlation",
533
- "odrl-dpv-fpv-trust-flow",
534
- "odrl-dpv-healthcare-risk-ranked",
535
- "odrl-dpv-risk-ranked",
536
- "orbital-transfer-design",
537
- "path-discovery",
538
- "peano-arithmetic",
539
- "peasant",
540
- "pendulum-period",
541
- "polynomial",
542
- "proof-contrapositive",
543
- "quadratic-formula",
544
- "radioactive-decay",
545
- "reusable-builtins",
546
- "riemann-hypothesis",
547
- "security-incident-correlation",
548
- "service-impact",
549
- "sieve",
550
- "skolem-functions",
551
- "socket-age",
552
- "socket-family",
553
- "socrates",
554
- "statistics-summary",
555
- "superdense-coding",
556
- "term-tools",
557
- "trust-flow-provenance-threshold",
558
- "turing",
559
- "vector-similarity",
560
- "vulnerability-impact",
561
- "witch",
562
- "wolf-goat-cabbage",
563
- "zebra"
564
- ];
437
+ "access-control-policy",
438
+ "ackermann",
439
+ "age",
440
+ "aliases-and-namespaces",
441
+ "alignment-demo",
442
+ "allen-interval-calculus",
443
+ "ancestor",
444
+ "animal",
445
+ "annotation",
446
+ "auroracare",
447
+ "backward",
448
+ "basic-monadic",
449
+ "bayes-diagnosis",
450
+ "bayes-therapy",
451
+ "beam-deflection",
452
+ "blocks-world-planning",
453
+ "bmi",
454
+ "braking-safety-worlds",
455
+ "buck-converter-design",
456
+ "cache-performance",
457
+ "canary-release",
458
+ "cat-koko",
459
+ "clinical-trial-screening",
460
+ "collatz-1000",
461
+ "combinatorics-findall-sort",
462
+ "competitive-enzyme-kinetics",
463
+ "complex",
464
+ "composition-of-injective-functions-is-injective",
465
+ "context-association",
466
+ "context-schema-audit",
467
+ "control-system",
468
+ "cyclic-path",
469
+ "d3-group",
470
+ "dairy-energy-balance",
471
+ "data-negotiation",
472
+ "deep-taxonomy-10",
473
+ "deep-taxonomy-100",
474
+ "deep-taxonomy-1000",
475
+ "deep-taxonomy-10000",
476
+ "deep-taxonomy-100000",
477
+ "delfour",
478
+ "deontic-logic",
479
+ "derived-backward-rule",
480
+ "derived-rule",
481
+ "diamond-property",
482
+ "dijkstra",
483
+ "dijkstra-findall-sort",
484
+ "dijkstra-risk-path",
485
+ "dining-philosophers",
486
+ "dog",
487
+ "dpv-odrl-purpose-mapping",
488
+ "drone-corridor-planner",
489
+ "easter-computus",
490
+ "electrical-rc-filter",
491
+ "epidemic-policy",
492
+ "equivalence-classes-overlap-implies-same-class",
493
+ "eulerian-path",
494
+ "ev-range-worlds",
495
+ "existential-rule",
496
+ "exoplanet-validation-worlds",
497
+ "expression-eval",
498
+ "family-cousins",
499
+ "fastpow",
500
+ "fft8-numeric",
501
+ "fibonacci",
502
+ "field-nitrogen-balance",
503
+ "flandor",
504
+ "floating-point",
505
+ "four-color-map",
506
+ "fundamental-theorem-arithmetic",
507
+ "gd-step-certified",
508
+ "gdpr-compliance",
509
+ "good-cobbler",
510
+ "gps",
511
+ "graph-reachability",
512
+ "gray-code-counter",
513
+ "greatest-lower-bound-uniqueness",
514
+ "group-inverse-uniqueness",
515
+ "hamiltonian-path",
516
+ "hamming-code",
517
+ "hanoi",
518
+ "heat-loss",
519
+ "heron-theorem",
520
+ "ideal-gas-law",
521
+ "illegitimate-reasoning",
522
+ "job-shop-scheduling",
523
+ "knapsack-optimization",
524
+ "knowledge-engineering-alignment-flow",
525
+ "law-of-cosines",
526
+ "least-squares-regression",
527
+ "list-collection",
528
+ "lldm",
529
+ "manufacturing-quality-control",
530
+ "microgrid-dispatch",
531
+ "monkey-bananas",
532
+ "n-queens-8",
533
+ "network-sla",
534
+ "newton-raphson",
535
+ "nixon-diamond",
536
+ "observability-log-correlation",
537
+ "odrl-dpv-fpv-trust-flow",
538
+ "odrl-dpv-healthcare-risk-ranked",
539
+ "odrl-dpv-risk-ranked",
540
+ "orbital-transfer-design",
541
+ "path-discovery",
542
+ "peano-arithmetic",
543
+ "peasant",
544
+ "pendulum-period",
545
+ "polynomial",
546
+ "proof-contrapositive",
547
+ "quadratic-formula",
548
+ "radioactive-decay",
549
+ "reusable-builtins",
550
+ "riemann-hypothesis",
551
+ "security-incident-correlation",
552
+ "send-more-money",
553
+ "service-impact",
554
+ "sieve",
555
+ "skolem-functions",
556
+ "socket-age",
557
+ "socket-family",
558
+ "socrates",
559
+ "stable-marriage",
560
+ "statistics-summary",
561
+ "superdense-coding",
562
+ "term-tools",
563
+ "trust-flow-provenance-threshold",
564
+ "turing",
565
+ "vector-similarity",
566
+ "vulnerability-impact",
567
+ "witch",
568
+ "wolf-goat-cabbage",
569
+ "zebra"
570
+ ];
565
571
  const FALLBACK_SOURCE = `materialize(answer, 1).
566
572
  answer(ok) :- eq(ok, ok).
567
573
  `;
568
574
  const HIGHLIGHT_LIMIT = 200000;
575
+ const MAX_SHARE_URL_LENGTH = 1900;
576
+ const GIST_STATE_FILENAME = 'eyelang-playground-state.json';
569
577
  const KEYWORDS = new Set(['materialize', 'memoize']);
570
578
  const BUILTINS = new Set([
571
579
  "abs",
@@ -671,12 +679,17 @@ answer(ok) :- eq(ok, ok).
671
679
  let activeWorkerUrl = null;
672
680
  let renderToken = 0;
673
681
  let syntaxErrorLine = null;
682
+ let sourceReference = { kind: 'custom' };
683
+ let sourceDirty = false;
684
+ let backgroundReference = null;
674
685
 
675
686
  populateExamples(EXAMPLES);
676
- restoreFromHash() || loadExample('ancestor');
687
+ await restoreFromHashOrLoadDefault();
677
688
  loadVersion();
678
689
 
679
690
  source.addEventListener('input', () => {
691
+ sourceDirty = true;
692
+ sourceReference = { kind: 'custom' };
680
693
  clearSyntaxError();
681
694
  render();
682
695
  });
@@ -690,6 +703,7 @@ answer(ok) :- eq(ok, ok).
690
703
  document.querySelector('#copy-source').addEventListener('click', () => copyText(source.value, 'Source copied.'));
691
704
  document.querySelector('#copy-output').addEventListener('click', () => copyText(output.textContent, 'Output copied.'));
692
705
  document.querySelector('#share').addEventListener('click', copyShareLink);
706
+ document.querySelector('#create-gist').addEventListener('click', createGistShare);
693
707
 
694
708
  function populateExamples(names) {
695
709
  const current = exampleSelect.value;
@@ -716,7 +730,7 @@ answer(ok) :- eq(ok, ok).
716
730
  const exampleUrl = new URL(`./examples/${name}.pl`, location.href);
717
731
  const response = await fetch(exampleUrl, { cache: 'no-store' });
718
732
  if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
719
- setSource(await response.text(), `examples/${name}.pl`);
733
+ setSource(await response.text(), `examples/${name}.pl`, { kind: 'example', name });
720
734
  setStatus(`Loaded examples/${name}.pl.`);
721
735
  } catch (error) {
722
736
  setSource(FALLBACK_SOURCE, 'fallback program');
@@ -735,10 +749,11 @@ answer(ok) :- eq(ok, ok).
735
749
  if (loadBackground.checked) {
736
750
  backgroundSource = text;
737
751
  backgroundName = url;
752
+ backgroundReference = { kind: 'url', url };
738
753
  updateBackgroundStatus();
739
754
  setStatus(`Loaded background knowledge from ${url}.`);
740
755
  } else {
741
- setSource(text, url);
756
+ setSource(text, url, { kind: 'url', url });
742
757
  setStatus(`Loaded ${url}.`);
743
758
  }
744
759
  } catch (error) {
@@ -749,6 +764,7 @@ answer(ok) :- eq(ok, ok).
749
764
  function clearBackground() {
750
765
  backgroundSource = '';
751
766
  backgroundName = '';
767
+ backgroundReference = null;
752
768
  updateBackgroundStatus();
753
769
  setStatus('Background knowledge cleared.');
754
770
  }
@@ -759,9 +775,11 @@ answer(ok) :- eq(ok, ok).
759
775
  : 'No background knowledge loaded.';
760
776
  }
761
777
 
762
- function setSource(text, name) {
778
+ function setSource(text, name, reference = { kind: 'custom' }) {
763
779
  source.value = text;
764
780
  sourceName.textContent = name;
781
+ sourceReference = reference;
782
+ sourceDirty = false;
765
783
  clearSyntaxError();
766
784
  render();
767
785
  }
@@ -1001,39 +1019,182 @@ answer(ok) :- eq(ok, ok).
1001
1019
  }
1002
1020
 
1003
1021
  async function copyShareLink() {
1004
- const payload = JSON.stringify({
1022
+ const link = buildShareLink();
1023
+ if (link != null) {
1024
+ await copyText(link, 'Share link copied.');
1025
+ return;
1026
+ }
1027
+ await createGistShare({
1028
+ reason: 'This program is too large for an inline URL, so Copy share link will create a Gist share instead.',
1029
+ });
1030
+ }
1031
+
1032
+ function buildShareLink() {
1033
+ const referenced = buildReferenceShareLink();
1034
+ if (referenced && referenced.length <= MAX_SHARE_URL_LENGTH) return referenced;
1035
+ const embedded = `${basePlaygroundUrl()}#state=${encodeState(currentShareState())}`;
1036
+ return embedded.length <= MAX_SHARE_URL_LENGTH ? embedded : null;
1037
+ }
1038
+
1039
+ function buildReferenceShareLink() {
1040
+ const params = new URLSearchParams();
1041
+ if (proof.checked) params.set('proof', '1');
1042
+ if (stats.checked) params.set('stats', '1');
1043
+ if (backgroundSource.trim()) {
1044
+ if (backgroundReference?.kind !== 'url') return null;
1045
+ params.set('background-url', backgroundReference.url);
1046
+ }
1047
+ if (!sourceDirty && sourceReference.kind === 'example') params.set('example', sourceReference.name);
1048
+ else if (!sourceDirty && sourceReference.kind === 'url') params.set('url', sourceReference.url);
1049
+ else return null;
1050
+ return `${basePlaygroundUrl()}#${params.toString()}`;
1051
+ }
1052
+
1053
+ async function createGistShare(options = {}) {
1054
+ const stateText = JSON.stringify(currentShareState(), null, 2);
1055
+ if (options.reason) setStatus(options.reason);
1056
+ const token = prompt(`${options.reason ? `${options.reason}\n\n` : ''}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 and open gist.github.com.`);
1057
+ if (token === null) {
1058
+ setStatus('Gist share cancelled.');
1059
+ return;
1060
+ }
1061
+ if (!token.trim()) {
1062
+ await copyText(stateText, `Gist-ready state copied. Create a Gist file named ${GIST_STATE_FILENAME}, paste it, then copy its raw URL into a playground #state-url link.`);
1063
+ window.open('https://gist.github.com/', '_blank', 'noopener');
1064
+ return;
1065
+ }
1066
+
1067
+ try {
1068
+ setStatus('Creating GitHub Gist…');
1069
+ const files = {
1070
+ [GIST_STATE_FILENAME]: { content: stateText },
1071
+ 'program.pl': { content: source.value },
1072
+ };
1073
+ if (backgroundSource.trim()) files['background.pl'] = { content: backgroundSource };
1074
+ const response = await fetch('https://api.github.com/gists', {
1075
+ method: 'POST',
1076
+ headers: {
1077
+ Accept: 'application/vnd.github+json',
1078
+ Authorization: `Bearer ${token.trim()}`,
1079
+ 'Content-Type': 'application/json',
1080
+ },
1081
+ body: JSON.stringify({
1082
+ description: 'Eyelang playground share',
1083
+ public: false,
1084
+ files,
1085
+ }),
1086
+ });
1087
+ const gist = await response.json().catch(() => ({}));
1088
+ if (!response.ok) throw new Error(gist.message || `${response.status} ${response.statusText}`);
1089
+ const rawUrl = gist.files?.[GIST_STATE_FILENAME]?.raw_url;
1090
+ if (!rawUrl) throw new Error('Gist response did not include a raw state URL.');
1091
+ await copyText(`${basePlaygroundUrl()}#state-url=${encodeURIComponent(rawUrl)}`, 'Gist share link copied.');
1092
+ } catch (error) {
1093
+ await copyText(stateText, `Could not create Gist: ${formatError(error)}. Gist-ready state copied instead.`);
1094
+ }
1095
+ }
1096
+
1097
+ function currentShareState() {
1098
+ return {
1005
1099
  source: source.value,
1006
1100
  proof: proof.checked,
1007
1101
  stats: stats.checked,
1008
1102
  backgroundSource,
1009
1103
  backgroundName,
1010
- });
1011
- const data = new TextEncoder().encode(payload);
1104
+ };
1105
+ }
1106
+
1107
+ function encodeState(payload) {
1108
+ const data = new TextEncoder().encode(JSON.stringify(payload));
1012
1109
  let binary = '';
1013
1110
  for (const byte of data) binary += String.fromCharCode(byte);
1014
- const encoded = btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
1015
- await copyText(`${location.href.split('#')[0]}#state=${encoded}`, 'Share link copied.');
1111
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
1016
1112
  }
1017
1113
 
1018
- function restoreFromHash() {
1019
- if (!location.hash.startsWith('#state=')) return false;
1114
+ function decodeState(encoded) {
1115
+ const base64 = encoded.replaceAll('-', '+').replaceAll('_', '/');
1116
+ const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
1117
+ const bytes = Uint8Array.from(atob(padded), (ch) => ch.charCodeAt(0));
1118
+ return JSON.parse(new TextDecoder().decode(bytes));
1119
+ }
1120
+
1121
+ function basePlaygroundUrl() {
1122
+ return location.href.split('#')[0];
1123
+ }
1124
+
1125
+ async function restoreFromHashOrLoadDefault() {
1126
+ if (!(await restoreFromHash())) await loadExample('ancestor');
1127
+ }
1128
+
1129
+ async function restoreFromHash() {
1130
+ if (!location.hash || location.hash === '#') return false;
1131
+ const params = new URLSearchParams(location.hash.slice(1));
1020
1132
  try {
1021
- const encoded = location.hash.slice(7).replaceAll('-', '+').replaceAll('_', '/');
1022
- const padded = encoded + '='.repeat((4 - encoded.length % 4) % 4);
1023
- const bytes = Uint8Array.from(atob(padded), (ch) => ch.charCodeAt(0));
1024
- const payload = JSON.parse(new TextDecoder().decode(bytes));
1025
- setSource(String(payload.source || ''), 'shared program');
1026
- proof.checked = Boolean(payload.proof);
1027
- stats.checked = Boolean(payload.stats);
1028
- backgroundSource = String(payload.backgroundSource || '');
1029
- backgroundName = String(payload.backgroundName || 'shared background');
1030
- updateBackgroundStatus();
1031
- setStatus('Loaded shared program.');
1032
- return true;
1133
+ if (params.has('state')) {
1134
+ applySharedState(decodeState(params.get('state')), 'shared program');
1135
+ setStatus('Loaded shared program.');
1136
+ return true;
1137
+ }
1138
+ if (params.has('state-url')) {
1139
+ const stateUrl = params.get('state-url');
1140
+ const response = await fetch(stateUrl);
1141
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
1142
+ applySharedState(await response.json(), `shared state from ${stateUrl}`);
1143
+ setStatus('Loaded shared Gist state.');
1144
+ return true;
1145
+ }
1146
+ await restoreBackgroundUrl(params);
1147
+ applyOptionParams(params);
1148
+ if (params.has('example')) {
1149
+ await loadExample(params.get('example'));
1150
+ applyOptionParams(params);
1151
+ return true;
1152
+ }
1153
+ if (params.has('url')) {
1154
+ await loadSourceFromUrl(params.get('url'), false);
1155
+ applyOptionParams(params);
1156
+ return true;
1157
+ }
1033
1158
  } catch (error) {
1034
1159
  setStatus(`Could not read shared state: ${formatError(error)}`, true);
1035
1160
  return false;
1036
1161
  }
1162
+ return false;
1163
+ }
1164
+
1165
+ async function restoreBackgroundUrl(params) {
1166
+ const url = params.get('background-url');
1167
+ if (!url) return;
1168
+ await loadSourceFromUrl(url, true);
1169
+ }
1170
+
1171
+ async function loadSourceFromUrl(url, asBackground) {
1172
+ const response = await fetch(url);
1173
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
1174
+ const text = await response.text();
1175
+ if (asBackground) {
1176
+ backgroundSource = text;
1177
+ backgroundName = url;
1178
+ backgroundReference = { kind: 'url', url };
1179
+ updateBackgroundStatus();
1180
+ } else {
1181
+ setSource(text, url, { kind: 'url', url });
1182
+ }
1183
+ }
1184
+
1185
+ function applySharedState(payload, label) {
1186
+ setSource(String(payload.source || ''), label, { kind: 'custom' });
1187
+ proof.checked = Boolean(payload.proof);
1188
+ stats.checked = Boolean(payload.stats);
1189
+ backgroundSource = String(payload.backgroundSource || '');
1190
+ backgroundName = String(payload.backgroundName || 'shared background');
1191
+ backgroundReference = null;
1192
+ updateBackgroundStatus();
1193
+ }
1194
+
1195
+ function applyOptionParams(params) {
1196
+ proof.checked = params.get('proof') === '1' || params.get('proof') === 'true';
1197
+ stats.checked = params.get('stats') === '1' || params.get('stats') === 'true';
1037
1198
  }
1038
1199
 
1039
1200
  async function loadVersion() {
@@ -524,6 +524,17 @@ function whiteBoxCases() {
524
524
  assertEqual(termToString(candidates.primary[0].head, new Env(), true), 'edge(a, b)', 'primary head');
525
525
  },
526
526
  },
527
+ {
528
+ name: 'n-queens example keeps diagonal checks memoized',
529
+ run: () => {
530
+ const text = fs.readFileSync(path.join(packageRoot, 'examples', 'n-queens-8.pl'), 'utf8');
531
+ const program = Program.parseSources([{ text, filename: 'n-queens-8.pl' }]);
532
+ const group = program.findGroup('no_diagonal_attack', 3);
533
+ assertEqual(Boolean(group), true, 'no_diagonal_attack/3 group exists');
534
+ assertEqual(group.memoized, true, 'no_diagonal_attack/3 memoized');
535
+ assertEqual(group.recursive, true, 'no_diagonal_attack/3 recursive');
536
+ },
537
+ },
527
538
  {
528
539
  name: 'collatz example keeps recursive trajectory predicate memoized',
529
540
  run: () => {
@@ -678,6 +689,18 @@ function playgroundStaticIssues() {
678
689
  if (!html.includes('id="line-numbers"') || !html.includes('updateLineNumbers') || !html.includes('lineNumbersInner.style.transform') || !html.includes('--line-number-bg')) {
679
690
  issues.push('playground editor must include synced line numbers');
680
691
  }
692
+ if (!html.includes('MAX_SHARE_URL_LENGTH') || !html.includes('buildReferenceShareLink') || !html.includes("params.set('example'") || !html.includes("params.set('url'")) {
693
+ issues.push('playground share links must avoid embedding large example or URL-loaded sources');
694
+ }
695
+ if (!html.includes('id="create-gist"') || !html.includes('createGistShare') || !html.includes('GIST_STATE_FILENAME') || !html.includes("fetch('https://api.github.com/gists'")) {
696
+ issues.push('playground must support Gist-backed sharing for large programs');
697
+ }
698
+ if (!html.includes('await createGistShare({') || html.includes('Use “Create Gist share” instead')) {
699
+ issues.push('playground Copy share link must automatically fall back to Gist sharing for large programs');
700
+ }
701
+ if (!html.includes("params.has('state-url')") || !html.includes('#state-url=')) {
702
+ issues.push('playground must restore state from raw Gist state URLs');
703
+ }
681
704
  if (!html.includes('id="example-search"') || !html.includes('id="examples"')) issues.push('playground must include searchable examples');
682
705
  const scriptMatch = html.match(new RegExp('<script type="module">\\n([\\s\\S]*?)\\n <\\/script>'));
683
706
  if (scriptMatch == null) {