@hviana/sema 0.1.5 → 0.1.6

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 (160) 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 +1687 -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 +17 -42
  51. package/dist/src/geometry.js +235 -266
  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 +749 -871
  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 +126 -185
  82. package/dist/src/mind/mind.d.ts +129 -154
  83. package/dist/src/mind/mind.js +258 -288
  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 +8 -32
  89. package/dist/src/mind/primitives.js +99 -117
  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 +183 -208
  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 +16 -58
  101. package/dist/src/mind/traverse.js +304 -357
  102. package/dist/src/mind/types.d.ts +120 -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 +169 -166
  117. package/dist/src/store-sqlite.js +762 -662
  118. package/dist/src/store.d.ts +586 -630
  119. package/dist/src/store.js +1423 -1581
  120. package/dist/src/vec.d.ts +5 -9
  121. package/dist/src/vec.js +61 -73
  122. package/example/train_base.ts +13 -10
  123. package/package.json +1 -1
  124. package/src/alu/README.md +1 -1
  125. package/src/alu/src/index.ts +1 -1
  126. package/src/config.ts +19 -27
  127. package/src/index.ts +6 -11
  128. package/src/rabitq-ivf/README.md +56 -0
  129. package/src/rabitq-ivf/src/database.ts +276 -0
  130. package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
  131. package/src/rabitq-ivf/src/ivf.ts +1330 -0
  132. package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
  133. package/src/store-sqlite.ts +196 -9
  134. package/src/store.ts +8 -32
  135. package/test/08-storage.test.mjs +3 -3
  136. package/test/14-scaling.test.mjs +2 -2
  137. package/test/35-ivf.test.mjs +263 -0
  138. package/test/36-bloom.test.mjs +123 -0
  139. package/dist/src/rabitq-hnsw/src/database.d.ts +0 -202
  140. package/dist/src/rabitq-hnsw/src/database.js +0 -405
  141. package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
  142. package/dist/src/rabitq-hnsw/src/heap.js +0 -94
  143. package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
  144. package/dist/src/rabitq-hnsw/src/hnsw.js +0 -500
  145. package/dist/src/rabitq-hnsw/src/index.d.ts +0 -15
  146. package/dist/src/rabitq-hnsw/src/prng.d.ts +0 -19
  147. package/dist/src/rabitq-hnsw/src/prng.js +0 -38
  148. package/dist/src/rabitq-hnsw/src/rabitq.d.ts +0 -95
  149. package/dist/src/rabitq-hnsw/src/rabitq.js +0 -299
  150. package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
  151. package/dist/src/rabitq-hnsw/src/store.js +0 -916
  152. package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
  153. package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -1197
  154. package/src/rabitq-hnsw/README.md +0 -303
  155. package/src/rabitq-hnsw/src/database.ts +0 -492
  156. package/src/rabitq-hnsw/src/heap.ts +0 -90
  157. package/src/rabitq-hnsw/src/hnsw.ts +0 -514
  158. package/src/rabitq-hnsw/src/store.ts +0 -994
  159. package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
  160. /package/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.ts +0 -0
@@ -32,449 +32,409 @@
32
32
  // glyph, or an operation the bytes do not spell at all (an integral, a limit) —
33
33
  // are resolved only through the host port; the kernel itself stays a pure,
34
34
  // deterministic machine over literal numerals and registered surface forms.
35
- import { asReal, decimalCodec, nd, parseValue, real } from "./value.js";
36
- import { NO_RESONANCE, OperationRegistry } from "./operation.js";
35
+ import { asReal, decimalCodec, nd, parseValue, real, } from "./value.js";
36
+ import { NO_RESONANCE, OperationRegistry, } from "./operation.js";
37
37
  import { registerLogic } from "./kernel-logic.js";
38
38
  import { registerBits } from "./kernel-bits.js";
39
39
  import { registerArith } from "./kernel-arith.js";
40
40
  import { registerNumeric } from "./kernel-numeric.js";
41
41
  import { registerNd } from "./kernel-nd.js";
42
42
  import { ExprGrammar } from "./expr.js";
43
- import { QueryParser, STRUCTURAL_HOST } from "./parser.js";
44
- import {
45
- CLOSE,
46
- DOT,
47
- isDigitByte,
48
- isSepByte,
49
- matchBracket,
50
- OPEN,
51
- trimEnd,
52
- trimStart,
53
- } from "./text.js";
43
+ import { QueryParser, STRUCTURAL_HOST, } from "./parser.js";
44
+ import { CLOSE, DOT, isDigitByte, isSepByte, matchBracket, OPEN, trimEnd, trimStart, } from "./text.js";
54
45
  import { latin1 } from "../../bytes.js";
