eyeling 1.34.2 → 1.34.4

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 (69) hide show
  1. package/README.md +7 -10
  2. package/docs/eyelang-guide.md +8 -30
  3. package/docs/eyelang-language-reference.md +32 -6
  4. package/examples/eyelang/basic-monadic.pl +16 -2
  5. package/examples/eyelang/bayes-therapy.pl +4 -4
  6. package/examples/eyelang/output/basic-monadic.pl +1314 -1314
  7. package/examples/eyelang/output/path-discovery.pl +3 -3
  8. package/examples/eyelang/output/reusable-builtins.pl +5 -0
  9. package/examples/eyelang/output/term-tools.pl +6 -0
  10. package/examples/eyelang/path-discovery.pl +22 -7
  11. package/examples/eyelang/reusable-builtins.pl +32 -0
  12. package/examples/eyelang/term-tools.pl +23 -0
  13. package/lib/eyelang/builtins/arithmetic.js +19 -6
  14. package/lib/eyelang/builtins/control.js +12 -0
  15. package/lib/eyelang/builtins/lists.js +146 -7
  16. package/lib/eyelang/builtins/registry.js +2 -4
  17. package/lib/eyelang/builtins/strings.js +165 -1
  18. package/lib/eyelang/builtins/terms.js +66 -0
  19. package/package.json +1 -1
  20. package/test/eyelang/conformance/README.md +1 -1
  21. package/test/eyelang/conformance/cases/extension/036_reusable_numeric_builtins.pl +10 -0
  22. package/test/eyelang/conformance/cases/extension/037_reusable_list_builtins.pl +11 -0
  23. package/test/eyelang/conformance/cases/extension/038_reusable_string_builtins.pl +12 -0
  24. package/test/eyelang/conformance/cases/extension/039_reusable_term_control_builtins.pl +11 -0
  25. package/test/eyelang/conformance/expected/extension/036_reusable_numeric_builtins.out +8 -0
  26. package/test/eyelang/conformance/expected/extension/037_reusable_list_builtins.out +9 -0
  27. package/test/eyelang/conformance/expected/extension/038_reusable_string_builtins.out +10 -0
  28. package/test/eyelang/conformance/expected/extension/039_reusable_term_control_builtins.out +6 -0
  29. package/examples/eyelang/collatz-1000.pl +0 -14
  30. package/examples/eyelang/complex-matrix-stability.pl +0 -45
  31. package/examples/eyelang/dense-hamiltonian-cycle.pl +0 -92
  32. package/examples/eyelang/gcd-bezout-identity.pl +0 -48
  33. package/examples/eyelang/goldbach-1000.pl +0 -185
  34. package/examples/eyelang/hamiltonian-cycle.pl +0 -55
  35. package/examples/eyelang/kaprekar.pl +0 -32
  36. package/examples/eyelang/matrix.pl +0 -296
  37. package/examples/eyelang/n-queens.pl +0 -23
  38. package/examples/eyelang/output/collatz-1000.pl +0 -1000
  39. package/examples/eyelang/output/complex-matrix-stability.pl +0 -5
  40. package/examples/eyelang/output/dense-hamiltonian-cycle.pl +0 -4
  41. package/examples/eyelang/output/gcd-bezout-identity.pl +0 -36
  42. package/examples/eyelang/output/goldbach-1000.pl +0 -667
  43. package/examples/eyelang/output/hamiltonian-cycle.pl +0 -4
  44. package/examples/eyelang/output/kaprekar.pl +0 -8
  45. package/examples/eyelang/output/matrix.pl +0 -10
  46. package/examples/eyelang/output/n-queens.pl +0 -93
  47. package/examples/eyelang/output/quine-mccluskey.pl +0 -3
  48. package/examples/eyelang/output/sat-dpll.pl +0 -5
  49. package/examples/eyelang/output/traveling-salesman.pl +0 -1
  50. package/examples/eyelang/quine-mccluskey.pl +0 -143
  51. package/examples/eyelang/sat-dpll.pl +0 -80
  52. package/examples/eyelang/traveling-salesman.pl +0 -64
  53. package/lib/eyelang/builtins/matrix.js +0 -226
  54. package/lib/eyelang/builtins/number-theory.js +0 -114
  55. package/lib/eyelang/builtins/search.js +0 -519
  56. package/test/eyelang/conformance/cases/extension/036_extended_gcd.pl +0 -3
  57. package/test/eyelang/conformance/cases/extension/037_collatz_trajectory.pl +0 -3
  58. package/test/eyelang/conformance/cases/extension/038_kaprekar_steps.pl +0 -3
  59. package/test/eyelang/conformance/cases/extension/039_goldbach_pair.pl +0 -3
  60. package/test/eyelang/conformance/cases/extension/040_matrix_operations.pl +0 -5
  61. package/test/eyelang/conformance/cases/extension/042_n_queens_small.pl +0 -3
  62. package/test/eyelang/conformance/cases/extension/043_cnf_model.pl +0 -3
  63. package/test/eyelang/conformance/expected/extension/036_extended_gcd.out +0 -1
  64. package/test/eyelang/conformance/expected/extension/037_collatz_trajectory.out +0 -1
  65. package/test/eyelang/conformance/expected/extension/038_kaprekar_steps.out +0 -1
  66. package/test/eyelang/conformance/expected/extension/039_goldbach_pair.out +0 -2
  67. package/test/eyelang/conformance/expected/extension/040_matrix_operations.out +0 -3
  68. package/test/eyelang/conformance/expected/extension/042_n_queens_small.out +0 -2
  69. package/test/eyelang/conformance/expected/extension/043_cnf_model.out +0 -1
