eyelang 1.4.0 → 1.5.0

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.
Files changed (81) hide show
  1. package/README.md +22 -43
  2. package/SPEC.md +16 -16
  3. package/bin/eyelang +0 -0
  4. package/conformance/README.md +4 -5
  5. package/conformance/cases/core/001_fact_output.pl +4 -0
  6. package/conformance/cases/core/002_rule_recursion.pl +4 -2
  7. package/conformance/cases/core/003_terms_and_readback.pl +15 -13
  8. package/conformance/cases/core/004_conjunction_and_parentheses.pl +1 -0
  9. package/conformance/cases/core/005_list_deconstruction.pl +1 -0
  10. package/conformance/cases/core/006_comma_formula_data.pl +1 -0
  11. package/conformance/cases/core/007_anonymous_variables.pl +1 -0
  12. package/conformance/cases/core/008_graphic_atoms.pl +5 -3
  13. package/conformance/cases/core/009_comments_and_whitespace.pl +1 -0
  14. package/conformance/cases/core/010_variable_scope_and_reuse.pl +1 -0
  15. package/conformance/cases/core/011_predicate_arity.pl +1 -0
  16. package/conformance/cases/core/012_nested_compound_unification.pl +1 -0
  17. package/conformance/cases/core/013_multiple_clauses_order.pl +1 -0
  18. package/conformance/cases/core/014_failure_filters_answers.pl +2 -0
  19. package/conformance/cases/core/015_improper_list_unification.pl +1 -0
  20. package/conformance/cases/core/016_zero_arity_compound.pl +1 -0
  21. package/conformance/cases/core/017_three_step_recursion.pl +1 -0
  22. package/conformance/cases/core/018_quoted_atom_readback.pl +1 -0
  23. package/conformance/cases/core/019_parenthesized_three_conjuncts.pl +1 -0
  24. package/conformance/cases/core/020_nested_list_terms.pl +1 -0
  25. package/conformance/cases/extension/001_default_derived_output.pl +1 -1
  26. package/conformance/cases/extension/002_materialize_focus.pl +1 -1
  27. package/conformance/cases/extension/003_arithmetic_and_comparison.pl +1 -0
  28. package/conformance/cases/extension/004_strings_and_atoms.pl +1 -0
  29. package/conformance/cases/extension/005_lists_aggregation_ordering.pl +1 -0
  30. package/conformance/cases/extension/006_formula_terms.pl +1 -0
  31. package/conformance/cases/extension/007_negation_once_generators.pl +1 -0
  32. package/conformance/cases/extension/008_equality_and_inequality.pl +1 -0
  33. package/conformance/cases/extension/009_list_relations.pl +1 -0
  34. package/conformance/cases/extension/010_append_splits.pl +1 -0
  35. package/conformance/cases/extension/011_matching_and_comparison.pl +1 -0
  36. package/conformance/cases/extension/012_memoize_declaration.pl +5 -3
  37. package/conformance/cases/extension/013_numeric_functions.pl +1 -0
  38. package/conformance/cases/extension/014_between_enumeration.pl +1 -0
  39. package/conformance/cases/extension/015_smallest_divisor.pl +1 -0
  40. package/conformance/cases/extension/016_negation_filter.pl +1 -0
  41. package/conformance/cases/extension/017_once_user_predicate.pl +1 -0
  42. package/conformance/cases/extension/018_findall_user_goal.pl +1 -0
  43. package/conformance/cases/extension/019_sort_deduplicates_atoms.pl +1 -0
  44. package/conformance/cases/extension/020_append_bound_prefix_suffix.pl +1 -0
  45. package/conformance/cases/extension/021_nth0_index_generation.pl +1 -0
  46. package/conformance/cases/extension/022_set_nth0_edges.pl +1 -0
  47. package/conformance/cases/extension/023_select_duplicate_occurrences.pl +1 -0
  48. package/conformance/cases/extension/024_not_member_filter.pl +1 -0
  49. package/conformance/cases/extension/025_is_list_filter.pl +1 -0
  50. package/conformance/cases/extension/026_nested_formula_terms.pl +1 -0
  51. package/conformance/cases/extension/027_materialize_excludes_source_fact.pl +1 -1
  52. package/conformance/cases/extension/028_numeric_and_lexical_comparison.pl +1 -0
  53. package/conformance/cases/extension/029_string_matching_filters.pl +1 -0
  54. package/conformance/cases/extension/030_string_and_atom_concat.pl +1 -0
  55. package/conformance/cases/extension/031_countall_empty_and_nonempty.pl +2 -0
  56. package/conformance/cases/extension/032_sumall_numeric_template.pl +2 -0
  57. package/conformance/cases/extension/033_aggregate_min_template.pl +2 -0
  58. package/conformance/cases/extension/034_aggregate_max_compound_key.pl +2 -0
  59. package/conformance/expected/extension/031_countall_empty_and_nonempty.out +1 -1
  60. package/conformance/expected/extension/032_sumall_numeric_template.out +1 -1
  61. package/conformance/expected/extension/033_aggregate_min_template.out +1 -1
  62. package/conformance/expected/extension/034_aggregate_max_compound_key.out +1 -1
  63. package/examples/basic-monadic.pl +1 -1
  64. package/examples/monkey-bananas.pl +1 -1
  65. package/examples/path-discovery.pl +3 -3
  66. package/examples/peano-arithmetic.pl +1 -1
  67. package/package.json +1 -1
  68. package/playground-worker.mjs +2 -15
  69. package/playground.html +5 -25
  70. package/src/builtins/aggregation.js +5 -5
  71. package/src/builtins/control.js +3 -3
  72. package/src/builtins/registry.js +7 -0
  73. package/src/cli.js +36 -38
  74. package/src/index.js +21 -28
  75. package/src/parser.js +3 -3
  76. package/src/solver.js +2 -2
  77. package/test/run-conformance.js +20 -45
  78. package/test/run-examples.js +42 -50
  79. package/test/run-regression.js +38 -59
  80. package/conformance/cases/core/001_fact_query.pl +0 -2
  81. /package/conformance/expected/core/{001_fact_query.out → 001_fact_output.out} +0 -0
