eyelang 1.4.1 → 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 (79) 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/cli.js +4 -26
  73. package/src/index.js +15 -24
  74. package/src/parser.js +3 -3
  75. package/src/solver.js +2 -2
  76. package/test/run-conformance.js +1 -3
  77. package/test/run-regression.js +38 -59
  78. package/conformance/cases/core/001_fact_query.pl +0 -2
  79. /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.1",
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;
package/src/cli.js CHANGED
@@ -1,5 +1,5 @@
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';
@@ -16,7 +16,6 @@ export async function main(argv) {
16
16
  const options = {
17
17
  files: [],
18
18
  proof: false,
19
- query: null,
20
19
  stats: false,
21
20
  version: false,
22
21
  };
@@ -35,9 +34,6 @@ export async function main(argv) {
35
34
  return;
36
35
  } else if (!endOptions && (arg === '--proof' || arg === '-p')) {
37
36
  options.proof = true;
38
- } else if (!endOptions && arg === '--query') {
39
- if (i + 1 >= argv.length) throw new Error('--query requires an argument');
40
- options.query = argv[++i];
41
37
  } else if (!endOptions && arg === '--stats') {
42
38
  options.stats = true;
43
39
  } else if (!endOptions && arg.startsWith('-') && arg !== '-') {
@@ -76,20 +72,18 @@ export async function main(argv) {
76
72
  const engine = await loadEngine();
77
73
  const program = engine.Program.parseSources(sourceParts, { sourceMetadata: options.proof, markRecursive: options.proof });
78
74
 
79
- if (options.query != null) await runQuery(engine, program, options.query, options);
80
- else await runDefault(engine, program, options);
75
+ await runDefault(engine, program, options);
81
76
  }
82
77
 
83
78
  async function loadEngine() {
84
79
  if (engineModule == null) {
85
- const [term, program, solver, parser, registry] = await Promise.all([
80
+ const [term, program, solver, registry] = await Promise.all([
86
81
  import('./term.js'),
87
82
  import('./program.js'),
88
83
  import('./solver.js'),
89
- import('./parser.js'),
90
84
  import('./builtins/registry.js'),
91
85
  ]);
92
- engineModule = { ...term, ...program, ...solver, ...parser, ...registry };
86
+ engineModule = { ...term, ...program, ...solver, ...registry };
93
87
  }
94
88
  return engineModule;
95
89
  }
@@ -99,21 +93,6 @@ async function loadExplanation() {
99
93
  return explanationModule;
100
94
  }
101
95
 
102
- async function runQuery(engine, program, query, options) {
103
- const goal = engine.parseQueryGoal(query);
104
- const registry = engine.getDefaultRegistry();
105
- const solver = new engine.Solver(program, { registry });
106
- const explanation = options.proof ? await loadExplanation() : null;
107
-
108
- for (const env of solver.solve([goal], new engine.Env(), 0)) {
109
- process.stdout.write(`${engine.termToString(goal, env, true)}.\n`);
110
-
111
- if (options.proof) writeExplanation(explanation, program, engine.copyResolved(goal, env), registry);
112
- }
113
-
114
- if (options.stats) printStats(solver.stats);
115
- }
116
-
117
96
  async function runDefault(engine, program, options) {
118
97
  const goals = program.materializationGoals();
119
98
  const materializedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
@@ -163,7 +142,6 @@ Input:
163
142
  Options:
164
143
  -h, --help Show this help text and exit.
165
144
  -p, --proof Enable proof explanations.
166
- --query GOAL Run GOAL as a query instead of materializing output predicates.
167
145
  --stats Print solver statistics to stderr after execution.
168
146
  -v, --version Show the package version and exit.
169
147
  -- Stop option parsing; following arguments are treated as files.
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Public JavaScript API surface for embedders and the browser playground.
2
2
  // The CLI imports the same parser, program, solver, and term primitives from here.
3
3
  export { Program, makeProgram } from './program.js';
4
- export { parseClauses, parseProgramText, parseQueryGoal } from './parser.js';
4
+ export { parseClauses, parseProgramText } from './parser.js';
5
5
  export { Solver } from './solver.js';
6
6
  export * from './term.js';
7
7
  export { BuiltinRegistry, createDefaultRegistry, getDefaultRegistry } from './builtins/registry.js';
@@ -9,7 +9,6 @@ export { BuiltinRegistry, createDefaultRegistry, getDefaultRegistry } from './bu
9
9
  import { Env, copyResolved, termIsGround, termToString } from './term.js';
10
10
  import { Program } from './program.js';
11
11
  import { Solver } from './solver.js';
12
- import { parseQueryGoal } from './parser.js';
13
12
  import { whyNoProof, whyProof } from './explain.js';
14
13
  import { getDefaultRegistry } from './builtins/registry.js';
15
14
 
@@ -20,28 +19,20 @@ export function run(source, options = {}) {
20
19
  const runOptions = options.registry ? options : { ...options, registry: getDefaultRegistry() };
21
20
  const solver = new Solver(program, runOptions);
22
21
  const output = [];
23
- if (options.query) {
24
- const goal = typeof options.query === 'string' ? parseQueryGoal(options.query) : options.query;
25
- for (const env of solver.solve([goal], new Env(), 0)) {
26
- output.push(`${termToString(goal, env, true)}.\n`);
27
- if (includeWhy) appendExplanation(output, program, copyResolved(goal, env), runOptions.registry);
28
- }
29
- } else {
30
- const goals = program.materializationGoals();
31
- const materializedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
32
- const facts = program.sourceFactLines(materializedKeys);
33
- const seen = new Set();
34
- for (const goal of goals) {
35
- const localSolver = new Solver(program, runOptions);
36
- for (const env of localSolver.solve([goal], new Env(), 0)) {
37
- const resolved = copyResolved(goal, env);
38
- if (!termIsGround(resolved)) continue;
39
- const line = `${termToString(resolved, new Env(), true)}.\n`;
40
- if (facts.has(line) || seen.has(line)) continue;
41
- seen.add(line);
42
- output.push(line);
43
- if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
44
- }
22
+ const goals = program.materializationGoals();
23
+ const materializedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
24
+ const facts = program.sourceFactLines(materializedKeys);
25
+ const seen = new Set();
26
+ for (const goal of goals) {
27
+ const localSolver = new Solver(program, runOptions);
28
+ for (const env of localSolver.solve([goal], new Env(), 0)) {
29
+ const resolved = copyResolved(goal, env);
30
+ if (!termIsGround(resolved)) continue;
31
+ const line = `${termToString(resolved, new Env(), true)}.\n`;
32
+ if (facts.has(line) || seen.has(line)) continue;
33
+ seen.add(line);
34
+ output.push(line);
35
+ if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
45
36
  }
46
37
  }
47
38
  return { stdout: output.join(''), stats: solver.stats };
package/src/parser.js CHANGED
@@ -399,9 +399,9 @@ export function parseProgramText(source) {
399
399
  return parseClauses(source);
400
400
  }
401
401
 
402
- export function parseQueryGoal(query) {
403
- const clauses = parseClauses(`q(${query}).`);
402
+ export function parseGoalText(text) {
403
+ const clauses = parseClauses(`zz_goal(${text}).`);
404
404
  const head = clauses[0]?.head;
405
- if (!head || head.args.length < 1) throw new Error('bad query');
405
+ if (!head || head.args.length < 1) throw new Error('bad goal');
406
406
  return head.args[0];
407
407
  }
package/src/solver.js CHANGED
@@ -31,7 +31,7 @@ export class Solver {
31
31
  };
32
32
  }
33
33
 
34
- cloneForSubquery(solutionLimit = this.solutionLimit) {
34
+ cloneForInnerGoal(solutionLimit = this.solutionLimit) {
35
35
  const solver = new Solver(this.program, { registry: this.registry, maxDepth: this.maxDepth, solutionLimit });
36
36
  solver.memo = this.memo;
37
37
  return solver;
@@ -114,7 +114,7 @@ export class Solver {
114
114
  }
115
115
  if (!entry.complete && !entry.computing) {
116
116
  entry.computing = true;
117
- const collector = this.cloneForSubquery();
117
+ const collector = this.cloneForInnerGoal();
118
118
  for (const answerEnv of collector.solveUserGoalUncached(group, goal, [], env.clone(), depth)) {
119
119
  entry.answers.push(goal.args.map((arg) => importResolved(arg, answerEnv)));
120
120
  }
@@ -37,12 +37,10 @@ function runProfile(reporter, profile) {
37
37
 
38
38
  function runCase(profile, name, file, casesDir, expectedDir) {
39
39
  const programFile = path.join(casesDir, file);
40
- const queryFile = path.join(casesDir, `${name}.query`);
41
40
  const expected = path.join(expectedDir, `${name}.out`);
42
41
  const text = fs.readFileSync(programFile, 'utf8');
43
42
  const program = Program.parseSources([{ text, filename: file }], { sourceMetadata: false, markRecursive: false });
44
- const options = fs.existsSync(queryFile) ? { query: fs.readFileSync(queryFile, 'utf8').trim() } : {};
45
- const actual = run(program, options).stdout;
43
+ const actual = run(program).stdout;
46
44
 
47
45
  if (!fs.existsSync(expected)) {
48
46
  throw new Error(`missing expected file: ${path.relative(root, expected)}`);