@@ -1,3 +1,3 @@
1
- airroute(discovered, "Ostend-Bruges International Airport -> Liège Airport -> Heraklion International Nikos Kazantzakis Airport -> Václav Havel Airport Prague").
2
- airroute(discovered, "Ostend-Bruges International Airport -> Liège Airport -> Diagoras Airport -> Václav Havel Airport Prague").
3
- airroute(discovered, "Ostend-Bruges International Airport -> Liège Airport -> Palma De Mallorca Airport -> Václav Havel Airport Prague").
1
+ airroute("Ostend-Bruges International Airport", "Václav Havel Airport Prague", 2, "Ostend-Bruges International Airport -> Liège Airport -> Heraklion International Nikos Kazantzakis Airport -> Václav Havel Airport Prague").
2
+ airroute("Ostend-Bruges International Airport", "Václav Havel Airport Prague", 2, "Ostend-Bruges International Airport -> Liège Airport -> Diagoras Airport -> Václav Havel Airport Prague").
3
+ airroute("Ostend-Bruges International Airport", "Václav Havel Airport Prague", 2, "Ostend-Bruges International Airport -> Liège Airport -> Palma De Mallorca Airport -> Václav Havel Airport Prague").
@@ -0,0 +1,5 @@
1
+ report(normalized_name, "ada lovelace").
2
+ report(unique_tags, ["logic", "math", "programming"]).
3
+ report(tag_label, "logic / math / programming").
4
+ report(score_summary, summary(42, 21, 6.4807406984078604)).
5
+ report(window, [13, 21]).
@@ -0,0 +1,6 @@
1
+ report(shape, shape(edge, 3)).
2
+ report(second_argument, b).
3
+ report(parts, parts(edge, [a, b, 3])).
4
+ report(rebuilt, edge(c, d, 5)).
5
+ report(rendered, "edge(a, [b, c])").
6
+ report(all_weights_positive, yes).
@@ -1,20 +1,35 @@
1
1
  % Generic path discovery over the air-routes graph.
2
- % Change or add route_request(FromLabel, ToLabel, MaxStopOvers) to answer other routes.
2
+ % Add route_request(FromLabel, ToLabel, MaxStopOvers) facts to answer other routes.
3
+ % MaxStopOvers is converted to a leg limit, and the recursive search keeps a
4
+ % visited list so it works without the removed finite-search helper builtins.
5
+
3
6
  % Output declarations: materialize/2 selects the relations written to this example's golden output.
4
- materialize(airroute, 2).
7
+ materialize(airroute, 4).
5
8
 
6
9
  % Program structure: facts set up the scenario, and rules derive the materialized conclusions.
7
10
  route_request("Ostend-Bruges International Airport", "Václav Havel Airport Prague", 2).
8
11
 
9
12
  % Derivation rules: each rule below contributes one logical step toward the displayed results.
10
- airroute(discovered, RouteText) :-
11
- route_request(From, To, MaxStopOvers),
12
- airport(Source, From),
13
- airport(Destination, To),
13
+ airroute(FromLabel, ToLabel, MaxStopOvers, RouteText) :-
14
+ route_request(FromLabel, ToLabel, MaxStopOvers),
15
+ route_between(FromLabel, ToLabel, MaxStopOvers, RouteText).
16
+
17
+ route_between(FromLabel, ToLabel, MaxStopOvers, RouteText) :-
18
+ airport(Source, FromLabel),
19
+ airport(Destination, ToLabel),
14
20
  add(MaxStopOvers, 1, MaxLegs),
15
- bounded_path(flight, Source, Destination, MaxLegs, Path),
21
+ simple_path(Source, Destination, MaxLegs, [Source], ReversePath),
22
+ reverse(ReversePath, Path),
16
23
  route_text(Path, RouteText).
17
24
 
25
+ simple_path(Node, Node, _RemainingLegs, Visited, Visited).
26
+ simple_path(Node, Goal, RemainingLegs, Visited, Path) :-
27
+ gt(RemainingLegs, 0),
28
+ flight(Node, Next),
29
+ not_member(Next, Visited),
30
+ sub(RemainingLegs, 1, NextRemainingLegs),
31
+ simple_path(Next, Goal, NextRemainingLegs, [Next|Visited], Path).
32
+
18
33
  route_text([Node], Text) :-