@@ -4,3 +4,4 @@ candidate(b).
4
4
  candidate(c).
5
5
  blocked(b).
6
6
  answer(open, X) :- candidate(X), not(blocked(X)).
7
+ materialize(answer, 2).
@@ -2,3 +2,4 @@
2
2
  choice(a).
3
3
  choice(b).
4
4
  answer(first, X) :- once(choice(X)).
5
+ materialize(answer, 2).
@@ -3,3 +3,4 @@ p(b).
3
3
  p(a).
4
4
  p(b).
5
5
  answer(bag, X) :- findall(P, p(P), X).
6
+ materialize(answer, 2).
@@ -1,2 +1,3 @@
1
1
  % SPEC 9.8: sort/2 sorts and deduplicates a proper list.
2
2
  answer(sorted, X) :- sort([c, a, b, a], X).
3
+ materialize(answer, 2).
@@ -1,3 +1,4 @@
1
1
  % SPEC 9.7: append/3 supports bound prefix and suffix use cases.
2
2
  answer(prefix, X) :- append(X, [c], [a, b, c]).
3
3
  answer(suffix, X) :- append([a], X, [a, b, c]).
4
+ materialize(answer, 2).
@@ -1,2 +1,3 @@
1
1
  % SPEC 9.7: nth0/3 can bind the index for a known list element.
2
2
  answer(index, I) :- nth0(I, [a, b, c], b).
3
+ materialize(answer, 2).
@@ -1,3 +1,4 @@
1
1
  % SPEC 9.7: set_nth0/4 updates zero-based positions functionally.
2
2
  answer(first, X) :- set_nth0(0, [a, b, c], x, X).
3
3
  answer(last, X) :- set_nth0(2, [a, b, c], z, X).
4
+ materialize(answer, 2).
@@ -1,2 +1,3 @@
1
1
  % SPEC 9.7: select/3 enumerates removals of matching occurrences.
2
2
  answer(rest, X) :- select(a, [a, b, a], X).
3
+ materialize(answer, 2).
@@ -3,3 +3,4 @@ candidate(a).
3
3
  candidate(b).
4
4
  candidate(c).
5
5
  answer(not_present, X) :- candidate(X), not_member(X, [a, b]).
6
+ materialize(answer, 2).
@@ -2,3 +2,4 @@
2
2
  thing([a, b]).
3
3
  thing(pair(a, b)).
4
4
  answer(list, X) :- thing(X), is_list(X).
5
+ materialize(answer, 2).
@@ -2,3 +2,4 @@
2
2
  formula(((name(a, "A"), knows(a, b)), likes(b, c))).
3
3
  answer(atom, A) :- formula(F), formula_atom(F, A).
4
4
  answer(binary, exposed(S, P, O)) :- formula(F), formula_binary(F, S, P, O).
5
+ materialize(answer, 2).
@@ -1,4 +1,4 @@
1
- % SPEC 11: no-query materialization excludes source facts even if also derivable.
1
+ % SPEC 11: default materialization excludes source facts even if also derivable.
2
2
  materialize(answer, 2).
