@hviana/sema 0.1.5 → 0.1.7

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 (170) hide show
  1. package/AGENTS.md +6 -5
  2. package/CITATION.cff +49 -0
  3. package/HOW_IT_WORKS.md +11 -12
  4. package/README.md +7 -5
  5. package/dist/example/demo.js +25 -25
  6. package/dist/example/train_base.d.ts +19 -21
  7. package/dist/example/train_base.js +1700 -1952
  8. package/dist/src/alphabet.d.ts +3 -3
  9. package/dist/src/alphabet.js +24 -27
  10. package/dist/src/alu/src/alu.d.ts +169 -194
  11. package/dist/src/alu/src/alu.js +374 -414
  12. package/dist/src/alu/src/expr.d.ts +36 -43
  13. package/dist/src/alu/src/expr.js +252 -278
  14. package/dist/src/alu/src/index.d.ts +9 -98
  15. package/dist/src/alu/src/index.js +10 -64
  16. package/dist/src/alu/src/kernel-arith.d.ts +1 -5
  17. package/dist/src/alu/src/kernel-arith.js +195 -285
  18. package/dist/src/alu/src/kernel-bits.d.ts +1 -5
  19. package/dist/src/alu/src/kernel-bits.js +81 -115
  20. package/dist/src/alu/src/kernel-logic.js +33 -63
  21. package/dist/src/alu/src/kernel-nd.js +155 -195
  22. package/dist/src/alu/src/kernel-numeric.d.ts +16 -80
  23. package/dist/src/alu/src/kernel-numeric.js +259 -331
  24. package/dist/src/alu/src/operation.d.ts +112 -132
  25. package/dist/src/alu/src/operation.js +149 -163
  26. package/dist/src/alu/src/parser.d.ts +168 -175
  27. package/dist/src/alu/src/parser.js +382 -421
  28. package/dist/src/alu/src/resonance.d.ts +16 -25
  29. package/dist/src/alu/src/resonance.js +49 -56
  30. package/dist/src/alu/src/text.d.ts +3 -7
  31. package/dist/src/alu/src/text.js +31 -37
  32. package/dist/src/alu/src/value.d.ts +14 -14
  33. package/dist/src/alu/src/value.js +150 -160
  34. package/dist/src/alu/test/alu.test.js +543 -656
  35. package/dist/src/bytes.d.ts +1 -5
  36. package/dist/src/bytes.js +33 -40
  37. package/dist/src/config.d.ts +99 -102
  38. package/dist/src/config.js +78 -83
  39. package/dist/src/derive/src/deduction.d.ts +60 -62
  40. package/dist/src/derive/src/deduction.js +103 -109
  41. package/dist/src/derive/src/index.d.ts +1 -7
  42. package/dist/src/derive/src/priority-queue.d.ts +8 -8
  43. package/dist/src/derive/src/priority-queue.js +57 -61
  44. package/dist/src/derive/src/rewrite.d.ts +15 -18
  45. package/dist/src/derive/src/rewrite.js +59 -67
  46. package/dist/src/derive/src/trie.d.ts +56 -56
  47. package/dist/src/derive/src/trie.js +175 -179
  48. package/dist/src/derive/test/derive.test.js +96 -100
  49. package/dist/src/extension.d.ts +13 -16
  50. package/dist/src/geometry.d.ts +29 -43
  51. package/dist/src/geometry.js +324 -268
  52. package/dist/src/index.d.ts +2 -17
  53. package/dist/src/index.js +2 -12
  54. package/dist/src/ingest-cache.d.ts +25 -30
  55. package/dist/src/ingest-cache.js +108 -127
  56. package/dist/src/mind/articulation.d.ts +1 -5
  57. package/dist/src/mind/articulation.js +77 -112
  58. package/dist/src/mind/attention.d.ts +40 -108
  59. package/dist/src/mind/attention.js +754 -872
  60. package/dist/src/mind/canonical.d.ts +4 -19
  61. package/dist/src/mind/canonical.js +27 -31
  62. package/dist/src/mind/graph-search.d.ts +201 -225
  63. package/dist/src/mind/graph-search.js +742 -821
  64. package/dist/src/mind/index.d.ts +2 -10
  65. package/dist/src/mind/junction.d.ts +31 -58
  66. package/dist/src/mind/junction.js +172 -237
  67. package/dist/src/mind/learning.d.ts +16 -52
  68. package/dist/src/mind/learning.js +143 -165
  69. package/dist/src/mind/match.d.ts +20 -69
  70. package/dist/src/mind/match.js +259 -318
  71. package/dist/src/mind/mechanisms/alu.js +16 -16
  72. package/dist/src/mind/mechanisms/cast.d.ts +9 -13
  73. package/dist/src/mind/mechanisms/cast.js +363 -456
  74. package/dist/src/mind/mechanisms/confluence.d.ts +8 -12
  75. package/dist/src/mind/mechanisms/confluence.js +152 -183
  76. package/dist/src/mind/mechanisms/cover.d.ts +2 -8
  77. package/dist/src/mind/mechanisms/cover.js +148 -210
  78. package/dist/src/mind/mechanisms/extraction.d.ts +8 -34
  79. package/dist/src/mind/mechanisms/extraction.js +234 -288
  80. package/dist/src/mind/mechanisms/recall.d.ts +6 -10
  81. package/dist/src/mind/mechanisms/recall.js +141 -185
  82. package/dist/src/mind/mind.d.ts +194 -155
  83. package/dist/src/mind/mind.js +392 -289
  84. package/dist/src/mind/pipeline-mechanism.d.ts +112 -124
  85. package/dist/src/mind/pipeline-mechanism.js +161 -172
  86. package/dist/src/mind/pipeline.d.ts +4 -14
  87. package/dist/src/mind/pipeline.js +125 -189
  88. package/dist/src/mind/primitives.d.ts +20 -33
  89. package/dist/src/mind/primitives.js +124 -120
  90. package/dist/src/mind/rationale.d.ts +86 -98
  91. package/dist/src/mind/rationale.js +113 -121
  92. package/dist/src/mind/reasoning.d.ts +2 -13
  93. package/dist/src/mind/reasoning.js +129 -166
  94. package/dist/src/mind/recognition.d.ts +1 -4
  95. package/dist/src/mind/recognition.js +198 -210
  96. package/dist/src/mind/resonance.d.ts +5 -22
  97. package/dist/src/mind/resonance.js +0 -0
  98. package/dist/src/mind/trace.d.ts +6 -25
  99. package/dist/src/mind/trace.js +50 -58
  100. package/dist/src/mind/traverse.d.ts +29 -58
  101. package/dist/src/mind/traverse.js +344 -356
  102. package/dist/src/mind/types.d.ts +122 -127
  103. package/dist/src/mind/types.js +60 -69
  104. package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
  105. package/dist/src/rabitq-ivf/src/database.js +201 -0
  106. package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
  107. package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.js +1 -3
  108. package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
  109. package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
  110. package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
  111. package/dist/src/rabitq-ivf/src/prng.js +36 -0
  112. package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
  113. package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
  114. package/dist/src/sema.d.ts +9 -13
  115. package/dist/src/sema.js +26 -40
  116. package/dist/src/store-sqlite.d.ts +171 -166
  117. package/dist/src/store-sqlite.js +768 -662
  118. package/dist/src/store.d.ts +597 -630
  119. package/dist/src/store.js +1434 -1581
  120. package/dist/src/vec.d.ts +5 -9
  121. package/dist/src/vec.js +61 -73
  122. package/example/train_base.ts +33 -15
  123. package/index.html +65 -0
  124. package/package.json +1 -1
  125. package/src/alu/README.md +1 -1
  126. package/src/alu/src/index.ts +1 -1
  127. package/src/config.ts +19 -27
  128. package/src/geometry.ts +93 -1
  129. package/src/index.ts +6 -11
  130. package/src/mind/attention.ts +11 -6
  131. package/src/mind/mechanisms/recall.ts +29 -13
  132. package/src/mind/mind.ts +250 -19
  133. package/src/mind/primitives.ts +43 -6
  134. package/src/mind/recognition.ts +26 -8
  135. package/src/mind/traverse.ts +47 -0
  136. package/src/mind/types.ts +20 -21
  137. package/src/rabitq-ivf/README.md +56 -0
  138. package/src/rabitq-ivf/src/database.ts +276 -0
  139. package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
  140. package/src/rabitq-ivf/src/ivf.ts +1330 -0
  141. package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
  142. package/src/store-sqlite.ts +204 -9
  143. package/src/store.ts +27 -34
  144. package/test/08-storage.test.mjs +3 -3
  145. package/test/13-conversation.test.mjs +190 -20
  146. package/test/14-scaling.test.mjs +2 -2
  147. package/test/35-ivf.test.mjs +263 -0
  148. package/test/36-bloom.test.mjs +123 -0
  149. package/dist/src/rabitq-hnsw/src/database.d.ts +0 -202
  150. package/dist/src/rabitq-hnsw/src/database.js +0 -405
  151. package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
  152. package/dist/src/rabitq-hnsw/src/heap.js +0 -94
  153. package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
  154. package/dist/src/rabitq-hnsw/src/hnsw.js +0 -500
  155. package/dist/src/rabitq-hnsw/src/index.d.ts +0 -15
  156. package/dist/src/rabitq-hnsw/src/prng.d.ts +0 -19
  157. package/dist/src/rabitq-hnsw/src/prng.js +0 -38
  158. package/dist/src/rabitq-hnsw/src/rabitq.d.ts +0 -95
  159. package/dist/src/rabitq-hnsw/src/rabitq.js +0 -299
  160. package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
  161. package/dist/src/rabitq-hnsw/src/store.js +0 -916
  162. package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
  163. package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -1197
  164. package/src/rabitq-hnsw/README.md +0 -303
  165. package/src/rabitq-hnsw/src/database.ts +0 -492
  166. package/src/rabitq-hnsw/src/heap.ts +0 -90
  167. package/src/rabitq-hnsw/src/hnsw.ts +0 -514
  168. package/src/rabitq-hnsw/src/store.ts +0 -994
  169. package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
  170. /package/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.ts +0 -0