19
34
  airport(Node, Text).
20
35
  route_text([Node|Rest], Text) :-
@@ -0,0 +1,32 @@
1
+ % Reusable builtin tour: normalize text, summarize lists, and compute numeric values.
2
+ materialize(report, 2).
3
+
4
+ name_raw(" Ada Lovelace ").
5
+ tag_csv("logic,math,logic,programming").
6
+ scores([8, 13, 21]).
7
+
8
+ report(normalized_name, Name) :-
9
+ name_raw(Raw),
10
+ trim(Raw, Trimmed),
11
+ lowercase(Trimmed, Name).
12
+
13
+ report(unique_tags, Tags) :-
14
+ tag_csv(Csv),
15
+ split(Csv, ",", Parts),
16
+ list_to_set(Parts, Tags).
17
+
18
+ report(tag_label, Label) :-
19
+ tag_csv(Csv),
20
+ split(Csv, ",", Parts),
21
+ list_to_set(Parts, Tags),
22
+ join(Tags, " / ", Label).
23
+
24
+ report(score_summary, summary(Total, Peak, RootTotal)) :-
25
+ scores(Scores),
26
+ sum_list(Scores, Total),
27
+ max_list(Scores, Peak),
28
+ sqrt(Total, RootTotal).
29
+
30
+ report(window, Slice) :-
31
+ scores(Scores),
32
+ slice(1, 2, Scores, Slice).
@@ -0,0 +1,23 @@
1
+ % Term tools: inspect and build structured terms, then validate all facts with forall/2.
2
+ materialize(report, 2).
3
+
4
+ edge(a, b, 3).
5
+ edge(b, c, 4).
6
+
7
+ report(shape, shape(Name, Arity)) :-
8
+ functor(edge(a, b, 3), Name, Arity).
9
+
10
+ report(second_argument, Node) :-
11
+ arg(2, edge(a, b, 3), Node).
12
+
13
+ report(parts, parts(Name, Args)) :-
14
+ compound_name_arguments(edge(a, b, 3), Name, Args).
15
+
16
+ report(rebuilt, Term) :-
17
+ compound_name_arguments(Term, edge, [c, d, 5]).
18
+
19
+ report(rendered, Text) :-
20
+ term_string(edge(a, [b, c]), Text).
21
+
22
+ report(all_weights_positive, yes) :-
23
+ forall(edge(_From, _To, Weight), gt(Weight, 0)).
@@ -2,8 +2,8 @@
2
2
  // The code keeps BigInt paths where possible so large eyelang integers remain exact.
3
3
  import { compareIntegerText, deref, isDecimalInteger, lexicalValue, numberTerm, numberTextFromDouble, parseFiniteNumber, unify } from '../term.js';
4
4
 
5
- const unaryNames = ['neg', 'abs', 'sin', 'cos', 'asin', 'acos', 'rounded', 'log'];
6
- const binaryNames = ['add', 'sub', 'mul', 'div', 'mod', 'min', 'pow'];
5
+ const unaryNames = ['neg', 'abs', 'sin', 'cos', 'tan', 'asin', 'acos', 'sqrt', 'floor', 'ceiling', 'trunc', 'rounded', 'exp', 'log'];
6
+ const binaryNames = ['add', 'sub', 'mul', 'div', 'mod', 'min', 'max', 'pow', 'atan2'];
7
7
  const compareNames = ['lt', 'gt', 'le', 'ge'];
8
8
 