3
3
  seed(a).
4
4
  answer(a, ok).
@@ -2,3 +2,4 @@
2
2
  answer(numeric_gt, true) :- gt(10, 2).
3
3
  answer(numeric_le, true) :- le(2, 2.0).
4
4
  answer(lexical_ge, true) :- ge(beta, alpha).
5
+ materialize(answer, 2).
@@ -3,3 +3,4 @@ text(a, "alpha").
3
3
  text(b, "beta").
4
4
  answer(has_ph, K) :- text(K, T), matches(T, "ph").
5
5
  answer(no_ph, K) :- text(K, T), not_matches(T, "ph").
6
+ materialize(answer, 2).
@@ -1,3 +1,4 @@
1
1
  % SPEC 9.6: atom_concat/3 and str_concat/3 concatenate like-typed scalars.
2
2
  answer(atom, X) :- atom_concat(eye, lang, X).
3
3
  answer(string, X) :- str_concat("eye", "lang", X).
4
+ materialize(answer, 2).
@@ -1,2 +1,4 @@
1
1
  item(a).
2
2
  item(b).
3
+ answer(counts, counts(C, Z)) :- countall(item(X), C), countall(missing(X), Z).
4
+ materialize(answer, 2).
@@ -1,3 +1,5 @@
1
1
  score(a, 4).
2
2
  score(b, 5).
3
3
  score(c, -2).
4
+ answer(sum, Sum) :- sumall(S, score(_Item, S), Sum).
5
+ materialize(answer, 2).
@@ -1,3 +1,5 @@
1
1
  score(alpha, 7).
2
2
  score(beta, 3).
3
3
  score(gamma, 5).
4
+ answer(min, result(BestS, Best)) :- aggregate_min(S, item(Name, S), score(Name, S), BestS, Best).
5
+ materialize(answer, 2).
@@ -1,3 +1,5 @@
1
1
  score(alpha, 7).
2
2
  score(beta, 7).
3
3
  score(gamma, 5).
4
+ answer(max, result(Key, BestName)) :- aggregate_max([S, Name], Name, score(Name, S), Key, BestName).
5
+ materialize(answer, 2).
@@ -1 +1 @@
1
- (countall(item(X), 2), countall(missing(X), 0)).
1
+ answer(counts, counts(2, 0)).
@@ -1 +1 @@
1
- sumall(S, score(_Item, S), 7).
1
+ answer(sum, 7).
@@ -1 +1 @@
1
- aggregate_min(S, item(Name, S), score(Name, S), 3, item(beta, 3)).
1
+ answer(min, result(3, item(beta, 3))).
@@ -1 +1 @@
1
- aggregate_max([S, Name], Name, score(Name, S), [7, beta], beta).
1
+ answer(max, result([7, beta], beta)).
@@ -1,7 +1,7 @@
1
1
  % Basic Monadic Benchmark port from EYE reasoning/basic-monadic.
2
2
  %
3
3
  % This example uses the ten Turtle inputs 1tt1.ttl ... 1tt10.ttl
4
- % from EYE and the EYE query shape:
4
+ % from EYE and the EYE selected-goal shape:
5
5
  % D0 R D1, D1 R D2, ..., D9 R D0 -> R cycle (D0 ... D9 D0).
6
6
  %
7
7
  % The expected output contains 1518 distinct cycle relations, matching
@@ -2,7 +2,7 @@
2
2
  % input/monkey-bananas.pl.
3
3
  %
4
4
  % A state is [bananas_location, monkey_location, box_location, on_box,
5
- % has_bananas]. The query searches bounded move lists and derives successful
5
+ % has_bananas]. The selected output searches bounded move lists and derives successful
6
6
  % plans.
7
7
 
8
8
  materialize(plan, 2).
@@ -1,11 +1,11 @@
1
1
  % Generic path discovery over the air-routes graph.
2
- % Change or add route_query(FromLabel, ToLabel, MaxStopOvers) to answer other routes.
2
+ % Change or add route_request(FromLabel, ToLabel, MaxStopOvers) to answer other routes.
3
3
  materialize(airroute, 2).
4
4
 
5
- route_query("Ostend-Bruges International Airport", "Václav Havel Airport Prague", 2).
5
+ route_request("Ostend-Bruges International Airport", "Václav Havel Airport Prague", 2).
6
6
 
7
7
  airroute(discovered, RouteText) :-