55
46
  export class Alu {
56
- registry = new OperationRegistry();
57
- codec;
58
- rt;
59
- /** Symbolic operator forms (no ASCII letter/digit), as byte patterns, sorted
60
- * longest-first so the scanner matches "<=" before "<". */
61
- symbolicForms;
62
- _grammar;
63
- _anchors;
64
- /** How many {@link apply} calls ended in a caught throw this session, and
65
- * the most recent caught error. A caught throw is USUALLY a routine
66
- * decline (a symbol fed to arithmetic — the "rule does not fire"
67
- * contract), but the same catch would also swallow a genuine kernel bug;
68
- * these two fields make that observable instead of silent. Zero cost when
69
- * nothing throws. */
70
- applyCaught = 0;
71
- lastApplyError = null;
72
- decoder = new TextDecoder();
73
- /** The query parser, wired to the host port — internal; hosts reach it only
74
- * through {@link parse} and {@link compute}. */
75
- parser;
76
- constructor(opts = {}, host = STRUCTURAL_HOST) {
77
- const precision = opts.precision ?? 6;
78
- this.codec = decimalCodec(precision);
79
- this.rt = { tol: opts.tol ?? 1e-10, maxIter: opts.maxIter ?? 1000 };
80
- registerLogic(this.registry);
81
- registerBits(this.registry);
82
- registerArith(this.registry);
83
- registerNumeric(this.registry);
84
- registerNd(this.registry);
85
- this.symbolicForms = this.buildSymbolicForms();
86
- this.parser = new QueryParser(this, host);
87
- }
88
- /** Recognise and evaluate every computation `query` invokes — infix
89
- * arithmetic runs, and operations named literally or by meaning (through the
90
- * host port). All async resonance is resolved inside, so the caller
91
- * receives finished spans it can fold synchronously into its search. See
92
- * {@link "./parser.js".QueryParser.parse}. */
93
- parse(query) {
94
- return this.parser.parse(query);
95
- }
96
- /** Apply an operation to operand byte spans — operand STRUCTURE recognised
97
- * into Values, every reachable symbol's resonance pre-resolved through the
98
- * host port — and encode the result canonically; null when the computation
99
- * declines. See {@link "./parser.js".QueryParser.compute}. */
100
- compute(name, operandBytes, asSymbol = () => false) {
101
- return this.parser.compute(name, operandBytes, asSymbol);
102
- }
103
- /** Infix arity of an op: its fixed arity, or 2 for a variadic op (an infix
104
- * operator binds two operands; the registry's variadic fold still accepts
105
- * the pair). */
106
- infixArity(op) {
107
- return op.arity === "variadic" ? 2 : op.arity;
108
- }
109
- /** Collect the symbolic surface forms (punctuation, no letters/digits) as
110
- * UTF-8 byte patterns. Each maps to the binary claimant when one exists, so
111
- * an infix scan resolves a shared symbol to its two-operand reading. */
112
- buildSymbolicForms() {
113
- const enc = new TextEncoder();
114
- const isSymbolic = (s) => s.length > 0 && !/[A-Za-z0-9]/.test(s);
115
- const byForm = new Map();
116
- for (const { form, name } of this.registry.formEntries()) {
117
- if (!isSymbolic(form)) {
118
- continue;
119
- }
120
- const a = byForm.get(form);
121
- if (a) {
122
- a.push(name);
123
- } else {
124
- byForm.set(form, [name]);
125
- }
47
+ registry = new OperationRegistry();
48
+ codec;
49
+ rt;
50
+ /** Symbolic operator forms (no ASCII letter/digit), as byte patterns, sorted
51
+ * longest-first so the scanner matches "<=" before "<". */
52
+ symbolicForms;
53
+ _grammar;
54
+ _anchors;
55
+ /** How many {@link apply} calls ended in a caught throw this session, and
56
+ * the most recent caught error. A caught throw is USUALLY a routine
57
+ * decline (a symbol fed to arithmetic — the "rule does not fire"
58
+ * contract), but the same catch would also swallow a genuine kernel bug;
59
+ * these two fields make that observable instead of silent. Zero cost when
60
+ * nothing throws. */
61
+ applyCaught = 0;
62
+ lastApplyError = null;
63
+ decoder = new TextDecoder();
64
+ /** The query parser, wired to the host port — internal; hosts reach it only
65
+ * through {@link parse} and {@link compute}. */
66
+ parser;
67
+ constructor(opts = {}, host = STRUCTURAL_HOST) {
68
+ const precision = opts.precision ?? 6;
69
+ this.codec = decimalCodec(precision);
70
+ this.rt = { tol: opts.tol ?? 1e-10, maxIter: opts.maxIter ?? 1000 };
71
+ registerLogic(this.registry);
72
+ registerBits(this.registry);
73
+ registerArith(this.registry);
74
+ registerNumeric(this.registry);
75
+ registerNd(this.registry);
76
+ this.symbolicForms = this.buildSymbolicForms();
77
+ this.parser = new QueryParser(this, host);
126
78
  }
127
- const out = [];
128
- for (const [form, names] of byForm) {
129
- // Prefer a binary/variadic claimant for infix matching.
130
- let chosen = names[0];
131
- let chosenArity = this.infixArity(this.registry.get(chosen));
132
- for (const n of names) {
133
- const a = this.infixArity(this.registry.get(n));
134
- if (a === 2) {
135
- chosen = n;
136
- chosenArity = a;
137
- break;
138
- }
139
- }
140
- out.push({ bytes: enc.encode(form), name: chosen, arity: chosenArity });
79
+ /** Recognise and evaluate every computation `query` invokes — infix
80
+ * arithmetic runs, and operations named literally or by meaning (through the
81
+ * host port). All async resonance is resolved inside, so the caller
82
+ * receives finished spans it can fold synchronously into its search. See
83
+ * {@link "./parser.js".QueryParser.parse}. */
84
+ parse(query) {
85
+ return this.parser.parse(query);
86
+ }
87
+ /** Apply an operation to operand byte spans — operand STRUCTURE recognised
88
+ * into Values, every reachable symbol's resonance pre-resolved through the
89
+ * host port — and encode the result canonically; null when the computation
90
+ * declines. See {@link "./parser.js".QueryParser.compute}. */
91
+ compute(name, operandBytes, asSymbol = () => false) {
92
+ return this.parser.compute(name, operandBytes, asSymbol);
141
93
  }
142
- // Longest first so a longer form wins over a prefix of it ("<=" before "<").
143
- out.sort((a, b) => b.bytes.length - a.bytes.length);
144
- return out;
145
- }
146
- /** The canonical op name(s) a literal surface form names (exact lookup). */
147
- lookupOperator(form) {
148
- return this.registry.lookupForm(form);
149
- }
150
- /** The operation CONCEPT anchors: the (canonical name, form bytes) pairs a
151
- * span's MEANING is resonated against, so an operation the bytes do not
152
- * literally spell (an integral, a derivative, a limit, …, in any modality)
153
- * is recognised by gist nearness to one of these forms. This is what makes
154
- * operation recognition generic and modality-agnostic rather than a fixed
155
- * symbol table — and the ALU, not the host, decides which of its surface
156
- * forms are anchors, using its own machinery:
157
- *
158
- * • a form the SCANNER already reads in full (a pure operator symbol like
159
- * "<=", a numeral like "0") is excluded — the literal path owns it, and
160
- * its meaning-space image would only add noise;
161
- * • a form must be COMPOUND (≥ 2 runes): a single atom's gist is a
162
- * coordinate of the alphabet, not a distributional meaning, so there is
163
- * nothing for resonance to read.
164
- *
165
- * Cached once; the stable array identity lets the host memoise whatever
166
- * representation (gists, indices) it derives from it. */
167
- conceptAnchors() {
168
- if (this._anchors) {
169
- return this._anchors;
94
+ /** Infix arity of an op: its fixed arity, or 2 for a variadic op (an infix
95
+ * operator binds two operands; the registry's variadic fold still accepts
96
+ * the pair). */
97
+ infixArity(op) {
98
+ return op.arity === "variadic" ? 2 : op.arity;
170
99
  }
171
- const enc = new TextEncoder();
172
- const out = [];
173
- for (const { form, name } of this.registry.formEntries()) {
174
- if ([...form].length < 2) {
175
- continue; // a lone atom carries no gist
176
- }
177
- const bytes = enc.encode(form);
178
- // Fully claimed by the literal scanner the literal path owns it.
179
- const { operands, operators } = this.scan(bytes);
180
- let claimed = 0;
181
- for (const t of [...operands, ...operators]) {
182
- claimed += t.j - t.i;
183
- }
184
- if (claimed === bytes.length) {
185
- continue;
186
- }
187
- out.push({ name, form: bytes });
100
+ /** Collect the symbolic surface forms (punctuation, no letters/digits) as
101
+ * UTF-8 byte patterns. Each maps to the binary claimant when one exists, so
102
+ * an infix scan resolves a shared symbol to its two-operand reading. */
103
+ buildSymbolicForms() {
104
+ const enc = new TextEncoder();
105
+ const isSymbolic = (s) => s.length > 0 && !/[A-Za-z0-9]/.test(s);
106
+ const byForm = new Map();
107
+ for (const { form, name } of this.registry.formEntries()) {
108
+ if (!isSymbolic(form))
109
+ continue;
110
+ const a = byForm.get(form);
111
+ if (a)
112
+ a.push(name);
113
+ else
114
+ byForm.set(form, [name]);
115
+ }
116
+ const out = [];
117
+ for (const [form, names] of byForm) {
118
+ // Prefer a binary/variadic claimant for infix matching.
119
+ let chosen = names[0];
120
+ let chosenArity = this.infixArity(this.registry.get(chosen));
121
+ for (const n of names) {
122
+ const a = this.infixArity(this.registry.get(n));
123
+ if (a === 2) {
124
+ chosen = n;
125
+ chosenArity = a;
126
+ break;
127
+ }
128
+ }
129
+ out.push({ bytes: enc.encode(form), name: chosen, arity: chosenArity });
130
+ }
131
+ // Longest first so a longer form wins over a prefix of it ("<=" before "<").
132
+ out.sort((a, b) => b.bytes.length - a.bytes.length);
133
+ return out;
188
134
  }
189
- return (this._anchors = out);
190
- }
191
- /** The registry-derived expression grammar, built once: infix binding and
192
- * prefix/function/constant resolution all read off the registered ops, and
193
- * every evaluation step routes back through this registry's own arithmetic —
194
- * recursion all the way down (see expr.ts). */
195
- get grammar() {
196
- if (!this._grammar) {
197
- const scalarCtx = this.registry.context(NO_RESONANCE, this.rt);
198
- this._grammar = new ExprGrammar(
199
- this.registry,
200
- (name, args) => asReal(scalarCtx.apply(name, args.map(real))),
201
- );
135
+ /** The canonical op name(s) a literal surface form names (exact lookup). */
136
+ lookupOperator(form) {
137
+ return this.registry.lookupForm(form);
202
138
  }
203
- return this._grammar;
204
- }
205
- /** Evaluate an expression's bytes at a variable binding, through this
206
- * registry's own arithmetic (see {@link grammar}). */
207
- evalExpression(bytes, variable, at) {
208
- return this.grammar.eval(this.decoder.decode(bytes), variable, at);
209
- }
210
- /** Whether a name is a registered operation. */
211
- has(name) {
212
- return this.registry.has(name);
213
- }
214
- /** The infix arity of a registered op (for the host to size a rule), or 0. */
215
- arityOf(name) {
216
- const op = this.registry.get(name);
217
- return op ? this.infixArity(op) : 0;
218
- }
219
- /** Whether an op acts on an EXPRESSION (a function) rather than plain numbers
220
- * — declared by the op itself at registration (the `expression` trait), so
221
- * there is no side table of names here. */
222
- isExpressionOp(name) {
223
- return this.registry.get(name)?.expression === true;
224
- }
225
- /** Scan a byte span for operand (numeric) and operator (symbolic) spans. A
226
- * pure, deterministic left-to-right lexer: at each position take a numeral if
227
- * one starts there, else the longest symbolic operator form, else skip one
228
- * byte. Numerals and operator symbols are disjoint character classes, so the
229
- * two never overlap. */
230
- scan(bytes) {
231
- const operands = [];
232
- const operators = [];
233
- const n = bytes.length;
234
- let i = 0;
235
- while (i < n) {
236
- const c = bytes[i];
237
- // A numeral: a run of digits, with at most one interior decimal point that
238
- // is itself flanked by digits (so a trailing "." is left to the operators
239
- // / skip path, not swallowed).
240
- if (
241
- isDigitByte(c) || (c === DOT && i + 1 < n && isDigitByte(bytes[i + 1]))
242
- ) {
243
- let j = i;
244
- let seenDot = false;
245
- while (j < n) {
246
- if (isDigitByte(bytes[j])) {
247
- j++;
248
- } else if (
249
- bytes[j] === DOT && !seenDot && j + 1 < n &&
250
- isDigitByte(bytes[j + 1])
251
- ) {
252
- seenDot = true;
253
- j++;
254
- } else {
255
- break;
256
- }
139
+ /** The operation CONCEPT anchors: the (canonical name, form bytes) pairs a
140
+ * span's MEANING is resonated against, so an operation the bytes do not
141
+ * literally spell (an integral, a derivative, a limit, …, in any modality)
142
+ * is recognised by gist nearness to one of these forms. This is what makes
143
+ * operation recognition generic and modality-agnostic rather than a fixed
144
+ * symbol table — and the ALU, not the host, decides which of its surface
145
+ * forms are anchors, using its own machinery:
146
+ *
147
+ * • a form the SCANNER already reads in full (a pure operator symbol like
148
+ * "<=", a numeral like "0") is excluded — the literal path owns it, and
149
+ * its meaning-space image would only add noise;
150
+ * • a form must be COMPOUND ( 2 runes): a single atom's gist is a
151
+ * coordinate of the alphabet, not a distributional meaning, so there is
152
+ * nothing for resonance to read.
153
+ *
154
+ * Cached once; the stable array identity lets the host memoise whatever
155
+ * representation (gists, indices) it derives from it. */
156
+ conceptAnchors() {
157
+ if (this._anchors)
158
+ return this._anchors;
159
+ const enc = new TextEncoder();
160
+ const out = [];
161
+ for (const { form, name } of this.registry.formEntries()) {
162
+ if ([...form].length < 2)
163
+ continue; // a lone atom carries no gist
164
+ const bytes = enc.encode(form);
165
+ // Fully claimed by the literal scanner → the literal path owns it.
166
+ const { operands, operators } = this.scan(bytes);
167
+ let claimed = 0;
168
+ for (const t of [...operands, ...operators])
169
+ claimed += t.j - t.i;
170
+ if (claimed === bytes.length)
171
+ continue;
172
+ out.push({ name, form: bytes });
257
173
  }
258
- operands.push({ i, j, value: parseValue(bytes.subarray(i, j)) });
259
- i = j;
260
- continue;
261
- }
262
- // The longest symbolic operator form starting here.
263
- const op = this.matchSymbolic(bytes, i);
264
- if (op) {
265
- operators.push({
266
- i,
267
- j: i + op.bytes.length,
268
- name: op.name,
269
- arity: op.arity,
270
- });
271
- i += op.bytes.length;
272
- continue;
273
- }
274
- i++;
174
+ return (this._anchors = out);
275
175
  }
276
- return { operands, operators };
277
- }
278
- matchSymbolic(bytes, at) {
279
- for (const f of this.symbolicForms) {
280
- const len = f.bytes.length;
281
- if (at + len > bytes.length) {
282
- continue;
283
- }
284
- let ok = true;
285
- for (let k = 0; k < len; k++) {
286
- if (bytes[at + k] !== f.bytes[k]) {
287
- ok = false;
288
- break;
176
+ /** The registry-derived expression grammar, built once: infix binding and
177
+ * prefix/function/constant resolution all read off the registered ops, and
178
+ * every evaluation step routes back through this registry's own arithmetic —
179
+ * recursion all the way down (see expr.ts). */
180
+ get grammar() {
181
+ if (!this._grammar) {
182
+ const scalarCtx = this.registry.context(NO_RESONANCE, this.rt);
183
+ this._grammar = new ExprGrammar(this.registry, (name, args) => asReal(scalarCtx.apply(name, args.map(real))));
289
184
  }
290
- }
291
- if (ok) {
292
- return f;
293
- }
185
+ return this._grammar;
294
186
  }
295
- return null;
296
- }
297
- /** Read a byte span into a {@link Value} by recognising its STRUCTURE — the
298
- * byte⇄Value boundary the host computes across. The kernel itself only knows
299
- * the irreducible scalar floor (a numeral's digits → a quantity, via {@link
300
- * parseValue}); recognising whether a span is one quantity or a LIST of them is
301
- * layered here, over {@link scan}, so no caller re-implements it.
302
- *
303
- * A LIST is a run of element values joined by a CONSISTENT separator. No
304
- * spelling is privileged: two complementary readings cover the forms a list
305
- * takes —
306
- *
307
- * • CONTAINER — an explicit `[ … ]` group delimits a list whose elements may
308
- * be anything (symbols, nested groups, mixed): split its interior at the TOP
309
- * level (bracket-depth aware) on separator runs, each element recognised in
310
- * turn (so nesting and heterogeneity recurse for free). This is also the
311
- * codec's canonical OUTPUT spelling, so a computed list feeds straight back
312
- * in as an operand.
313
- * • SEQUENCE — a bare run of ≥2 numeric operands with a consistent connective
314
- * between them (`1 2 3`, `1, 2, 3`, `1 and 2 and 3`): the operands {@link
315
- * scan} finds are the elements, and whatever sits between them — a space, a
316
- * comma, " and " — is the separator, accepted as long as it is the same
317
- * throughout and the run has no leftover edges.
318
- *
319
- * Anything else is a SCALAR ({@link parseValue}): a numeral, or an opaque
320
- * symbol of any modality (a learnt form, an operator name a higher-order op
321
- * will resolve, a single word). */
322
- recogniseValue(bytes) {
323
- const s = bytes.subarray(trimStart(bytes), trimEnd(bytes));
324
- // CONTAINER: a balanced [ … ] group spanning the whole (trimmed) span.
325
- if (s.length >= 2 && s[0] === OPEN && matchBracket(s, 0) === s.length - 1) {
326
- return nd(this.recogniseContainer(s.subarray(1, s.length - 1)));
187
+ /** Evaluate an expression's bytes at a variable binding, through this
188
+ * registry's own arithmetic (see {@link grammar}). */
189
+ evalExpression(bytes, variable, at) {
190
+ return this.grammar.eval(this.decoder.decode(bytes), variable, at);
327
191
  }
328
- // SEQUENCE: ≥2 numeric operands with a consistent connective between them.
329
- const seq = this.recogniseSequence(s);
330
- if (seq !== null) {
331
- return nd(seq);
192
+ /** Whether a name is a registered operation. */
193
+ has(name) {
194
+ return this.registry.has(name);
332
195
  }
333
- // SCALAR floor.
334
- return parseValue(s);
335
- }
336
- /** Split a CONTAINER's interior into its element values — top-level (bracket-
337
- * depth aware) spans divided by separator RUNS (maximal runs of {@link
338
- * isSepByte}). The brackets are the explicit delimiter, so the divider's
339
- * spelling is not privileged (`[1,2,3]`, `[1, 2, 3]`, `[1 2 3]` are the same
340
- * list). Each element is recognised in turn, so a nested `[ … ]` element
341
- * recurses and a symbol element stays opaque. An empty interior is `[]`. */
342
- recogniseContainer(inner) {
343
- if (trimEnd(inner) <= trimStart(inner)) {
344
- return []; // empty / all space → []
196
+ /** The infix arity of a registered op (for the host to size a rule), or 0. */
197
+ arityOf(name) {
198
+ const op = this.registry.get(name);
199
+ return op ? this.infixArity(op) : 0;
345
200
  }
346
- const elements = [];
347
- let depth = 0;
348
- let start = 0;
349
- let p = 0;
350
- while (p < inner.length) {
351
- const c = inner[p];
352
- if (c === OPEN) {
353
- depth++;
354
- } else if (c === CLOSE) {
355
- depth--;
356
- }
357
- if (depth === 0 && isSepByte(c)) {
358
- let q = p;
359
- while (q < inner.length && isSepByte(inner[q])) {
360
- q++;
201
+ /** Whether an op acts on an EXPRESSION (a function) rather than plain numbers
202
+ * — declared by the op itself at registration (the `expression` trait), so
203
+ * there is no side table of names here. */
204
+ isExpressionOp(name) {
205
+ return this.registry.get(name)?.expression === true;
206
+ }
207
+ /** Scan a byte span for operand (numeric) and operator (symbolic) spans. A
208
+ * pure, deterministic left-to-right lexer: at each position take a numeral if
209
+ * one starts there, else the longest symbolic operator form, else skip one
210
+ * byte. Numerals and operator symbols are disjoint character classes, so the
211
+ * two never overlap. */
212
+ scan(bytes) {
213
+ const operands = [];
214
+ const operators = [];
215
+ const n = bytes.length;
216
+ let i = 0;
217
+ while (i < n) {
218
+ const c = bytes[i];
219
+ // A numeral: a run of digits, with at most one interior decimal point that
220
+ // is itself flanked by digits (so a trailing "." is left to the operators
221
+ // / skip path, not swallowed).
222
+ if (isDigitByte(c) || (c === DOT && i + 1 < n && isDigitByte(bytes[i + 1]))) {
223
+ let j = i;
224
+ let seenDot = false;
225
+ while (j < n) {
226
+ if (isDigitByte(bytes[j])) {
227
+ j++;
228
+ }
229
+ else if (bytes[j] === DOT && !seenDot && j + 1 < n &&
230
+ isDigitByte(bytes[j + 1])) {
231
+ seenDot = true;
232
+ j++;
233
+ }
234
+ else
235
+ break;
236
+ }
237
+ operands.push({ i, j, value: parseValue(bytes.subarray(i, j)) });
238
+ i = j;
239
+ continue;
240
+ }
241
+ // The longest symbolic operator form starting here.
242
+ const op = this.matchSymbolic(bytes, i);
243
+ if (op) {
244
+ operators.push({
245
+ i,
246
+ j: i + op.bytes.length,
247
+ name: op.name,
248
+ arity: op.arity,
249
+ });
250
+ i += op.bytes.length;
251
+ continue;
252
+ }
253
+ i++;
361
254
  }
362
- elements.push(inner.subarray(start, p));
363
- start = q;
364
- p = q;
365
- continue;
366
- }
367
- p++;
255
+ return { operands, operators };
368
256
  }
369
- elements.push(inner.subarray(start));
370
- return elements.map((e) => this.recogniseValue(e));
371
- }
372
- /** Recognise a bare SEQUENCE — ≥2 numeric operands separated by a consistent
373
- * connective, with no leftover material at the edges — or null when the span
374
- * is not such a sequence. The operands are exactly the ones {@link scan}
375
- * finds; the bytes between consecutive operands are the separator, which need
376
- * only be the SAME throughout (a space, a comma, " and ", …) — its spelling is
377
- * not constrained, so no separator is privileged. */
378
- recogniseSequence(s) {
379
- const { operands } = this.scan(s);
380
- if (operands.length < 2) {
381
- return null;
257
+ matchSymbolic(bytes, at) {
258
+ for (const f of this.symbolicForms) {
259
+ const len = f.bytes.length;
260
+ if (at + len > bytes.length)
261
+ continue;
262
+ let ok = true;
263
+ for (let k = 0; k < len; k++) {
264
+ if (bytes[at + k] !== f.bytes[k]) {
265
+ ok = false;
266
+ break;
267
+ }
268
+ }
269
+ if (ok)
270
+ return f;
271
+ }
272
+ return null;
382
273
  }
383
- // No leftover before the first operand or after the last (a clean sequence).
384
- if (trimStart(s) !== operands[0].i) {
385
- return null;
274
+ /** Read a byte span into a {@link Value} by recognising its STRUCTURE — the
275
+ * byte⇄Value boundary the host computes across. The kernel itself only knows
276
+ * the irreducible scalar floor (a numeral's digits → a quantity, via {@link
277
+ * parseValue}); recognising whether a span is one quantity or a LIST of them is
278
+ * layered here, over {@link scan}, so no caller re-implements it.
279
+ *
280
+ * A LIST is a run of element values joined by a CONSISTENT separator. No
281
+ * spelling is privileged: two complementary readings cover the forms a list
282
+ * takes —
283
+ *
284
+ * • CONTAINER — an explicit `[ … ]` group delimits a list whose elements may
285
+ * be anything (symbols, nested groups, mixed): split its interior at the TOP
286
+ * level (bracket-depth aware) on separator runs, each element recognised in
287
+ * turn (so nesting and heterogeneity recurse for free). This is also the
288
+ * codec's canonical OUTPUT spelling, so a computed list feeds straight back
289
+ * in as an operand.
290
+ * • SEQUENCE — a bare run of ≥2 numeric operands with a consistent connective
291
+ * between them (`1 2 3`, `1, 2, 3`, `1 and 2 and 3`): the operands {@link
292
+ * scan} finds are the elements, and whatever sits between them — a space, a
293
+ * comma, " and " — is the separator, accepted as long as it is the same
294
+ * throughout and the run has no leftover edges.
295
+ *
296
+ * Anything else is a SCALAR ({@link parseValue}): a numeral, or an opaque
297
+ * symbol of any modality (a learnt form, an operator name a higher-order op
298
+ * will resolve, a single word). */
299
+ recogniseValue(bytes) {
300
+ const s = bytes.subarray(trimStart(bytes), trimEnd(bytes));
301
+ // CONTAINER: a balanced [ … ] group spanning the whole (trimmed) span.
302
+ if (s.length >= 2 && s[0] === OPEN && matchBracket(s, 0) === s.length - 1) {
303
+ return nd(this.recogniseContainer(s.subarray(1, s.length - 1)));
304
+ }
305
+ // SEQUENCE: ≥2 numeric operands with a consistent connective between them.
306
+ const seq = this.recogniseSequence(s);
307
+ if (seq !== null)
308
+ return nd(seq);
309
+ // SCALAR floor.
310
+ return parseValue(s);
386
311
  }
387
- if (trimEnd(s) !== operands[operands.length - 1].j) {
388
- return null;
312
+ /** Split a CONTAINER's interior into its element values — top-level (bracket-
313
+ * depth aware) spans divided by separator RUNS (maximal runs of {@link
314
+ * isSepByte}). The brackets are the explicit delimiter, so the divider's
315
+ * spelling is not privileged (`[1,2,3]`, `[1, 2, 3]`, `[1 2 3]` are the same
316
+ * list). Each element is recognised in turn, so a nested `[ … ]` element
317
+ * recurses and a symbol element stays opaque. An empty interior is `[]`. */
318
+ recogniseContainer(inner) {
319
+ if (trimEnd(inner) <= trimStart(inner))
320
+ return []; // empty / all space → []
321
+ const elements = [];
322
+ let depth = 0;
323
+ let start = 0;
324
+ let p = 0;
325
+ while (p < inner.length) {
326
+ const c = inner[p];
327
+ if (c === OPEN)
328
+ depth++;
329
+ else if (c === CLOSE)
330
+ depth--;
331
+ if (depth === 0 && isSepByte(c)) {
332
+ let q = p;
333
+ while (q < inner.length && isSepByte(inner[q]))
334
+ q++;
335
+ elements.push(inner.subarray(start, p));
336
+ start = q;
337
+ p = q;
338
+ continue;
339
+ }
340
+ p++;
341
+ }
342
+ elements.push(inner.subarray(start));
343
+ return elements.map((e) => this.recogniseValue(e));
389
344
  }
390
- let sep = null;
391
- for (let k = 0; k + 1 < operands.length; k++) {
392
- const gap = latin1(s.subarray(operands[k].j, operands[k + 1].i));
393
- if (gap.length === 0) {
394
- return null; // operands must be separated
395
- }
396
- if (sep === null) {
397
- sep = gap;
398
- } else if (gap !== sep) {
399
- return null; // inconsistent → not one sequence
400
- }
345
+ /** Recognise a bare SEQUENCE — ≥2 numeric operands separated by a consistent
346
+ * connective, with no leftover material at the edges or null when the span
347
+ * is not such a sequence. The operands are exactly the ones {@link scan}
348
+ * finds; the bytes between consecutive operands are the separator, which need
349
+ * only be the SAME throughout (a space, a comma, " and ", …) — its spelling is
350
+ * not constrained, so no separator is privileged. */
351
+ recogniseSequence(s) {
352
+ const { operands } = this.scan(s);
353
+ if (operands.length < 2)
354
+ return null;
355
+ // No leftover before the first operand or after the last (a clean sequence).
356
+ if (trimStart(s) !== operands[0].i)
357
+ return null;
358
+ if (trimEnd(s) !== operands[operands.length - 1].j)
359
+ return null;
360
+ let sep = null;
361
+ for (let k = 0; k + 1 < operands.length; k++) {
362
+ const gap = latin1(s.subarray(operands[k].j, operands[k + 1].i));
363
+ if (gap.length === 0)
364
+ return null; // operands must be separated
365
+ if (sep === null)
366
+ sep = gap;
367
+ else if (gap !== sep)
368
+ return null; // inconsistent → not one sequence
369
+ }
370
+ return operands.map((o) => this.recogniseValue(s.subarray(o.i, o.j)));
401
371
  }
402
- return operands.map((o) => this.recogniseValue(s.subarray(o.i, o.j)));
403
- }
404
- /** Apply an op to operand values, with the given pre-resolved resonance.
405
- * Returns the result value, or null if the op is unknown or the computation
406
- * throws (e.g. a symbol fed to arithmetic) — the caller treats null as "this
407
- * rule does not fire", never as a wrong answer. */
408
- apply(name, operands, resonance = NO_RESONANCE) {
409
- if (!this.registry.has(name)) {
410
- return null;
372
+ /** Apply an op to operand values, with the given pre-resolved resonance.
373
+ * Returns the result value, or null if the op is unknown or the computation
374
+ * throws (e.g. a symbol fed to arithmetic) the caller treats null as "this
375
+ * rule does not fire", never as a wrong answer. */
376
+ apply(name, operands, resonance = NO_RESONANCE) {
377
+ if (!this.registry.has(name))
378
+ return null;
379
+ try {
380
+ const ctx = this.registry.context(resonance, this.rt, this.makeEvalExpr());
381
+ return ctx.apply(name, operands);
382
+ }
383
+ catch (err) {
384
+ // The "rule does not fire" contract — but never invisibly: the counter
385
+ // and lastApplyError let a debugger distinguish a routine decline from
386
+ // a kernel bug that would otherwise read as "computation unknown".
387
+ this.applyCaught++;
388
+ this.lastApplyError = err;
389
+ return null;
390
+ }
411
391
  }
412
- try {
413
- const ctx = this.registry.context(
414
- resonance,
415
- this.rt,
416
- this.makeEvalExpr(),
417
- );
418
- return ctx.apply(name, operands);
419
- } catch (err) {
420
- // The "rule does not fire" contract — but never invisibly: the counter
421
- // and lastApplyError let a debugger distinguish a routine decline from
422
- // a kernel bug that would otherwise read as "computation unknown".
423
- this.applyCaught++;
424
- this.lastApplyError = err;
425
- return null;
392
+ /** The expression evaluator handed to op contexts, bound to the cached {@link
393
+ * grammar}: the numerical layer's integrand is evaluated by a recursive
394
+ * application of the same derived-from-nand arithmetic. */
395
+ makeEvalExpr() {
396
+ return (bytes, variable, at) => this.grammar.eval(this.decoder.decode(bytes), variable, at);
426
397
  }
427
- }
428
- /** The expression evaluator handed to op contexts, bound to the cached {@link
429
- * grammar}: the numerical layer's integrand is evaluated by a recursive
430
- * application of the same derived-from-nand arithmetic. */
431
- makeEvalExpr() {
432
- return (bytes, variable, at) =>
433
- this.grammar.eval(this.decoder.decode(bytes), variable, at);
434
- }
435
- /** Decode operand byte spans, apply `name`, and encode the result — the pure
436
- * computation the graph rule materialises into an output span. Returns null
437
- * on any failure (unknown op, undecodable operand, thrown computation), so a
438
- * rule that cannot compute simply does not fire. The result bytes are
439
- * canonical (see {@link decimalCodec}), so two derivations of the same value
440
- * agree byte-for-byte — required for the search's chart memoization. */
441
- applyBytes(name, operandBytes, resonance = NO_RESONANCE) {
442
- return this.applyBytesTyped(name, operandBytes, () => false, resonance);
443
- }
444
- /** Apply `name` to operand VALUES (a scalar, or an `nd` recognised by {@link
445
- * recogniseValue}), and encode the result canonically. This is the entry
446
- * point for computation over STRUCTURE: the scalar kernel and the nd kernel
447
- * both consume Values, so once a span is recognised the computation is the
448
- * same whether the operand came in as a number or a list. Returns null on any
449
- * failure (unknown op, thrown computation), the same "this rule does not fire"
450
- * contract as {@link applyBytes}. */
451
- applyValues(name, operands, resonance = NO_RESONANCE) {
452
- const result = this.apply(name, operands, resonance);
453
- if (result === null) {
454
- return null;
398
+ /** Decode operand byte spans, apply `name`, and encode the result — the pure
399
+ * computation the graph rule materialises into an output span. Returns null
400
+ * on any failure (unknown op, undecodable operand, thrown computation), so a
401
+ * rule that cannot compute simply does not fire. The result bytes are
402
+ * canonical (see {@link decimalCodec}), so two derivations of the same value
403
+ * agree byte-for-byte required for the search's chart memoization. */
404
+ applyBytes(name, operandBytes, resonance = NO_RESONANCE) {
405
+ return this.applyBytesTyped(name, operandBytes, () => false, resonance);
455
406
  }
456
- return this.codec.encode(result);
457
- }
458
- /** Like {@link applyBytes}, but `asSymbol(idx)` marks operand positions that
459
- * must be kept as an opaque SYMBOL (not parsed as a number) the convention
460
- * the numerical layer needs, where operand 0 is an EXPRESSION (a function's
461
- * bytes) the kernel samples via the expression evaluator, not a numeral. */
462
- applyBytesTyped(name, operandBytes, asSymbol, resonance = NO_RESONANCE) {
463
- const operands = [];
464
- for (let i = 0; i < operandBytes.length; i++) {
465
- const b = operandBytes[i];
466
- const v = asSymbol(i)
467
- ? { domain: "symbol", bytes: b }
468
- : this.codec.decode(b);
469
- if (v === null) {
470
- return null;
471
- }
472
- operands.push(v);
407
+ /** Apply `name` to operand VALUES (a scalar, or an `nd` recognised by {@link
408
+ * recogniseValue}), and encode the result canonically. This is the entry
409
+ * point for computation over STRUCTURE: the scalar kernel and the nd kernel
410
+ * both consume Values, so once a span is recognised the computation is the
411
+ * same whether the operand came in as a number or a list. Returns null on any
412
+ * failure (unknown op, thrown computation), the same "this rule does not fire"
413
+ * contract as {@link applyBytes}. */
414
+ applyValues(name, operands, resonance = NO_RESONANCE) {
415
+ const result = this.apply(name, operands, resonance);
416
+ if (result === null)
417
+ return null;
418
+ return this.codec.encode(result);
473
419
  }
474
- const result = this.apply(name, operands, resonance);
475
- if (result === null) {
476
- return null;
420
+ /** Like {@link applyBytes}, but `asSymbol(idx)` marks operand positions that
421
+ * must be kept as an opaque SYMBOL (not parsed as a number) — the convention
422
+ * the numerical layer needs, where operand 0 is an EXPRESSION (a function's
423
+ * bytes) the kernel samples via the expression evaluator, not a numeral. */
424
+ applyBytesTyped(name, operandBytes, asSymbol, resonance = NO_RESONANCE) {
425
+ const operands = [];
426
+ for (let i = 0; i < operandBytes.length; i++) {
427
+ const b = operandBytes[i];
428
+ const v = asSymbol(i)
429
+ ? { domain: "symbol", bytes: b }
430
+ : this.codec.decode(b);
431
+ if (v === null)
432
+ return null;
433
+ operands.push(v);
434
+ }
435
+ const result = this.apply(name, operands, resonance);
436
+ if (result === null)
437
+ return null;
438
+ return this.codec.encode(result);
477
439
  }
478
- return this.codec.encode(result);
479
- }
480
440
  }