9
9
  export const arithmeticBuiltins = {
@@ -22,9 +22,11 @@ function unary(name) {
22
22
  const text = lexicalValue(goal.args[0], env);
23
23
  if (text == null) return;
24
24
  let out = null;
25
- if ((name === 'neg' || name === 'abs') && isDecimalInteger(text)) {
25
+ if ((name === 'neg' || name === 'abs' || name === 'floor' || name === 'ceiling' || name === 'trunc' || name === 'rounded') && isDecimalInteger(text)) {
26
26
  const value = BigInt(text);
27
- out = (name === 'neg' ? -value : value < 0n ? -value : value).toString();
27
+ if (name === 'neg') out = (-value).toString();
28
+ else if (name === 'abs') out = (value < 0n ? -value : value).toString();
29
+ else out = value.toString();
28
30
  } else {
29
31
  const input = parseFiniteNumber(text);
30
32
  if (input == null) return;
@@ -33,14 +35,22 @@ function unary(name) {
33
35
  else if (name === 'abs') value = Math.abs(input);
34
36
  else if (name === 'sin') value = Math.sin(input);
35
37
  else if (name === 'cos') value = Math.cos(input);
38
+ else if (name === 'tan') value = Math.tan(input);
36
39
  else if (name === 'asin') value = Math.asin(input);
37
40
  else if (name === 'acos') value = Math.acos(input);
41
+ else if (name === 'sqrt') { if (input < 0) return; value = Math.sqrt(input); }
42
+ else if (name === 'floor') value = Math.floor(input);
43
+ else if (name === 'ceiling') value = Math.ceil(input);
44
+ else if (name === 'trunc') value = Math.trunc(input);
38
45
  else if (name === 'rounded') value = Math.round(input);
46
+ else if (name === 'exp') value = Math.exp(input);
39
47
  else if (name === 'log') {
40
48
  if (input <= 0) return;
41
49
  value = logCompat(input);
42
50
  }
43
- out = name === 'rounded' ? String(Math.trunc(value)) : numberTextFromDouble(value);
51
+ out = (name === 'floor' || name === 'ceiling' || name === 'trunc' || name === 'rounded')
52
+ ? String(Math.trunc(value))
53
+ : numberTextFromDouble(value);
44
54
  }
45
55
  const next = env.clone();
46
56
  if (out != null && unify(goal.args[1], numberTerm(out), next)) yield next;
@@ -53,7 +63,7 @@ function binary(name) {
53
63
  const rightText = lexicalValue(goal.args[1], env);
54
64
  if (leftText == null || rightText == null) return;
55
65
  let out = null;
56
- if (isDecimalInteger(leftText) && isDecimalInteger(rightText) && name !== 'mod') {
66
+ if (isDecimalInteger(leftText) && isDecimalInteger(rightText) && name !== 'mod' && name !== 'atan2') {
57
67
  const a = BigInt(leftText);
58
68
  const b = BigInt(rightText);
59
69
  if (name === 'add') out = (a + b).toString();
@@ -61,6 +71,7 @@ function binary(name) {
61
71
  else if (name === 'mul') out = (a * b).toString();
62
72
  else if (name === 'div') { if (b === 0n) return; out = (a / b).toString(); }
63
73
  else if (name === 'min') out = (a <= b ? a : b).toString();
74
+ else if (name === 'max') out = (a >= b ? a : b).toString();
64
75
  else if (name === 'pow') { if (b < 0n) return; out = (a ** b).toString(); }
65
76
  } else if (name === 'mod') {
66
77
  if (!isDecimalInteger(leftText) || !isDecimalInteger(rightText)) return;
@@ -77,6 +88,8 @@ function binary(name) {
77
88
  else if (name === 'div') { if (b === 0) return; value = a / b; }
78
89
  else if (name === 'pow') value = Math.pow(a, b);
79
90
  else if (name === 'min') value = Math.min(a, b);
91
+ else if (name === 'max') value = Math.max(a, b);
92
+ else if (name === 'atan2') value = Math.atan2(a, b);
80
93
  out = numberTextFromDouble(value);
81
94
  }
82
95
  const next = env.clone();
@@ -3,6 +3,7 @@ export const controlBuiltins = {
3
3
  register(registry) {
4
4
  registry.add('not', 1, notBuiltin);
5
5
  registry.add('once', 1, onceBuiltin);
6
+ registry.add('forall', 2, forallBuiltin);
6
7
  }
7
8
  };
8
9
 
@@ -20,3 +21,14 @@ function* onceBuiltin({ solver, goal, env }) {
20
21
  break;
21
22
  }
22
23
  }
24
+
25
+ function* forallBuiltin({ solver, goal, env }) {
26
+ const generator = solver.cloneForInnerGoal(10000000);
27
+ for (const answerEnv of generator.solve([goal.args[0]], env.clone(), 0)) {
28
+ const checker = solver.cloneForInnerGoal(1);
29
+ let ok = false;
30
+ for (const _ of checker.solve([goal.args[1]], answerEnv.clone(), 0)) { ok = true; break; }
31
+ if (!ok) return;
32
+ }
33
+ yield env;
34
+ }
@@ -1,23 +1,62 @@
1
- // List builtins for proper lists, selection, membership, sorting, and indexing.
1
+ // List builtins for proper lists, selection, membership, sorting, indexing, slicing, and summaries.
2
2
  // Several predicates support both checking and generation, so the argument modes are handled explicitly.
3
- import { compareTerms, copyResolved, deref, isCons, lexicalValue, listFromItems, numberTerm, properListItems, unify } from '../term.js';
3
+ import {
4
+ compareTerms,
5
+ copyResolved,
6
+ deref,
7
+ isDecimalInteger,
8
+ isCons,
9
+ lexicalValue,
10
+ listFromItems,
11
+ numberTerm,
12
+ numberTextFromDouble,
13
+ parseFiniteNumber,
14
+ properListItems,
15
+ unify,
16
+ } from '../term.js';
4
17
 
5
18
  export const listBuiltins = {
6
19
  register(registry) {
7
20
  registry.add('append', 3, append);
8
21
  registry.add('nth0', 3, nth0);
9
22
  registry.add('set_nth0', 4, setNth0, { deterministic: true });
10
- registry.add('rest', 2, rest, { deterministic: true });
23
+ registry.add('head', 2, head, { deterministic: true, fallbackWhenNotReady: true, ready: firstConsReady });
24
+ registry.add('rest', 2, rest, { deterministic: true, fallbackWhenNotReady: true, ready: firstConsReady });
25
+ registry.add('last', 2, last, { deterministic: true, fallbackWhenNotReady: true, ready: firstProperListReady });
26
+ registry.add('take', 3, take, { deterministic: true, fallbackWhenNotReady: true, ready: countAndListReady });
27
+ registry.add('drop', 3, drop, { deterministic: true, fallbackWhenNotReady: true, ready: countAndListReady });
28
+ registry.add('slice', 4, slice, { deterministic: true, fallbackWhenNotReady: true, ready: sliceReady });
11
29
  registry.add('member', 2, member);
12
30
  registry.add('select', 3, select);
13
31
  registry.add('not_member', 2, notMember, { deterministic: true });
14
32
  registry.add('reverse', 2, reverse, { deterministic: true });
15
33
  registry.add('length', 2, lengthBuiltin, { deterministic: true });
34
+ registry.add('sum_list', 2, sumList, { deterministic: true, fallbackWhenNotReady: true, ready: firstProperListReady });
35
+ registry.add('min_list', 2, minList, { deterministic: true, fallbackWhenNotReady: true, ready: firstProperListReady });
36
+ registry.add('max_list', 2, maxList, { deterministic: true, fallbackWhenNotReady: true, ready: firstProperListReady });
37
+ registry.add('list_to_set', 2, listToSet, { deterministic: true, fallbackWhenNotReady: true, ready: firstProperListReady });
16
38
  registry.add('sort', 2, sortBuiltin, { deterministic: true });
17
39
  }
18
40
  };
19
41
 
20
42
 
43
+
44
+ function firstConsReady(goal, env) {
45
+ return isCons(deref(goal.args[0], env));
46
+ }
47
+
48
+ function firstProperListReady(goal, env) {
49
+ return properListItems(goal.args[0], env) !== null;
50
+ }
51
+
52
+ function countAndListReady(goal, env) {
53
+ return safeIndex(goal.args[0], env) !== null && properListItems(goal.args[1], env) !== null;
54
+ }
55
+
56
+ function sliceReady(goal, env) {
57
+ return safeIndex(goal.args[0], env) !== null && safeIndex(goal.args[1], env) !== null && properListItems(goal.args[2], env) !== null;
58
+ }
59
+
21
60
  function listFromItemsExcept(items, skip) {
22
61
  const copy = new Array(items.length - 1);
23
62
  for (let i = 0, j = 0; i < items.length; i++) if (i !== skip) copy[j++] = items[i];
@@ -62,10 +101,8 @@ function* nth0({ goal, env }) {
62
101
  }
63
102
 
64
103
  function* setNth0({ goal, env }) {
65
- const indexText = lexicalValue(goal.args[0], env);
66
- if (!/^-?\d+$/.test(indexText ?? '')) return;
67
- const index = Number(indexText);
68
- if (!Number.isSafeInteger(index) || index < 0) return;
104
+ const index = safeIndex(goal.args[0], env);
105
+ if (index == null) return;
69
106
  const items = properListItems(goal.args[1], env);
70
107
  if (!items || index >= items.length) return;
71
108
  const out = items.slice();
@@ -74,6 +111,13 @@ function* setNth0({ goal, env }) {
74
111
  if (unify(goal.args[3], listFromItems(out), next)) yield next;
75
112
  }
76
113
 
114
+ function* head({ goal, env }) {
115
+ const list = deref(goal.args[0], env);
116
+ if (!isCons(list)) return;
117
+ const next = env.clone();
118
+ if (unify(goal.args[1], list.args[0], next)) yield next;
119
+ }
120
+
77
121
  function* rest({ goal, env }) {
78
122
  const list = deref(goal.args[0], env);
79
123
  if (!isCons(list)) return;
@@ -81,6 +125,41 @@ function* rest({ goal, env }) {
81
125
  if (unify(goal.args[1], list.args[1], next)) yield next;
82
126
  }
83
127
 
128
+ function* last({ goal, env }) {
129
+ const items = properListItems(goal.args[0], env);
130
+ if (!items || items.length === 0) return;
131
+ const next = env.clone();
132
+ if (unify(goal.args[1], items[items.length - 1], next)) yield next;
133
+ }
134
+
135
+ function* take({ goal, env }) {
136
+ const count = safeIndex(goal.args[0], env);
137
+ if (count == null) return;
138
+ const items = properListItems(goal.args[1], env);
139
+ if (!items || count > items.length) return;
140
+ const next = env.clone();
141
+ if (unify(goal.args[2], listFromItems(items, 0, count), next)) yield next;
142
+ }
143
+
144
+ function* drop({ goal, env }) {
145
+ const count = safeIndex(goal.args[0], env);
146
+ if (count == null) return;
147
+ const items = properListItems(goal.args[1], env);
148
+ if (!items || count > items.length) return;
149
+ const next = env.clone();
150
+ if (unify(goal.args[2], listFromItems(items, count, items.length), next)) yield next;
151
+ }
152
+
153
+ function* slice({ goal, env }) {
154
+ const start = safeIndex(goal.args[0], env);
155
+ const count = safeIndex(goal.args[1], env);
156
+ if (start == null || count == null) return;
157
+ const items = properListItems(goal.args[2], env);
158
+ if (!items || start + count > items.length) return;
159
+ const next = env.clone();
160
+ if (unify(goal.args[3], listFromItems(items, start, start + count), next)) yield next;
161
+ }
162
+
84
163
  function* member({ goal, env }) {
85
164
  const items = properListItems(goal.args[1], env);
86
165
  if (!items) return;
@@ -132,6 +211,59 @@ function* lengthBuiltin({ goal, env }) {
132
211
  if (unify(goal.args[1], numberTerm(items.length), next)) yield next;
133
212
  }
134
213
 
214
+ function* sumList({ goal, env }) {
215
+ const items = properListItems(goal.args[0], env);
216
+ if (!items) return;
217
+ let intSum = 0n;
218
+ let floatMode = false;
219
+ let floatSum = 0;
220
+ for (const item of items) {
221
+ const text = lexicalValue(item, env);
222
+ if (text == null) return;
223
+ if (!floatMode && isDecimalInteger(text)) intSum += BigInt(text);
224
+ else {
225
+ const value = parseFiniteNumber(text);
226
+ if (value == null) return;
227
+ if (!floatMode) { floatSum = Number(intSum); floatMode = true; }
228
+ floatSum += value;
229
+ }
230
+ }
231
+ const out = floatMode ? numberTextFromDouble(floatSum) : intSum.toString();
232
+ const next = env.clone();
233
+ if (unify(goal.args[1], numberTerm(out), next)) yield next;
234
+ }
235
+
236
+ function* minList({ goal, env }) {
237
+ yield* minMaxList(goal, env, true);
238
+ }
239
+
240
+ function* maxList({ goal, env }) {
241
+ yield* minMaxList(goal, env, false);
242
+ }
243
+
244
+ function* minMaxList(goal, env, wantMin) {
245
+ const items = properListItems(goal.args[0], env);
246
+ if (!items || items.length === 0) return;
247
+ let best = copyResolved(items[0], env);
248
+ for (let i = 1; i < items.length; i++) {
249
+ const item = copyResolved(items[i], env);
250
+ const cmp = compareTerms(item, best);
251
+ if ((wantMin && cmp < 0) || (!wantMin && cmp > 0)) best = item;
252
+ }
253
+ const next = env.clone();
254
+ if (unify(goal.args[1], best, next)) yield next;
255
+ }
256
+
257
+ function* listToSet({ goal, env }) {
258
+ const items = properListItems(goal.args[0], env);
259
+ if (!items) return;
260
+ const unique = [];
261
+ for (const item of items.map((entry) => copyResolved(entry, env))) {
262
+ if (!unique.some((seen) => compareTerms(seen, item) === 0)) unique.push(item);
263
+ }
264
+ const next = env.clone();
265
+ if (unify(goal.args[1], listFromItems(unique), next)) yield next;
266
+ }
135
267
 
136
268
  function* sortBuiltin({ goal, env }) {
137
269
  const items = properListItems(goal.args[0], env);
@@ -142,3 +274,10 @@ function* sortBuiltin({ goal, env }) {
142
274
  const next = env.clone();
143
275
  if (unify(goal.args[1], listFromItems(unique), next)) yield next;
144
276
  }
277
+
278
+ function safeIndex(term, env) {
279
+ const text = lexicalValue(term, env);
280
+ if (!/^-?\d+$/.test(text ?? '')) return null;
281
+ const index = Number(text);
282
+ return Number.isSafeInteger(index) && index >= 0 ? index : null;
283
+ }
@@ -7,9 +7,7 @@ import { listBuiltins } from './lists.js';
7
7
  import { aggregationBuiltins } from './aggregation.js';
8
8
  import { contextBuiltins } from './context.js';
9
9
  import { controlBuiltins } from './control.js';
10
- import { searchBuiltins } from './search.js';
11
- import { numberTheoryBuiltins } from './number-theory.js';
12
- import { matrixBuiltins } from './matrix.js';
10
+ import { termBuiltins } from './terms.js';
13
11
 
14
12
  export class BuiltinRegistry {
15
13
  constructor() {
@@ -36,7 +34,7 @@ export class BuiltinRegistry {
36
34
 
37
35
  export function createDefaultRegistry() {
38
36
  const registry = new BuiltinRegistry();
39
- for (const mod of [coreBuiltins, arithmeticBuiltins, stringBuiltins, listBuiltins, aggregationBuiltins, contextBuiltins, controlBuiltins, searchBuiltins, numberTheoryBuiltins, matrixBuiltins]) {
37
+ for (const mod of [coreBuiltins, arithmeticBuiltins, stringBuiltins, listBuiltins, aggregationBuiltins, contextBuiltins, controlBuiltins, termBuiltins]) {
40
38
  mod.register(registry);
41
39
  }
42
40
  return registry;
@@ -1,16 +1,76 @@
1
1
  // String builtins.
2
2
  // They mostly project from already-ground terms to avoid guessing string domains.
3
- import { compound, lexicalValue, stringTerm, unify } from '../term.js';
3
+ import {
4
+ atom,
5
+ compound,
6
+ deref,
7
+ isDecimalInteger,
8
+ lexicalValue,
9
+ listFromItems,
10
+ numberTerm,
11
+ parseFiniteNumber,
12
+ properListItems,
13
+ stringTerm,
14
+ termToString,
15
+ unify,
16
+ } from '../term.js';
4
17
 
5
18
  export const stringBuiltins = {
6
19
  register(registry) {
7
20
  registry.add('str_concat', 3, concat, { deterministic: true });
8
21
  for (const name of ['contains', 'matches', 'not_matches']) registry.add(name, 2, contains(name), { deterministic: true });
9
22
  registry.add('matches', 3, matchCaptures, { deterministic: true });
23
+ registry.add('split', 3, split, { deterministic: true, fallbackWhenNotReady: true, ready: firstTwoLexicalReady });
24
+ registry.add('join', 3, join, { deterministic: true, fallbackWhenNotReady: true, ready: listAndSecondLexicalReady });
25
+ registry.add('substring', 4, substring, { deterministic: true, fallbackWhenNotReady: true, ready: substringReady });
26
+ registry.add('replace', 4, replace, { deterministic: true, fallbackWhenNotReady: true, ready: firstThreeLexicalReady });
27
+ registry.add('lowercase', 2, caseFold('lower'), { deterministic: true, fallbackWhenNotReady: true, ready: firstLexicalReady });
28
+ registry.add('uppercase', 2, caseFold('upper'), { deterministic: true, fallbackWhenNotReady: true, ready: firstLexicalReady });
29
+ registry.add('trim', 2, trim, { deterministic: true, fallbackWhenNotReady: true, ready: firstLexicalReady });
30
+ registry.add('number_string', 2, numberString, { deterministic: true, fallbackWhenNotReady: true, ready: numberStringReady });
31
+ registry.add('atom_string', 2, atomString, { deterministic: true, fallbackWhenNotReady: true, ready: atomStringReady });
32
+ registry.add('term_string', 2, termString, { deterministic: true, fallbackWhenNotReady: true, ready: firstNonvarReady });
10
33
  }
11
34
  };
12
35
 
13
36
 
37
+
38
+ function firstLexicalReady(goal, env) {
39
+ return lexicalValue(goal.args[0], env) !== null;
40
+ }
41
+
42
+ function firstTwoLexicalReady(goal, env) {
43
+ return lexicalValue(goal.args[0], env) !== null && lexicalValue(goal.args[1], env) !== null;
44
+ }
45
+
46
+ function firstThreeLexicalReady(goal, env) {
47
+ return firstTwoLexicalReady(goal, env) && lexicalValue(goal.args[2], env) !== null;
48
+ }
49
+
50
+ function listAndSecondLexicalReady(goal, env) {
51
+ return properListItems(goal.args[0], env) !== null && lexicalValue(goal.args[1], env) !== null;
52
+ }
53
+
54
+ function substringReady(goal, env) {
55
+ return lexicalValue(goal.args[0], env) !== null && safeIndex(goal.args[1], env) !== null && safeIndex(goal.args[2], env) !== null;
56
+ }
57
+
58
+ function numberStringReady(goal, env) {
59
+ const left = deref(goal.args[0], env);
60
+ const right = deref(goal.args[1], env);
61
+ return left.type === 'number' || right.type === 'string' || right.type === 'atom';
62
+ }
63
+
64
+ function atomStringReady(goal, env) {
65
+ const left = deref(goal.args[0], env);
66
+ const right = deref(goal.args[1], env);
67
+ return left.type === 'atom' || right.type === 'string' || right.type === 'atom' || right.type === 'number';
68
+ }
69
+
70
+ function firstNonvarReady(goal, env) {
71
+ return deref(goal.args[0], env).type !== 'var';
72
+ }
73
+
14
74
  function* concat({ goal, env }) {
15
75
  const left = lexicalValue(goal.args[0], env);
16
76
  const right = lexicalValue(goal.args[1], env);
@@ -53,6 +113,99 @@ function* matchCaptures({ goal, env }) {
53
113
  if (unify(goal.args[2], context, next)) yield next;
54
114
  }
55
115
 
116
+ function* split({ goal, env }) {
117
+ const text = lexicalValue(goal.args[0], env);
118
+ const separator = lexicalValue(goal.args[1], env);
119
+ if (text == null || separator == null) return;
120
+ const parts = text.split(separator).map(stringTerm);
121
+ const next = env.clone();
122
+ if (unify(goal.args[2], listFromItems(parts), next)) yield next;
123
+ }
124
+
125
+ function* join({ goal, env }) {
126
+ const items = properListItems(goal.args[0], env);
127
+ const separator = lexicalValue(goal.args[1], env);
128
+ if (!items || separator == null) return;
129
+ const strings = [];
130
+ for (const item of items) {
131
+ const value = lexicalValue(item, env);
132
+ if (value == null) return;
133
+ strings.push(value);
134
+ }
135
+ const next = env.clone();
136
+ if (unify(goal.args[2], stringTerm(strings.join(separator)), next)) yield next;
137
+ }
138
+
139
+ function* substring({ goal, env }) {
140
+ const text = lexicalValue(goal.args[0], env);
141
+ const start = safeIndex(goal.args[1], env);
142
+ const count = safeIndex(goal.args[2], env);
143
+ if (text == null || start == null || count == null || start + count > text.length) return;
144
+ const next = env.clone();
145
+ if (unify(goal.args[3], stringTerm(text.slice(start, start + count)), next)) yield next;
146
+ }
147
+
148
+ function* replace({ goal, env }) {
149
+ const text = lexicalValue(goal.args[0], env);
150
+ const search = lexicalValue(goal.args[1], env);
151
+ const replacement = lexicalValue(goal.args[2], env);
152
+ if (text == null || search == null || replacement == null) return;
153
+ const out = search === '' ? text : text.split(search).join(replacement);
154
+ const next = env.clone();
155
+ if (unify(goal.args[3], stringTerm(out), next)) yield next;
156
+ }
157
+
158
+ function caseFold(direction) {
159
+ return function* ({ goal, env }) {
160
+ const text = lexicalValue(goal.args[0], env);
161
+ if (text == null) return;
162
+ const next = env.clone();
163
+ const out = direction === 'lower' ? text.toLowerCase() : text.toUpperCase();
164
+ if (unify(goal.args[1], stringTerm(out), next)) yield next;
165
+ };
166
+ }
167
+
168
+ function* trim({ goal, env }) {
169
+ const text = lexicalValue(goal.args[0], env);
170
+ if (text == null) return;
171
+ const next = env.clone();
172
+ if (unify(goal.args[1], stringTerm(text.trim()), next)) yield next;
173
+ }
174
+
175
+ function* numberString({ goal, env }) {
176
+ const left = deref(goal.args[0], env);
177
+ const right = deref(goal.args[1], env);
178
+ const next = env.clone();
179
+ if (left.type === 'number') {
180
+ if (unify(goal.args[1], stringTerm(left.name), next)) yield next;
181
+ return;
182
+ }
183
+ if (right.type === 'string' || right.type === 'atom') {
184
+ if (!numericText(right.name)) return;
185
+ if (unify(goal.args[0], numberTerm(right.name), next)) yield next;
186
+ }
187
+ }
188
+
189
+ function* atomString({ goal, env }) {
190
+ const left = deref(goal.args[0], env);
191
+ const right = deref(goal.args[1], env);
192
+ const next = env.clone();
193
+ if (left.type === 'atom') {
194
+ if (unify(goal.args[1], stringTerm(left.name), next)) yield next;
195
+ return;
196
+ }
197
+ if (right.type === 'string' || right.type === 'atom' || right.type === 'number') {
198
+ if (unify(goal.args[0], atom(right.name), next)) yield next;
199
+ }
200
+ }
201
+
202
+ function* termString({ goal, env }) {
203
+ const term = deref(goal.args[0], env);
204
+ if (term.type === 'var') return;
205
+ const next = env.clone();
206
+ if (unify(goal.args[1], stringTerm(termToString(term, env, true)), next)) yield next;
207
+ }
208
+
56
209
  function contextFromGroups(groups) {
57
210
  const terms = [];
58
211
  for (const [name, value] of Object.entries(groups)) {
@@ -68,3 +221,14 @@ function contextFromGroups(groups) {
68
221
  function simpleAlternationMatch(haystack, pattern) {
69
222
  return pattern.split('|').some((part) => part === '' || haystack.includes(part));
70
223
  }
224
+
225
+ function safeIndex(term, env) {
226
+ const text = lexicalValue(term, env);
227
+ if (!/^-?\d+$/.test(text ?? '')) return null;
228
+ const index = Number(text);
229
+ return Number.isSafeInteger(index) && index >= 0 ? index : null;
230
+ }
231
+
232
+ function numericText(text) {
233
+ return isDecimalInteger(text) || parseFiniteNumber(text) != null;
234
+ }