8
- route_query(From, To, MaxStopOvers),
8
+ route_request(From, To, MaxStopOvers),
9
9
  airport(Source, From),
10
10
  airport(Destination, To),
11
11
  add(MaxStopOvers, 1, MaxLegs),
@@ -1,7 +1,7 @@
1
1
  % Peano arithmetic port from EYE reasoning/peano.
2
2
  %
3
3
  % The EYE example defines add, multiply and factorial over Peano numerals.
4
- % Its query computes (1 * 2 + 3)! and emits the factorial of 5.
4
+ % Its selected output computes (1 * 2 + 3)! and emits the factorial of 5.
5
5
 
6
6
  materialize(factorial, 2).
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyelang",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "A small rule engine for Prolog-style Horn clauses",
6
6
  "keywords": [
@@ -46,7 +46,7 @@ async function initialize(requestId) {
46
46
  }
47
47
 
48
48
  async function runEyelang(request) {
49
- const { id, program, query, stats, proof = false } = request;
49
+ const { id, program, stats, proof = false } = request;
50
50
  if (active) {
51
51
  self.postMessage({
52
52
  type: 'result',
@@ -72,7 +72,6 @@ async function runEyelang(request) {
72
72
  Program,
73
73
  Solver,
74
74
  copyResolved,
75
- parseQueryGoal,
76
75
  termIsGround,
77
76
  termToString,
78
77
  whyNoProof,
@@ -82,19 +81,7 @@ async function runEyelang(request) {
82
81
  phase = 'parsing input';
83
82
  const parsed = Program.parse(program || '', { filename: '<playground>', sourceMetadata: proof, markRecursive: proof });
84
83
 
85
- if (query && query.trim()) {
86
- phase = 'parsing query';
87
- const goal = parseQueryGoal(query.trim());
88
- phase = 'solving query';
89
- const solver = new Solver(parsed);
90
- const out = [];
91
- for (const env of solver.solve([goal], new Env(), 0)) {
92
- out.push(`${termToString(goal, env, true)}.\n`);
93
- if (proof) appendExplanation(out, parsed, copyResolved(goal, env), whyProof, whyNoProof);
94
- }
95
- stdout = out.join('');
96
- if (stats) stderr = formatStats(solver.stats);
97
- } else {
84
+ {
98
85
  phase = 'materializing output';
99
86
  const goals = parsed.materializationGoals();
100
87
  const materializedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
package/playground.html CHANGED
@@ -172,13 +172,6 @@
172
172
  button.primary { border-color: var(--accent); background: var(--accent); color: white; }
173
173
  button.primary:hover { background: var(--accent-strong); }
174
174
  button:disabled { opacity: 0.55; cursor: not-allowed; }
175
- .query-row {
176
- display: grid;
177
- grid-template-columns: minmax(0, 1fr);
178
- gap: 8px;
179
- margin-top: 10px;
180
- }
181
- .query-row input { width: 100%; }
182
175
  .run-options {
183
176
  margin-top: 10px;
184
177
  align-items: flex-start;
@@ -399,10 +392,6 @@
399
392
  <textarea id="programInput" class="program-textarea" spellcheck="false" autocapitalize="off" autocomplete="off" wrap="soft" aria-label="Editable eyelang program"></textarea>
400
393
  </div>
401
394
  </div>
402
- <div class="query-row">
403
- <label for="queryInput">Optional <code>--query</code> goal</label>
404
- <input id="queryInput" type="text" placeholder="ancestor(pat, X)" spellcheck="false">
405
- </div>
406
395
  <div class="toolbar run-options">
407
396
  <label class="row"><input id="statsToggle" type="checkbox"> include <code>--stats</code> counters</label>
408
397
  <label class="row"><input id="proofToggle" type="checkbox"> include <code>--proof</code> explanations</label>
@@ -415,7 +404,7 @@
415
404
  <button id="clearSavedButton" type="button">Clear autosave</button>
416
405
  <span id="status" class="status" role="status">Idle.</span>
417
406
  </div>
418
- <p class="small">The playground concatenates loaded background programs before the editor contents. Use the query field and <code>--stats</code> toggle like the CLI. Output is ordinary eyelang syntax: answer facts by default, with <code>why/2</code> explanations added when <code>--proof</code> is selected.</p>
407
+ <p class="small">The playground concatenates loaded background programs before the editor contents. Use <code>materialize/2</code> declarations to focus output and the <code>--stats</code> toggle for counters. Output is ordinary eyelang syntax: answer facts by default, with <code>why/2</code> explanations added when <code>--proof</code> is selected.</p>
419
408
  </div>
420
409
  </section>
421
410
 
@@ -566,13 +555,12 @@
566
555
  "zebra"
567
556
  ];
568
557
  const DEFAULT_PROGRAM = "% Socrates is mortal.\n\nmaterialize(type, 2).\nmaterialize(is, 2).\n\ntype(socrates, man).\n\ntype(X, mortal) :-\n type(X, man).\n\nis(test, true) :-\n type(socrates, mortal).";
569
- const STORAGE_KEY = 'eyelang.playground.v6';
558
+ const STORAGE_KEY = 'eyelang.playground.v7';
570
559
 
571
560
  const programInput = document.getElementById('programInput');
572
561
  const programSyntax = document.getElementById('programSyntax');
573
562
  const programGutter = document.getElementById('programGutter');
574
563
  const programErrorBand = document.getElementById('programErrorBand');
575
- const queryInput = document.getElementById('queryInput');
576
564
  const statsToggle = document.getElementById('statsToggle');
577
565
  const proofToggle = document.getElementById('proofToggle');
578
566
  const commandPreview = document.getElementById('commandPreview');
@@ -943,7 +931,6 @@
943
931
  saveTimer = setTimeout(() => {
944
932
  localStorage.setItem(STORAGE_KEY, JSON.stringify({
945
933
  program: programInput.value,
946
- query: queryInput.value,
947
934
  stats: statsToggle.checked,
948
935
  proof: proofToggle.checked,
949
936
  backgroundPrograms,
@@ -979,7 +966,6 @@
979
966
  const args = ['eyelang'];
980
967
  if (statsToggle.checked) args.push('--stats');
981
968
  if (proofToggle.checked) args.push('--proof');
982
- if (queryInput.value.trim()) args.push('--query', shellQuote(queryInput.value.trim()));
983
969
  args.push(currentProgramStillMatchesUrl() ? currentProgramUrl : 'input.pl');
984
970
  commandPreview.textContent = args.join(' ');
985
971
  }
@@ -989,7 +975,6 @@
989
975
  if (searchParams.has('url')) {
990
976
  const sourceUrl = searchParams.get('url');
991
977
  urlInput.value = sourceUrl;
992
- queryInput.value = searchParams.get('query') || searchParams.get('q') || '';
993
978
  statsToggle.checked = parseFlag(searchParams.get('stats') || searchParams.get('s'));
994
979
  proofToggle.checked = parseFlag(searchParams.get('proof') || searchParams.get('pr'));
995
980
  updateCommandPreview();
@@ -1006,7 +991,6 @@
1006
991
  if (params.has('p')) {
1007
992
  try {
1008
993
  setProgramValue(decodePayload(params.get('p')));
1009
- queryInput.value = params.has('q') ? decodePayload(params.get('q')) : '';
1010
994
  statsToggle.checked = parseFlag(params.get('s'));
1011
995
  proofToggle.checked = parseFlag(params.get('r'));
1012
996
  currentProgramUrl = null;
@@ -1018,11 +1002,10 @@
1018
1002
  }
1019
1003
  }
1020
1004
  try {
1021
- const raw = localStorage.getItem(STORAGE_KEY) || localStorage.getItem('eyelang.playground.v4') || localStorage.getItem('eyelang.playground.v3') || localStorage.getItem('eyelang.playground.v2') || localStorage.getItem('eyelang.playground.v1');
1005
+ const raw = localStorage.getItem(STORAGE_KEY) || localStorage.getItem('eyelang.playground.v6') || localStorage.getItem('eyelang.playground.v5') || localStorage.getItem('eyelang.playground.v4') || localStorage.getItem('eyelang.playground.v3') || localStorage.getItem('eyelang.playground.v2') || localStorage.getItem('eyelang.playground.v1');
1022
1006
  if (raw) {
1023
1007
  const payload = JSON.parse(raw);
1024
1008
  setProgramValue(payload.program || DEFAULT_PROGRAM);
1025
- queryInput.value = payload.query || '';
1026
1009
  statsToggle.checked = Boolean(payload.stats);
1027
1010
  proofToggle.checked = Boolean(payload.proof);
1028
1011
  backgroundPrograms = Array.isArray(payload.backgroundPrograms) ? payload.backgroundPrograms : [];
@@ -1248,7 +1231,6 @@
1248
1231
  type: 'run',
1249
1232
  id: currentRunId,
1250
1233
  program: combinedProgram(),
1251
- query: queryInput.value,
1252
1234
  stats: statsToggle.checked,
1253
1235
  proof: proofToggle.checked,
1254
1236
  });
@@ -1304,13 +1286,11 @@
1304
1286
  if (currentProgramStillMatchesUrl()) {
1305
1287
  url.search = '';
1306
1288
  url.searchParams.set('url', currentProgramUrl);
1307
- if (queryInput.value.trim()) url.searchParams.set('query', queryInput.value.trim());
1308
1289
  if (statsToggle.checked) url.searchParams.set('stats', '1');
1309
1290
  if (proofToggle.checked) url.searchParams.set('proof', '1');
1310
1291
  } else {
1311
1292
  const params = new URLSearchParams();
1312
1293
  params.set('p', encodePayload(programInput.value));
1313
- if (queryInput.value.trim()) params.set('q', encodePayload(queryInput.value));
1314
1294
  if (statsToggle.checked) params.set('s', '1');
1315
1295
  if (proofToggle.checked) params.set('r', '1');
1316
1296
  url.search = '';
@@ -1324,7 +1304,6 @@
1324
1304
  loadExampleButton.addEventListener('click', () => loadIntoEditor(`examples/${exampleSelect.value}.pl`, false).catch(error => setStatus(error.message, 'error')));
1325
1305
  resetButton.addEventListener('click', () => {
1326
1306
  setProgramValue(DEFAULT_PROGRAM);
1327
- queryInput.value = '';
1328
1307
  statsToggle.checked = false;
1329
1308
  proofToggle.checked = false;
1330
1309
  backgroundPrograms = [];
@@ -1357,6 +1336,8 @@
1357
1336
  copyLinkButton.addEventListener('click', () => copyShareLink().catch(error => setStatus(`Could not copy link: ${error.message}`, 'error')));
1358
1337
  clearSavedButton.addEventListener('click', () => {
1359
1338
  localStorage.removeItem(STORAGE_KEY);
1339
+ localStorage.removeItem('eyelang.playground.v6');
1340
+ localStorage.removeItem('eyelang.playground.v5');
1360
1341
  localStorage.removeItem('eyelang.playground.v4');
1361
1342
  localStorage.removeItem('eyelang.playground.v3');
1362
1343
  localStorage.removeItem('eyelang.playground.v2');
@@ -1366,7 +1347,6 @@
1366
1347
  programInput.addEventListener('input', () => { clearErrorLine(); updateProgramSyntax(); markProgramEdited(); });
1367
1348
  programInput.addEventListener('scroll', updateProgramHighlight);
1368
1349
  combinedOutput.addEventListener('scroll', () => { outputGutter.scrollTop = combinedOutput.scrollTop; });
1369
- queryInput.addEventListener('input', saveState);
1370
1350
  statsToggle.addEventListener('change', saveState);
1371
1351
  proofToggle.addEventListener('change', saveState);
1372
1352
 
@@ -1,4 +1,4 @@
1
- // Aggregation builtins that run a subquery and collect, count, sum, or select the best answers.
1
+ // Aggregation builtins that run a inner goal and collect, count, sum, or select the best answers.
2
2
  // Each handler clones the solver so the inner goal can enumerate independently of the outer goal.
3
3
  import { compareTerms, copyResolved, isDecimalInteger, lexicalValue, listFromItems, numberTerm, numberTextFromDouble, parseFiniteNumber, unify } from '../term.js';
4
4
 
@@ -14,7 +14,7 @@ export const aggregationBuiltins = {
14
14
 
15
15
  function* findall({ solver, goal, env }) {
16
16
  const [template, innerGoal, bag] = goal.args;
17
- const collector = solver.cloneForSubquery(10000000);
17
+ const collector = solver.cloneForInnerGoal(10000000);
18
18
  const collected = [];
19
19
  for (const answerEnv of collector.solve([innerGoal], env.clone(), 0)) collected.push(copyResolved(template, answerEnv));
20
20
  const next = env.clone();
@@ -23,7 +23,7 @@ function* findall({ solver, goal, env }) {
23
23
 
24
24
  function* countall({ solver, goal, env }) {
25
25
  const [innerGoal, count] = goal.args;
26
- const collector = solver.cloneForSubquery(10000000);
26
+ const collector = solver.cloneForInnerGoal(10000000);
27
27
  let n = 0;
28
28
  for (const _ of collector.solve([innerGoal], env.clone(), 0)) n++;
29
29
  const next = env.clone();
@@ -32,7 +32,7 @@ function* countall({ solver, goal, env }) {
32
32
 
33
33
  function* sumall({ solver, goal, env }) {
34
34
  const [template, innerGoal, sum] = goal.args;
35
- const collector = solver.cloneForSubquery(10000000);
35
+ const collector = solver.cloneForInnerGoal(10000000);
36
36
  let intSum = 0n;
37
37
  let floatMode = false;
38
38
  let floatSum = 0;
@@ -55,7 +55,7 @@ function* sumall({ solver, goal, env }) {
55
55
  function aggregateBest(wantMin) {
56
56
  return function* ({ solver, goal, env }) {
57
57
  const [keyTemplate, valueTemplate, innerGoal, bestKey, bestValue] = goal.args;
58
- const collector = solver.cloneForSubquery(10000000);
58
+ const collector = solver.cloneForInnerGoal(10000000);
59
59
  let has = false;
60
60
  let key = null;
61
61
  let value = null;
@@ -1,4 +1,4 @@
1
- // Control builtins. These intentionally use bounded sub-solvers so not/1 and once/1 only ask for the answers they need.
1
+ // Control builtins. These intentionally use bounded nested solvers so not/1 and once/1 only ask for the answers they need.
2
2
  export const controlBuiltins = {
3
3
  register(registry) {
4
4
  registry.add('not', 1, notBuiltin);
@@ -7,14 +7,14 @@ export const controlBuiltins = {
7
7
  };
8
8
 
9
9
  function* notBuiltin({ solver, goal, env }) {
10
- const limited = solver.cloneForSubquery(1);
10
+ const limited = solver.cloneForInnerGoal(1);
11
11
  let found = false;
12
12
  for (const _ of limited.solve([goal.args[0]], env.clone(), 0)) { found = true; break; }
13
13
  if (!found) yield env;
14
14
  }
15
15
 
16
16
  function* onceBuiltin({ solver, goal, env }) {
17
- const limited = solver.cloneForSubquery(1);
17
+ const limited = solver.cloneForInnerGoal(1);
18
18
  for (const answerEnv of limited.solve([goal.args[0]], env.clone(), 0)) {
19
19
  yield answerEnv;
20
20
  break;
@@ -44,3 +44,10 @@ export function createDefaultRegistry() {
44
44
  }
45
45
  return registry;
46
46
  }
47
+
48
+ let defaultRegistry = null;
49
+
50
+ export function getDefaultRegistry() {
51
+ if (defaultRegistry == null) defaultRegistry = createDefaultRegistry();
52
+ return defaultRegistry;
53
+ }
package/src/cli.js CHANGED
@@ -1,26 +1,21 @@
1
1
  // Command-line interface for eyelang.
2
- // It loads programs from files, URLs, or stdin, then either materializes derived output or evaluates an explicit query.
2
+ // It loads programs from files, URLs, or stdin, then materializes derived output predicates.
3
3
  import fs from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import process from 'node:process';
6
- import { Env, copyResolved, termIsGround, termToString } from './term.js';
7
- import { Program } from './program.js';
8
- import { Solver } from './solver.js';
9
- import { parseQueryGoal } from './parser.js';
10
- import { whyNoProof, whyProof } from './explain.js';
11
6
 
12
- const VERSION = await packageVersion();
7
+ let engineModule = null;
8
+ let explanationModule = null;
13
9
 
14
10
  export async function main(argv) {
15
11
  if (argv.length === 0) {
16
- usage(process.stdout);
12
+ await usage(process.stdout);
17
13
  return;
18
14
  }
19
15
 
20
16
  const options = {
21
17
  files: [],
22
18
  proof: false,
23
- query: null,
24
19
  stats: false,
25
20
  version: false,
26
21
  };
@@ -35,13 +30,10 @@ export async function main(argv) {
35
30
  } else if (!endOptions && (arg === '--version' || arg === '-v')) {
36
31
  options.version = true;
37
32
  } else if (!endOptions && (arg === '--help' || arg === '-h')) {
38
- usage(process.stdout);
33
+ await usage(process.stdout);
39
34
  return;
40
35
  } else if (!endOptions && (arg === '--proof' || arg === '-p')) {
41
36
  options.proof = true;
42
- } else if (!endOptions && arg === '--query') {
43
- if (i + 1 >= argv.length) throw new Error('--query requires an argument');
44
- options.query = argv[++i];
45
37
  } else if (!endOptions && arg === '--stats') {
46
38
  options.stats = true;
47
39
  } else if (!endOptions && arg.startsWith('-') && arg !== '-') {
@@ -52,7 +44,7 @@ export async function main(argv) {
52
44
  }
53
45
 
54
46
  if (options.version) {
55
- process.stdout.write(`eyelang ${VERSION}\n`);
47
+ process.stdout.write(`eyelang ${await packageVersion()}\n`);
56
48
  return;
57
49
  }
58
50
 
@@ -77,45 +69,52 @@ export async function main(argv) {
77
69
  }
78
70
  }
79
71
 
80
- const program = Program.parseSources(sourceParts, { sourceMetadata: options.proof, markRecursive: options.proof });
72
+ const engine = await loadEngine();
73
+ const program = engine.Program.parseSources(sourceParts, { sourceMetadata: options.proof, markRecursive: options.proof });
81
74
 
82
- if (options.query != null) runQuery(program, options.query, options);
83
- else runDefault(program, options);
75
+ await runDefault(engine, program, options);
84
76
  }
85
77
 
86
- function runQuery(program, query, options) {
87
- const goal = parseQueryGoal(query);
88
- const solver = new Solver(program);
89
-
90
- for (const env of solver.solve([goal], new Env(), 0)) {
91
- process.stdout.write(`${termToString(goal, env, true)}.\n`);
92
-
93
- if (options.proof) writeExplanation(program, copyResolved(goal, env));
78
+ async function loadEngine() {
79
+ if (engineModule == null) {
80
+ const [term, program, solver, registry] = await Promise.all([
81
+ import('./term.js'),
82
+ import('./program.js'),
83
+ import('./solver.js'),
84
+ import('./builtins/registry.js'),
85
+ ]);
86
+ engineModule = { ...term, ...program, ...solver, ...registry };
94
87
  }
88
+ return engineModule;
89
+ }
95
90
 
96
- if (options.stats) printStats(solver.stats);
91
+ async function loadExplanation() {
92
+ if (explanationModule == null) explanationModule = await import('./explain.js');
93
+ return explanationModule;
97
94
  }
98
95
 
99
- function runDefault(program, options) {
96
+ async function runDefault(engine, program, options) {
100
97
  const goals = program.materializationGoals();
101
98
  const materializedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
102
99
  const facts = program.sourceFactLines(materializedKeys);
103
100
  const lines = new Set();
104
101
  let lastStats = null;
102
+ const registry = engine.getDefaultRegistry();
103
+ const explanation = options.proof ? await loadExplanation() : null;
105
104
 
106
105
  for (const goal of goals) {
107
- const solver = new Solver(program);
106
+ const solver = new engine.Solver(program, { registry });
108
107
 
109
- for (const env of solver.solve([goal], new Env(), 0)) {
110
- if (!termIsGround(goal, env)) continue;
108
+ for (const env of solver.solve([goal], new engine.Env(), 0)) {
109
+ if (!engine.termIsGround(goal, env)) continue;
111
110
 
112
- const line = `${termToString(goal, env, true)}.\n`;
111
+ const line = `${engine.termToString(goal, env, true)}.\n`;
113
112
  if (facts.has(line) || lines.has(line)) continue;
114
113
 
115
114
  lines.add(line);
116
115
 
117
116
  process.stdout.write(line);
118
- if (options.proof) writeExplanation(program, copyResolved(goal, env));
117
+ if (options.proof) writeExplanation(explanation, program, engine.copyResolved(goal, env), registry);
119
118
  }
120
119
 
121
120
  lastStats = solver.stats;
@@ -124,14 +123,14 @@ function runDefault(program, options) {
124
123
  if (options.stats && lastStats) printStats(lastStats);
125
124
  }
126
125
 
127
- function writeExplanation(program, resolved) {
128
- const proof = whyProof(program, resolved);
126
+ function writeExplanation(explanation, program, resolved, registry) {
127
+ const proof = explanation.whyProof(program, resolved, { registry });
129
128
  process.stdout.write(proof.text);
130
- if (!proof.ok) process.stdout.write(whyNoProof(resolved));
129
+ if (!proof.ok) process.stdout.write(explanation.whyNoProof(resolved));
131
130
  }
132
131
 
133
- function usage(stream) {
134
- stream.write(`eyelang ${VERSION}
132
+ async function usage(stream) {
133
+ stream.write(`eyelang ${await packageVersion()}
135
134
 
136
135
  Usage:
137
136
  eyelang [options] [file-or-url.pl|- ...]
@@ -143,7 +142,6 @@ Input:
143
142
  Options:
144
143
  -h, --help Show this help text and exit.
145
144
  -p, --proof Enable proof explanations.
146
- --query GOAL Run GOAL as a query instead of materializing output predicates.
147
145
  --stats Print solver statistics to stderr after execution.
148
146
  -v, --version Show the package version and exit.
149
147
  -- Stop option parsing; following arguments are treated as files.