@@ -26,8 +26,7 @@ const isDigit = (c) => c >= "0" && c <= "9";
26
26
  // so a variable or function name may be written in any script (multimodal /
27
27
  // multilingual), not just A–Z. Operator glyphs (registered symbolic forms)
28
28
  // are claimed BEFORE identifiers, so "·" and "≤" never read as names.
29
- const isLetter = (c) =>
30
- (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c.charCodeAt(0) > 127;
29
+ const isLetter = (c) => (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c.charCodeAt(0) > 127;
31
30
  const isSpace = (c) => c === " " || c === "\t" || c === "\n" || c === "\r";
32
31
  /** Tokenize an expression string: numerals (the irreducible floor), operator
33
32
  * tokens (matched longest-first against `operators` — the registry's own
@@ -36,114 +35,107 @@ const isSpace = (c) => c === " " || c === "\t" || c === "\n" || c === "\r";
36
35
  * extraction). Returns null when a character fits none of these — the span is
37
36
  * not a clean expression. */
38
37
  export function tokenize(s, operators) {
39
- const toks = [];
40
- let i = 0;
41
- while (i < s.length) {
42
- const c = s[i];
43
- if (isSpace(c)) {
44
- i++;
45
- continue;
46
- }
47
- if (isDigit(c) || (c === "." && i + 1 < s.length && isDigit(s[i + 1]))) {
48
- let j = i;
49
- let seenDot = false;
50
- while (j < s.length) {
51
- if (isDigit(s[j])) {
52
- j++;
53
- } else if (s[j] === "." && !seenDot) {
54
- seenDot = true;
55
- j++;
56
- } else {
57
- break;
38
+ const toks = [];
39
+ let i = 0;
40
+ while (i < s.length) {
41
+ const c = s[i];
42
+ if (isSpace(c)) {
43
+ i++;
44
+ continue;
58
45
  }
59
- }
60
- // optional exponent
61
- if (j < s.length && (s[j] === "e" || s[j] === "E")) {
62
- let k = j + 1;
63
- if (k < s.length && (s[k] === "+" || s[k] === "-")) {
64
- k++;
46
+ if (isDigit(c) || (c === "." && i + 1 < s.length && isDigit(s[i + 1]))) {
47
+ let j = i;
48
+ let seenDot = false;
49
+ while (j < s.length) {
50
+ if (isDigit(s[j]))
51
+ j++;
52
+ else if (s[j] === "." && !seenDot) {
53
+ seenDot = true;
54
+ j++;
55
+ }
56
+ else
57
+ break;
58
+ }
59
+ // optional exponent
60
+ if (j < s.length && (s[j] === "e" || s[j] === "E")) {
61
+ let k = j + 1;
62
+ if (k < s.length && (s[k] === "+" || s[k] === "-"))
63
+ k++;
64
+ if (k < s.length && isDigit(s[k])) {
65
+ while (k < s.length && isDigit(s[k]))
66
+ k++;
67
+ j = k;
68
+ }
69
+ }
70
+ const text = s.slice(i, j);
71
+ const num = Number(text);
72
+ if (!Number.isFinite(num))
73
+ return null;
74
+ toks.push({ kind: "num", text, value: num });
75
+ i = j;
76
+ continue;
65
77
  }
66
- if (k < s.length && isDigit(s[k])) {
67
- while (k < s.length && isDigit(s[k])) {
68
- k++;
69
- }
70
- j = k;
78
+ if (c === "(") {
79
+ toks.push({ kind: "lparen", text: c });
80
+ i++;
81
+ continue;
71
82
  }
72
- }
73
- const text = s.slice(i, j);
74
- const num = Number(text);
75
- if (!Number.isFinite(num)) {
76
- return null;
77
- }
78
- toks.push({ kind: "num", text, value: num });
79
- i = j;
80
- continue;
81
- }
82
- if (c === "(") {
83
- toks.push({ kind: "lparen", text: c });
84
- i++;
85
- continue;
86
- }
87
- if (c === ")") {
88
- toks.push({ kind: "rparen", text: c });
89
- i++;
90
- continue;
91
- }
92
- // A registered symbolic operator form, longest first — claimed before the
93
- // identifier path so an operator GLYPH (·, ×, ≤, …) is an operator, not a
94
- // name.
95
- const op = operators?.find((f) => s.startsWith(f, i));
96
- if (op !== undefined) {
97
- toks.push({ kind: "op", text: op });
98
- i += op.length;
99
- continue;
100
- }
101
- if (isLetter(c)) {
102
- let j = i;
103
- while (j < s.length && (isLetter(s[j]) || isDigit(s[j]))) {
104
- j++;
105
- }
106
- toks.push({ kind: "name", text: s.slice(i, j) });
107
- i = j;
108
- continue;
109
- }
110
- // Fallback for a symbolic character outside any vocabulary: its own token,
111
- // so name extraction (freeVariables) still splits "a*b" without a grammar.
112
- if (operators === undefined) {
113
- toks.push({ kind: "op", text: c });
114
- i++;
115
- continue;
83
+ if (c === ")") {
84
+ toks.push({ kind: "rparen", text: c });
85
+ i++;
86
+ continue;
87
+ }
88
+ // A registered symbolic operator form, longest first — claimed before the
89
+ // identifier path so an operator GLYPH (·, ×, ≤, …) is an operator, not a
90
+ // name.
91
+ const op = operators?.find((f) => s.startsWith(f, i));
92
+ if (op !== undefined) {
93
+ toks.push({ kind: "op", text: op });
94
+ i += op.length;
95
+ continue;
96
+ }
97
+ if (isLetter(c)) {
98
+ let j = i;
99
+ while (j < s.length && (isLetter(s[j]) || isDigit(s[j])))
100
+ j++;
101
+ toks.push({ kind: "name", text: s.slice(i, j) });
102
+ i = j;
103
+ continue;
104
+ }
105
+ // Fallback for a symbolic character outside any vocabulary: its own token,
106
+ // so name extraction (freeVariables) still splits "a*b" without a grammar.
107
+ if (operators === undefined) {
108
+ toks.push({ kind: "op", text: c });
109
+ i++;
110
+ continue;
111
+ }
112
+ return null; // an unexpected character → not a clean expression
116
113
  }
117
- return null; // an unexpected character → not a clean expression
118
- }
119
- return toks;
114
+ return toks;
120
115
  }
121
116
  /** The identifiers in `s` that are NOT known unary functions and NOT registered
122
117
  * constants — the free-variable candidates. Used to auto-detect the variable
123
118
  * when the caller did not name one. */
124
119
  export function freeVariables(s, isUnaryFn, isConstant = () => false) {
125
- const toks = tokenize(s);
126
- if (!toks) {
127
- return [];
128
- }
129
- const out = [];
130
- const seen = new Set();
131
- for (let k = 0; k < toks.length; k++) {
132
- const t = toks[k];
133
- if (t.kind !== "name") {
134
- continue;
135
- }
136
- // A name immediately followed by "(" is a function call, not a variable.
137
- const isCall = k + 1 < toks.length && toks[k + 1].kind === "lparen";
138
- if (isCall || isUnaryFn(t.text) || isConstant(t.text)) {
139
- continue;
140
- }
141
- if (!seen.has(t.text)) {
142
- seen.add(t.text);
143
- out.push(t.text);
120
+ const toks = tokenize(s);
121
+ if (!toks)
122
+ return [];
123
+ const out = [];
124
+ const seen = new Set();
125
+ for (let k = 0; k < toks.length; k++) {
126
+ const t = toks[k];
127
+ if (t.kind !== "name")
128
+ continue;
129
+ // A name immediately followed by "(" is a function call, not a variable.
130
+ const isCall = k + 1 < toks.length && toks[k + 1].kind === "lparen";
131
+ if (isCall || isUnaryFn(t.text) || isConstant(t.text))
132
+ continue;
133
+ if (!seen.has(t.text)) {
134
+ seen.add(t.text);
135
+ out.push(t.text);
136
+ }
144
137
  }
145
- }
146
- return out;
138
+ return out;
147
139
  }
148
140
  /** The expression grammar, read off an operation registry once and reused for
149
141
  * every evaluation. It resolves each token through the registry's own form
@@ -151,194 +143,176 @@ export function freeVariables(s, isUnaryFn, isConstant = () => false) {
151
143
  * arity-1 claimant of a shared symbol, functions as arity-1 ops, constants as
152
144
  * arity-0 ops — so the grammar and the kernel can never drift apart. */
153
145
  export class ExprGrammar {
154
- registry;
155
- applyScalar;
156
- /** Symbolic operator tokens, longest first (so "<=" wins over "<"). */
157
- operatorTokens;
158
- /** token → the infix op it names and its declared binding. */
159
- infixTable = new Map();
160
- /** token → the arity-1 (prefix) op it names. */
161
- prefixTable = new Map();
162
- /** The tightest infix tier — a prefix operator binds its operand up through
163
- * it, so "-x^2" reads -(x^2) without a hardcoded rule. */
164
- maxPrecedence;
165
- constructor(registry, applyScalar) {
166
- this.registry = registry;
167
- this.applyScalar = applyScalar;
168
- const tokens = new Set();
169
- let maxPrec = 0;
170
- for (const { form, name } of registry.formEntries()) {
171
- const op = registry.get(name);
172
- const symbolic = form.length > 0 && !/[A-Za-z0-9]/.test(form);
173
- if (!symbolic) {
174
- continue;
175
- }
176
- tokens.add(form);
177
- if (op.infix && !this.infixTable.has(form)) {
178
- this.infixTable.set(form, { name, syntax: op.infix });
179
- maxPrec = Math.max(maxPrec, op.infix.precedence);
180
- }
181
- if (op.arity === 1 && !this.prefixTable.has(form)) {
182
- this.prefixTable.set(form, name);
183
- }
146
+ registry;
147
+ applyScalar;
148
+ /** Symbolic operator tokens, longest first (so "<=" wins over "<"). */
149
+ operatorTokens;
150
+ /** token → the infix op it names and its declared binding. */
151
+ infixTable = new Map();
152
+ /** token → the arity-1 (prefix) op it names. */
153
+ prefixTable = new Map();
154
+ /** The tightest infix tier — a prefix operator binds its operand up through
155
+ * it, so "-x^2" reads -(x^2) without a hardcoded rule. */
156
+ maxPrecedence;
157
+ constructor(registry, applyScalar) {
158
+ this.registry = registry;
159
+ this.applyScalar = applyScalar;
160
+ const tokens = new Set();
161
+ let maxPrec = 0;
162
+ for (const { form, name } of registry.formEntries()) {
163
+ const op = registry.get(name);
164
+ const symbolic = form.length > 0 && !/[A-Za-z0-9]/.test(form);
165
+ if (!symbolic)
166
+ continue;
167
+ tokens.add(form);
168
+ if (op.infix && !this.infixTable.has(form)) {
169
+ this.infixTable.set(form, { name, syntax: op.infix });
170
+ maxPrec = Math.max(maxPrec, op.infix.precedence);
171
+ }
172
+ if (op.arity === 1 && !this.prefixTable.has(form)) {
173
+ this.prefixTable.set(form, name);
174
+ }
175
+ }
176
+ this.operatorTokens = [...tokens].sort((a, b) => b.length - a.length);
177
+ this.maxPrecedence = maxPrec;
184
178
  }
185
- this.operatorTokens = [...tokens].sort((a, b) => b.length - a.length);
186
- this.maxPrecedence = maxPrec;
187
- }
188
- /** Resolve an identifier to a registered op of the given arity: a surface
189
- * form claimant of that arity first, else the canonical name itself. */
190
- resolveName(name, arity) {
191
- for (const n of this.registry.lookupForm(name)) {
192
- if (this.registry.get(n).arity === arity) {
193
- return n;
194
- }
179
+ /** Resolve an identifier to a registered op of the given arity: a surface
180
+ * form claimant of that arity first, else the canonical name itself. */
181
+ resolveName(name, arity) {
182
+ for (const n of this.registry.lookupForm(name)) {
183
+ if (this.registry.get(n).arity === arity)
184
+ return n;
185
+ }
186
+ const own = this.registry.get(name);
187
+ return own && own.arity === arity ? name : null;
195
188
  }
196
- const own = this.registry.get(name);
197
- return own && own.arity === arity ? name : null;
198
- }
199
- /** Whether an identifier names a unary function. */
200
- isFunction(name) {
201
- return this.resolveName(name, 1) !== null;
202
- }
203
- /** Whether an identifier names a nullary constant (an arity-0 op). */
204
- isConstant(name) {
205
- return this.resolveName(name, 0) !== null;
206
- }
207
- /** Tokenize with this grammar's operator vocabulary. */
208
- tokenize(s) {
209
- return tokenize(s, this.operatorTokens);
210
- }
211
- /** The free-variable candidates of `s` under this grammar. */
212
- freeVariables(s) {
213
- return freeVariables(
214
- s,
215
- (n) => this.isFunction(n),
216
- (n) => this.isConstant(n),
217
- );
218
- }
219
- /** Evaluate expression `s` with `variable` bound to `at`. Returns a number,
220
- * or null if `s` is not a well-formed expression. Every binary/function/
221
- * constant step is applied through the kernel, so the arithmetic is the
222
- * kernel's own. When `variable` is empty, the single free variable is
223
- * auto-detected; if there is more than one and none was named, evaluation
224
- * declines (null) — a genuinely multivariate expression needs an explicit
225
- * binding. */
226
- eval(s, variable, at) {
227
- const toks = this.tokenize(s);
228
- if (!toks || toks.length === 0) {
229
- return null;
189
+ /** Whether an identifier names a unary function. */
190
+ isFunction(name) {
191
+ return this.resolveName(name, 1) !== null;
230
192
  }
231
- let v = variable;
232
- if (v === "") {
233
- const fv = this.freeVariables(s);
234
- if (fv.length === 1) {
235
- v = fv[0];
236
- } else if (fv.length > 1) {
237
- return null; // multivariate, no binding named
238
- }
239
- // fv.length === 0: a constant expression is fine.
193
+ /** Whether an identifier names a nullary constant (an arity-0 op). */
194
+ isConstant(name) {
195
+ return this.resolveName(name, 0) !== null;
240
196
  }
241
- let pos = 0;
242
- const peek = () => toks[pos];
243
- const next = () => toks[pos++];
244
- const FAIL = Symbol("expr-fail");
245
- const fail = () => {
246
- throw FAIL;
247
- };
248
- const grammar = this;
249
- // Precedence climbing over the registry-declared infix tiers.
250
- function parseExpr(minPrec) {
251
- let lhs = parseOperand();
252
- for (;;) {
253
- const t = peek();
254
- if (!t || t.kind !== "op") {
255
- break;
256
- }
257
- const infix = grammar.infixTable.get(t.text);
258
- if (!infix || infix.syntax.precedence < minPrec) {
259
- break;
260
- }
261
- next();
262
- const nextMin = infix.syntax.rightAssoc
263
- ? infix.syntax.precedence
264
- : infix.syntax.precedence + 1;
265
- const rhs = parseExpr(nextMin);
266
- lhs = grammar.applyScalar(infix.name, [lhs, rhs]);
267
- }
268
- return lhs;
197
+ /** Tokenize with this grammar's operator vocabulary. */
198
+ tokenize(s) {
199
+ return tokenize(s, this.operatorTokens);
269
200
  }
270
- // An operand: a prefix operator (the arity-1 claimant of its symbol,
271
- // binding up through the tightest infix tier), or an atom.
272
- function parseOperand() {
273
- const t = peek();
274
- if (t && t.kind === "op") {
275
- const prefix = grammar.prefixTable.get(t.text);
276
- if (prefix === undefined) {
277
- return fail();
278
- }
279
- next();
280
- return grammar.applyScalar(prefix, [parseExpr(grammar.maxPrecedence)]);
281
- }
282
- return parseAtom();
201
+ /** The free-variable candidates of `s` under this grammar. */
202
+ freeVariables(s) {
203
+ return freeVariables(s, (n) => this.isFunction(n), (n) => this.isConstant(n));
283
204
  }
284
- function parseAtom() {
285
- const t = peek();
286
- if (!t) {
287
- fail();
288
- }
289
- if (t.kind === "num") {
290
- next();
291
- return t.value;
292
- }
293
- if (t.kind === "lparen") {
294
- next();
295
- const inner = parseExpr(0);
296
- if (!peek() || peek().kind !== "rparen") {
297
- fail();
205
+ /** Evaluate expression `s` with `variable` bound to `at`. Returns a number,
206
+ * or null if `s` is not a well-formed expression. Every binary/function/
207
+ * constant step is applied through the kernel, so the arithmetic is the
208
+ * kernel's own. When `variable` is empty, the single free variable is
209
+ * auto-detected; if there is more than one and none was named, evaluation
210
+ * declines (null) a genuinely multivariate expression needs an explicit
211
+ * binding. */
212
+ eval(s, variable, at) {
213
+ const toks = this.tokenize(s);
214
+ if (!toks || toks.length === 0)
215
+ return null;
216
+ let v = variable;
217
+ if (v === "") {
218
+ const fv = this.freeVariables(s);
219
+ if (fv.length === 1)
220
+ v = fv[0];
221
+ else if (fv.length > 1)
222
+ return null; // multivariate, no binding named
223
+ // fv.length === 0: a constant expression is fine.
224
+ }
225
+ let pos = 0;
226
+ const peek = () => toks[pos];
227
+ const next = () => toks[pos++];
228
+ const FAIL = Symbol("expr-fail");
229
+ const fail = () => {
230
+ throw FAIL;
231
+ };
232
+ const grammar = this;
233
+ // Precedence climbing over the registry-declared infix tiers.
234
+ function parseExpr(minPrec) {
235
+ let lhs = parseOperand();
236
+ for (;;) {
237
+ const t = peek();
238
+ if (!t || t.kind !== "op")
239
+ break;
240
+ const infix = grammar.infixTable.get(t.text);
241
+ if (!infix || infix.syntax.precedence < minPrec)
242
+ break;
243
+ next();
244
+ const nextMin = infix.syntax.rightAssoc
245
+ ? infix.syntax.precedence
246
+ : infix.syntax.precedence + 1;
247
+ const rhs = parseExpr(nextMin);
248
+ lhs = grammar.applyScalar(infix.name, [lhs, rhs]);
249
+ }
250
+ return lhs;
298
251
  }
299
- next();
300
- return inner;
301
- }
302
- if (t.kind === "name") {
303
- next();
304
- // A function call: name "(" expr ")".
305
- if (peek() && peek().kind === "lparen") {
306
- const fn = grammar.resolveName(t.text, 1);
307
- if (fn === null) {
252
+ // An operand: a prefix operator (the arity-1 claimant of its symbol,
253
+ // binding up through the tightest infix tier), or an atom.
254
+ function parseOperand() {
255
+ const t = peek();
256
+ if (t && t.kind === "op") {
257
+ const prefix = grammar.prefixTable.get(t.text);
258
+ if (prefix === undefined)
259
+ return fail();
260
+ next();
261
+ return grammar.applyScalar(prefix, [parseExpr(grammar.maxPrecedence)]);
262
+ }
263
+ return parseAtom();
264
+ }
265
+ function parseAtom() {
266
+ const t = peek();
267
+ if (!t)
268
+ fail();
269
+ if (t.kind === "num") {
270
+ next();
271
+ return t.value;
272
+ }
273
+ if (t.kind === "lparen") {
274
+ next();
275
+ const inner = parseExpr(0);
276
+ if (!peek() || peek().kind !== "rparen")
277
+ fail();
278
+ next();
279
+ return inner;
280
+ }
281
+ if (t.kind === "name") {
282
+ next();
283
+ // A function call: name "(" expr ")".
284
+ if (peek() && peek().kind === "lparen") {
285
+ const fn = grammar.resolveName(t.text, 1);
286
+ if (fn === null)
287
+ return fail();
288
+ next(); // (
289
+ const arg = parseExpr(0);
290
+ if (!peek() || peek().kind !== "rparen")
291
+ fail();
292
+ next(); // )
293
+ return grammar.applyScalar(fn, [arg]);
294
+ }
295
+ // Otherwise an identifier: the free variable, or a nullary constant
296
+ // resolved through the registry like any other operation.
297
+ if (t.text === v)
298
+ return at;
299
+ const konst = grammar.resolveName(t.text, 0);
300
+ if (konst !== null)
301
+ return grammar.applyScalar(konst, []);
302
+ return fail();
303
+ }
308
304
  return fail();
309
- }
310
- next(); // (
311
- const arg = parseExpr(0);
312
- if (!peek() || peek().kind !== "rparen") {
313
- fail();
314
- }
315
- next(); // )
316
- return grammar.applyScalar(fn, [arg]);
317
305
  }
318
- // Otherwise an identifier: the free variable, or a nullary constant
319
- // resolved through the registry like any other operation.
320
- if (t.text === v) {
321
- return at;
306
+ try {
307
+ const result = parseExpr(0);
308
+ if (pos !== toks.length)
309
+ return null; // trailing tokens → malformed
310
+ return Number.isFinite(result) ? result : null;
322
311
  }
323
- const konst = grammar.resolveName(t.text, 0);
324
- if (konst !== null) {
325
- return grammar.applyScalar(konst, []);
312
+ catch (err) {
313
+ if (err === FAIL)
314
+ return null;
315
+ return null; // a kernel apply threw (e.g. ÷0 produced ∞) → decline
326
316
  }
327
- return fail();
328
- }
329
- return fail();
330
- }
331
- try {
332
- const result = parseExpr(0);
333
- if (pos !== toks.length) {
334
- return null; // trailing tokens → malformed
335
- }
336
- return Number.isFinite(result) ? result : null;
337
- } catch (err) {
338
- if (err === FAIL) {
339
- return null;
340
- }
341
- return null; // a kernel apply threw (e.g. ÷0 produced ∞) → decline
342
317
  }
343
- }
344
318
  }