solve-engine 1.0.0-beta.2 → 1.0.0-beta.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 (171) hide show
  1. package/dist/{Lexer-D9l4Yrk2.d.ts → Lexer-Cfm79Dak.d.cts} +18 -2
  2. package/dist/{Lexer-BJdhlnej.d.cts → Lexer-W9MBOP0V.d.ts} +18 -2
  3. package/dist/{PackageRegistry-DaH4aIKP.d.ts → PackageRegistry-CjDt-Jy_.d.ts} +60 -4
  4. package/dist/{PackageRegistry-D-Tv_7ca.d.cts → PackageRegistry-pHtKythi.d.cts} +60 -4
  5. package/dist/{Parselet-CK8cQu2a.d.cts → Parselet-Cu0bLSis.d.cts} +1 -1
  6. package/dist/{Parselet-ConOIdRO.d.ts → Parselet-DEdF9I7n.d.ts} +1 -1
  7. package/dist/{Token-0jpvUdPY.d.cts → Token-BzG5G4ja.d.cts} +44 -0
  8. package/dist/{Token-0jpvUdPY.d.ts → Token-BzG5G4ja.d.ts} +44 -0
  9. package/dist/TokenNormalizer-DGVa24Q-.d.cts +377 -0
  10. package/dist/TokenNormalizer-t_GotBxr.d.ts +377 -0
  11. package/dist/{chunk-HWSZJQCI.js → chunk-2CS6OMZK.js} +37 -15
  12. package/dist/chunk-2CS6OMZK.js.map +1 -0
  13. package/dist/{chunk-XJCMXB2E.cjs → chunk-2MV4HBKC.cjs} +187 -61
  14. package/dist/chunk-2MV4HBKC.cjs.map +1 -0
  15. package/dist/{chunk-J73SJHR3.cjs → chunk-2NPS5DQ3.cjs} +558 -341
  16. package/dist/chunk-2NPS5DQ3.cjs.map +1 -0
  17. package/dist/{chunk-QWY3VEZN.js → chunk-3MNKQZ77.js} +65 -12
  18. package/dist/chunk-3MNKQZ77.js.map +1 -0
  19. package/dist/{chunk-RZCWSXTA.cjs → chunk-43G3JT2E.cjs} +1491 -218
  20. package/dist/chunk-43G3JT2E.cjs.map +1 -0
  21. package/dist/{chunk-WQTTOGXC.cjs → chunk-4AVD7NZW.cjs} +4 -4
  22. package/dist/{chunk-WQTTOGXC.cjs.map → chunk-4AVD7NZW.cjs.map} +1 -1
  23. package/dist/{chunk-HTXVVJRA.cjs → chunk-4CVLFLOB.cjs} +112 -2
  24. package/dist/chunk-4CVLFLOB.cjs.map +1 -0
  25. package/dist/{chunk-5YEMOYSE.js → chunk-6GCKCWLB.js} +9 -3
  26. package/dist/chunk-6GCKCWLB.js.map +1 -0
  27. package/dist/{chunk-EBSPLUW4.cjs → chunk-AHCLWAM5.cjs} +28 -10
  28. package/dist/chunk-AHCLWAM5.cjs.map +1 -0
  29. package/dist/{chunk-KVILKGMS.js → chunk-AJA6LUI7.js} +36 -2
  30. package/dist/chunk-AJA6LUI7.js.map +1 -0
  31. package/dist/{chunk-EHAHVROS.cjs → chunk-CLVQBF5C.cjs} +5 -5
  32. package/dist/{chunk-EHAHVROS.cjs.map → chunk-CLVQBF5C.cjs.map} +1 -1
  33. package/dist/{chunk-34RRD7PC.js → chunk-CYFK5SY2.js} +111 -3
  34. package/dist/chunk-CYFK5SY2.js.map +1 -0
  35. package/dist/{chunk-M5LX5AOO.js → chunk-DDQVZGLO.js} +25 -12
  36. package/dist/chunk-DDQVZGLO.js.map +1 -0
  37. package/dist/{chunk-LR7YASZF.cjs → chunk-EAAHVJ4P.cjs} +3 -3
  38. package/dist/chunk-EAAHVJ4P.cjs.map +1 -0
  39. package/dist/{chunk-3PPFLFH4.js → chunk-FIFQPKBA.js} +1417 -144
  40. package/dist/chunk-FIFQPKBA.js.map +1 -0
  41. package/dist/{chunk-RIN643A3.js → chunk-GPPLSM2Z.js} +61 -2
  42. package/dist/chunk-GPPLSM2Z.js.map +1 -0
  43. package/dist/{chunk-4QADQTWS.js → chunk-KHJUSHFU.js} +231 -14
  44. package/dist/chunk-KHJUSHFU.js.map +1 -0
  45. package/dist/{chunk-NMD5VRN4.cjs → chunk-M4F66R4O.cjs} +74 -73
  46. package/dist/chunk-M4F66R4O.cjs.map +1 -0
  47. package/dist/{chunk-6NTVRDQV.cjs → chunk-M4Q47KHF.cjs} +126 -73
  48. package/dist/chunk-M4Q47KHF.cjs.map +1 -0
  49. package/dist/{chunk-LIPPNDBE.js → chunk-M5E34VG5.js} +3 -3
  50. package/dist/{chunk-LIPPNDBE.js.map → chunk-M5E34VG5.js.map} +1 -1
  51. package/dist/{chunk-53B6KDDJ.cjs → chunk-MBNQVDVC.cjs} +38 -3
  52. package/dist/{chunk-53B6KDDJ.cjs.map → chunk-MBNQVDVC.cjs.map} +1 -1
  53. package/dist/{chunk-SDGRK7EP.js → chunk-MEOHSQEH.js} +6 -5
  54. package/dist/chunk-MEOHSQEH.js.map +1 -0
  55. package/dist/{chunk-NMCRQP3Z.cjs → chunk-MTX2KVU7.cjs} +71 -70
  56. package/dist/chunk-MTX2KVU7.cjs.map +1 -0
  57. package/dist/{chunk-QNJ4ACRT.cjs → chunk-NJTXJ5AG.cjs} +37 -15
  58. package/dist/chunk-NJTXJ5AG.cjs.map +1 -0
  59. package/dist/{chunk-C4XZV6E7.cjs → chunk-NZFKROS7.cjs} +26 -20
  60. package/dist/chunk-NZFKROS7.cjs.map +1 -0
  61. package/dist/{chunk-64W6GLLZ.js → chunk-OFXOTECC.js} +24 -6
  62. package/dist/chunk-OFXOTECC.js.map +1 -0
  63. package/dist/{chunk-3YNVWKR2.cjs → chunk-OUHT5Z36.cjs} +28 -5
  64. package/dist/chunk-OUHT5Z36.cjs.map +1 -0
  65. package/dist/{chunk-EIGTWK5N.js → chunk-OWRHDUJD.js} +3 -3
  66. package/dist/chunk-OWRHDUJD.js.map +1 -0
  67. package/dist/{chunk-4MG4XKO2.js → chunk-PCPX42KL.js} +38 -3
  68. package/dist/{chunk-4MG4XKO2.js.map → chunk-PCPX42KL.js.map} +1 -1
  69. package/dist/{chunk-GW32KPCU.cjs → chunk-R24DI24X.cjs} +61 -2
  70. package/dist/chunk-R24DI24X.cjs.map +1 -0
  71. package/dist/{chunk-XVWCOTR6.js → chunk-SFQWJMKT.js} +7 -6
  72. package/dist/chunk-SFQWJMKT.js.map +1 -0
  73. package/dist/{chunk-6BKTCEUP.cjs → chunk-TY3TLZAW.cjs} +36 -2
  74. package/dist/chunk-TY3TLZAW.cjs.map +1 -0
  75. package/dist/{chunk-DM3LMRBC.js → chunk-UKPGSAZW.js} +187 -61
  76. package/dist/chunk-UKPGSAZW.js.map +1 -0
  77. package/dist/{chunk-NH2O2AUR.js → chunk-WC5FFSHB.js} +26 -4
  78. package/dist/chunk-WC5FFSHB.js.map +1 -0
  79. package/dist/{chunk-CLL7RUQV.cjs → chunk-WWOLFXTX.cjs} +40 -18
  80. package/dist/chunk-WWOLFXTX.cjs.map +1 -0
  81. package/dist/{chunk-JBSYC7BB.cjs → chunk-X2BWAZAU.cjs} +66 -53
  82. package/dist/chunk-X2BWAZAU.cjs.map +1 -0
  83. package/dist/{chunk-GOLDJNMZ.js → chunk-XBTEO4OB.js} +28 -5
  84. package/dist/chunk-XBTEO4OB.js.map +1 -0
  85. package/dist/{chunk-EPOXXJBK.js → chunk-YU2CUNFO.js} +3 -3
  86. package/dist/{chunk-EPOXXJBK.js.map → chunk-YU2CUNFO.js.map} +1 -1
  87. package/dist/constants.cjs +4 -4
  88. package/dist/constants.js +1 -1
  89. package/dist/engine.cjs +29 -30
  90. package/dist/engine.d.cts +6 -6
  91. package/dist/engine.d.ts +6 -6
  92. package/dist/engine.js +19 -20
  93. package/dist/format.cjs +7 -8
  94. package/dist/format.cjs.map +1 -1
  95. package/dist/format.js +2 -3
  96. package/dist/format.js.map +1 -1
  97. package/dist/index.cjs +28 -29
  98. package/dist/index.cjs.map +1 -1
  99. package/dist/index.d.cts +6 -6
  100. package/dist/index.d.ts +6 -6
  101. package/dist/index.js +20 -21
  102. package/dist/index.js.map +1 -1
  103. package/dist/language.cjs +74 -12
  104. package/dist/language.cjs.map +1 -1
  105. package/dist/language.d.cts +6 -6
  106. package/dist/language.d.ts +6 -6
  107. package/dist/language.js +68 -6
  108. package/dist/language.js.map +1 -1
  109. package/dist/lexer.cjs +19 -19
  110. package/dist/lexer.d.cts +3 -3
  111. package/dist/lexer.d.ts +3 -3
  112. package/dist/lexer.js +7 -7
  113. package/dist/normalizer.cjs +12 -12
  114. package/dist/normalizer.d.cts +4 -210
  115. package/dist/normalizer.d.ts +4 -210
  116. package/dist/normalizer.js +6 -6
  117. package/dist/packages.cjs +36 -37
  118. package/dist/packages.d.cts +5 -47
  119. package/dist/packages.d.ts +5 -47
  120. package/dist/packages.js +13 -14
  121. package/dist/parser.cjs +12 -12
  122. package/dist/parser.d.cts +3 -2
  123. package/dist/parser.d.ts +3 -2
  124. package/dist/parser.js +4 -4
  125. package/dist/resolvers.d.cts +1 -1
  126. package/dist/resolvers.d.ts +1 -1
  127. package/dist/uom.cjs +14 -14
  128. package/dist/uom.d.cts +12 -4
  129. package/dist/uom.d.ts +12 -4
  130. package/dist/uom.js +4 -4
  131. package/dist/vm.cjs +17 -17
  132. package/dist/vm.js +6 -6
  133. package/package.json +2 -1
  134. package/dist/NormalizerRule-BrVoVjmP.d.cts +0 -163
  135. package/dist/NormalizerRule-CEjf1FyD.d.ts +0 -163
  136. package/dist/chunk-34RRD7PC.js.map +0 -1
  137. package/dist/chunk-3PPFLFH4.js.map +0 -1
  138. package/dist/chunk-3YNVWKR2.cjs.map +0 -1
  139. package/dist/chunk-4QADQTWS.js.map +0 -1
  140. package/dist/chunk-5YEMOYSE.js.map +0 -1
  141. package/dist/chunk-64W6GLLZ.js.map +0 -1
  142. package/dist/chunk-6BKTCEUP.cjs.map +0 -1
  143. package/dist/chunk-6NTVRDQV.cjs.map +0 -1
  144. package/dist/chunk-C4XZV6E7.cjs.map +0 -1
  145. package/dist/chunk-CLL7RUQV.cjs.map +0 -1
  146. package/dist/chunk-DM3LMRBC.js.map +0 -1
  147. package/dist/chunk-EBSPLUW4.cjs.map +0 -1
  148. package/dist/chunk-EIGTWK5N.js.map +0 -1
  149. package/dist/chunk-GOLDJNMZ.js.map +0 -1
  150. package/dist/chunk-GW32KPCU.cjs.map +0 -1
  151. package/dist/chunk-HTXVVJRA.cjs.map +0 -1
  152. package/dist/chunk-HWSZJQCI.js.map +0 -1
  153. package/dist/chunk-J73SJHR3.cjs.map +0 -1
  154. package/dist/chunk-JBSYC7BB.cjs.map +0 -1
  155. package/dist/chunk-KVILKGMS.js.map +0 -1
  156. package/dist/chunk-LR7YASZF.cjs.map +0 -1
  157. package/dist/chunk-M5LX5AOO.js.map +0 -1
  158. package/dist/chunk-NH2O2AUR.js.map +0 -1
  159. package/dist/chunk-NMCRQP3Z.cjs.map +0 -1
  160. package/dist/chunk-NMD5VRN4.cjs.map +0 -1
  161. package/dist/chunk-OT6OJY7C.cjs +0 -114
  162. package/dist/chunk-OT6OJY7C.cjs.map +0 -1
  163. package/dist/chunk-QNJ4ACRT.cjs.map +0 -1
  164. package/dist/chunk-QWY3VEZN.js.map +0 -1
  165. package/dist/chunk-RFYD5TJE.js +0 -111
  166. package/dist/chunk-RFYD5TJE.js.map +0 -1
  167. package/dist/chunk-RIN643A3.js.map +0 -1
  168. package/dist/chunk-RZCWSXTA.cjs.map +0 -1
  169. package/dist/chunk-SDGRK7EP.js.map +0 -1
  170. package/dist/chunk-XJCMXB2E.cjs.map +0 -1
  171. package/dist/chunk-XVWCOTR6.js.map +0 -1
@@ -0,0 +1,377 @@
1
+ import { T as Token } from './Token-BzG5G4ja.cjs';
2
+
3
+ /**
4
+ * NormalizerRule, pluggable token normalization rule for the
5
+ * TokenNormalizer post-lexer pass.
6
+ *
7
+ * ## Purpose
8
+ * After the ExpressionLexer produces raw tokens (numbers, identifiers,
9
+ * operators, etc.), the TokenNormalizer applies domain-specific rules
10
+ * to transform the token stream before parsing. This keeps the lexer
11
+ * focused on single-token production and moves multi-token pattern
12
+ * matching into a dedicated normalization layer.
13
+ *
14
+ * ## How rules work
15
+ * Rules are applied in priority order (highest first). At each token
16
+ * position, the normalizer tries every rule in priority order until one
17
+ * matches. Matched tokens are consumed and replaced; unmatched tokens
18
+ * pass through unchanged.
19
+ *
20
+ * ## What rules can do
21
+ * - **Phrase fusion**: Merge consecutive words into compound tokens
22
+ * (e.g., `"to" "the" "power" "of"` → `CARET`)
23
+ * - **Implicit operators**: Insert missing operators between tokens
24
+ * (e.g., `NUMBER IDENT` → `NUMBER STAR IDENT`)
25
+ * - **Domain transformations**: Coalesce item names, currency pairs, etc.
26
+ *
27
+ * ## Why a separate file?
28
+ * This is a duplicate-free copy of the interface defined in
29
+ * TokenNormalizer.ts. Storing it in a separate file avoids circular
30
+ * imports, TokenNormalizer imports NormalizerRule, and rule factories
31
+ * import TokenNormalizer's `createFusedToken`.
32
+ *
33
+ * @module NormalizerRule
34
+ */
35
+
36
+ /**
37
+ * Result of a successful rule match attempt against the token stream.
38
+ *
39
+ * When a {@link NormalizerRule.match} function finds a pattern at the
40
+ * current position, it returns a NormalizerMatch describing how many
41
+ * tokens to consume and what to replace them with.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * // The phrase "to the power of" (5 tokens) becomes a single CARET token
46
+ * const match: NormalizerMatch = {
47
+ * consumed: 5,
48
+ * replacement: [caretToken],
49
+ * };
50
+ * ```
51
+ */
52
+ interface NormalizerMatch {
53
+ /**
54
+ * Number of tokens consumed from the stream at the match position.
55
+ * Must be ≥ 1, a match always advances the cursor.
56
+ */
57
+ consumed: number;
58
+ /**
59
+ * Replacement tokens to insert at the match position.
60
+ * May be empty (deletion), a single token (fusion), or multiple
61
+ * tokens (expansion/splitting).
62
+ */
63
+ replacement: Token[];
64
+ /**
65
+ * Human-readable rule name for diagnostic fusion tracking.
66
+ * When set, the normalizer uses this instead of the rule's `name`
67
+ * in {@link TokenFusion} records. Used by {@link PhraseTrie} to
68
+ * report which specific phrase matched (e.g., "phrase:to the power of").
69
+ */
70
+ ruleName?: string;
71
+ }
72
+ /**
73
+ * A pluggable normalization rule registered with the TokenNormalizer.
74
+ *
75
+ * Each rule has a {@link name}, {@link priority}, and {@link match} function.
76
+ * The match function receives the current token stream and a position,
77
+ * and returns a {@link NormalizerMatch} on success or `null` on failure.
78
+ *
79
+ * ## Priority ordering
80
+ * Higher priority rules are tried first at each position. This allows
81
+ * long phrases (priority 100, e.g. "to the power of") to match before
82
+ * shorter fragments (priority 80, e.g. "power of").
83
+ *
84
+ * ## Match contract
85
+ * - Must be pure (no side effects, no mutation of input tokens)
86
+ * - Must return `null` for any position that doesn't match
87
+ * - Consumed tokens must be consecutive starting at `pos`
88
+ * - Replacement tokens must be valid for downstream parsing
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * // A phrase fusion rule that converts "to the power of" into CARET
93
+ * const phraseRule: NormalizerRule = {
94
+ * name: 'phrase:to the power of',
95
+ * priority: 100,
96
+ * match: (tokens, pos) => {
97
+ * if (pos + 4 > tokens.length) return null;
98
+ * const phrase = tokens.slice(pos, pos + 5)
99
+ * .map(t => t.value.toLowerCase()).join(' ');
100
+ * if (phrase === 'to the power of') {
101
+ * return {
102
+ * consumed: 5,
103
+ * replacement: [createFusedToken('CARET', 'to the power of', tokens.slice(pos, pos + 5))],
104
+ * };
105
+ * }
106
+ * return null;
107
+ * },
108
+ * };
109
+ * ```
110
+ */
111
+ interface NormalizerRule {
112
+ /**
113
+ * Human-readable name for debugging and diagnostic display.
114
+ * Convention: `"category:description"`, e.g. `"phrase:to the power of"`.
115
+ */
116
+ readonly name: string;
117
+ /**
118
+ * Priority for ordering rules. Higher values are tried first.
119
+ * Recommended ranges:
120
+ * - 100: Long multi-word phrase fusion (e.g., "to the power of")
121
+ * - 80: Short phrase fusion (e.g., "power of", "times by")
122
+ * - 50: Implicit operator insertion (e.g., implicit multiply)
123
+ * - 20: Domain-specific transformations
124
+ */
125
+ readonly priority: number;
126
+ /**
127
+ * Attempt to match a pattern starting at position `pos` in the token stream.
128
+ *
129
+ * @param tokens - The current token stream (may be partially normalized from prior passes)
130
+ * @param pos - The current position to attempt matching from
131
+ * @returns A {@link NormalizerMatch} if the pattern is found, or `null` if no match
132
+ */
133
+ match(tokens: Token[], pos: number): NormalizerMatch | null;
134
+ }
135
+ /**
136
+ * Record of a token fusion event performed by the normalizer.
137
+ *
138
+ * When a rule merges multiple source tokens into fewer replacement tokens,
139
+ * the normalizer fires a {@link NormalizerOptions.onFusion | fusion callback}
140
+ * with this record. The playground uses these records to render the
141
+ * fusion detail table showing exactly which tokens were merged and by
142
+ * which rule.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * // "to" "the" "power" "of" fused into CARET "^"
147
+ * const fusion: TokenFusion = {
148
+ * rule: "phrase:to the power of",
149
+ * sourceTokens: [toToken, theToken, powerToken, ofToken],
150
+ * fusedToken: caretToken,
151
+ * };
152
+ * ```
153
+ */
154
+ interface TokenFusion {
155
+ /** The name of the rule that triggered this fusion (e.g., "phrase:to the power of") */
156
+ rule: string;
157
+ /** The original tokens before fusion, always ≥ 2 tokens */
158
+ sourceTokens: Token[];
159
+ /** The resulting fused token with its new type and combined value */
160
+ fusedToken: Token;
161
+ }
162
+
163
+ /**
164
+ * TokenNormalizer, post-lexer token normalization pass.
165
+ *
166
+ * ## Purpose
167
+ * Applies domain-specific {@link NormalizerRule | NormalizerRules} to the raw
168
+ * token stream produced by the {@link ExpressionLexer}. This keeps the lexer
169
+ * slim and focused on single-token production, while multi-token pattern
170
+ * matching (phrases, implicit operators, domain merges) lives here.
171
+ *
172
+ * ## What rules can do
173
+ * - **Phrase fusion**: Merge consecutive words into compound tokens
174
+ * (e.g., `IDENT + ... + IDENT` → `CARET`)
175
+ * - **Implicit operator insertion**: Insert missing operators between tokens
176
+ * (e.g., `NUMBER IDENT` → `NUMBER STAR IDENT`)
177
+ * - **Domain-specific transformations**: Coalesce item names, currency pairs,
178
+ * percentage syntax, etc.
179
+ *
180
+ * ## Architecture
181
+ * Providers register NormalizerRules alongside Parselets and OpCode handlers
182
+ * via {@link IEnginePackage.normalizerRules}. The normalizer applies them
183
+ * greedily left-to-right in multiple passes with safety limits.
184
+ *
185
+ * @module TokenNormalizer
186
+ */
187
+
188
+ /**
189
+ * Configuration options for the normalization pass.
190
+ *
191
+ * These control safety limits and diagnostic callbacks. The defaults
192
+ * are chosen to be generous enough for any realistic expression while
193
+ * preventing runaway token expansion from recursive rules.
194
+ */
195
+ interface NormalizerOptions {
196
+ /**
197
+ * Maximum number of full passes over the token stream before bailing out.
198
+ * Prevents infinite loops from recursive rule chains.
199
+ * @default 100
200
+ */
201
+ maxPasses?: number;
202
+ /**
203
+ * Maximum number of tokens allowed after normalization.
204
+ * If exceeded, an Error is thrown rather than passing a bloated stream
205
+ * to the parser.
206
+ * @default 10000
207
+ */
208
+ maxTokens?: number;
209
+ /**
210
+ * Callback invoked for each fusion event during normalization.
211
+ * Used by diagnostic mode to populate {@link NormalizerOutput.fusions}.
212
+ * When `undefined`, fusions are still tracked internally but no callbacks fire.
213
+ */
214
+ onFusion?: (fusion: TokenFusion) => void;
215
+ }
216
+ /**
217
+ * Creates a new normalized token from fused source tokens.
218
+ *
219
+ * The fused token inherits position information (offset, line, column)
220
+ * from the first source token, which preserves source-map accuracy
221
+ * for error messages and diagnostic highlighting.
222
+ *
223
+ * It also records where the source text ENDS, on `sourceEnd`. The start alone
224
+ * is not enough to describe the span a fusion covers, because `text` is the
225
+ * replacement rather than the original: `10 frames` fuses into a FRAME_COUNT
226
+ * whose text is `10`, and a timecode fuses into a token whose text is a
227
+ * comma-separated tuple that appears nowhere in the line. Anything painting the
228
+ * line needs both ends, and only this function is in a position to know them.
229
+ *
230
+ * @param type - The new token type (e.g., "CARET", "TIMES_BY")
231
+ * @param text - The combined text representation (e.g., "to the power of")
232
+ * @param sourceTokens - The original tokens being fused (at least 2)
233
+ * @returns A new {@link LexerToken} with the fused type and combined text
234
+ */
235
+ declare function createFusedToken(type: string, text: string, sourceTokens: Token[]): Token;
236
+ /**
237
+ * Token normalizer: applies {@link NormalizerRule | NormalizerRules} to a token stream.
238
+ *
239
+ * ## Lifecycle
240
+ * 1. **Registration**: Rules are added via {@link register} and sorted by priority
241
+ * 2. **Normalization**: {@link normalize} applies rules greedily left-to-right
242
+ * 3. **Cleanup**: {@link clear} or {@link unregister} removes rules
243
+ *
244
+ * ## Normalization algorithm
245
+ * The normalizer uses a greedy left-to-right multi-pass algorithm:
246
+ * - At each token position, rules are tried in priority order (highest first)
247
+ * - When a rule matches, matched tokens are consumed and replaced
248
+ * - Processing continues from the replacement position
249
+ * - Multiple passes handle cascading matches (one rule's output triggers another)
250
+ * - Safety limits ({@link NormalizerOptions.maxPasses}) prevent infinite loops
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * const normalizer = new TokenNormalizer();
255
+ * normalizer.register(phraseRule); // "to the power of" → CARET
256
+ * normalizer.register(implicitMultRule); // "2 x" → "2 * x"
257
+ * const normalized = normalizer.normalize(rawTokens);
258
+ * ```
259
+ */
260
+ declare class TokenNormalizer {
261
+ /** Registered rules, unsorted, the source of truth. */
262
+ private rules;
263
+ /**
264
+ * Priority-sorted copy of {@link rules}, rebuilt lazily on the next
265
+ * {@link normalize} call after a mutation. Rules are registered once at
266
+ * engine/package-registration time and essentially never change during a
267
+ * session, but normalize() runs on every keystroke-driven evaluation, an
268
+ * earlier version re-sorted a fresh copy of `rules` on every single call,
269
+ * which meant every keystroke paid for an allocation + sort of a list that
270
+ * had usually not changed since the last one. `null` means "stale, rebuild
271
+ * on next use"; {@link register}/{@link unregister}/{@link clear} all
272
+ * invalidate it.
273
+ */
274
+ private sortedRulesCache;
275
+ /**
276
+ * Phrase trie for single-pass multi-word phrase fusion.
277
+ * Tried at each token position BEFORE other rules, the trie walk
278
+ * is O(depth) vs O(R × W) for separate rule matching.
279
+ */
280
+ private phraseTrie;
281
+ /** Merged options with defaults applied. */
282
+ private options;
283
+ /**
284
+ * @param options - Configuration overrides for safety limits and diagnostic callbacks
285
+ */
286
+ constructor(options?: NormalizerOptions);
287
+ /**
288
+ * Register a normalization rule.
289
+ *
290
+ * Rules are sorted by priority (descending) on each {@link normalize} call.
291
+ * Multiple rules can share the same priority, they are tried in registration
292
+ * order when priorities are equal.
293
+ *
294
+ * @param rule - The rule to register
295
+ */
296
+ register(rule: NormalizerRule): void;
297
+ /**
298
+ * Unregister a normalization rule by its {@link NormalizerRule.name | name}.
299
+ *
300
+ * If multiple rules share the same name, all are removed. This is safe to
301
+ * call with a name that doesn't match any rule, it simply has no effect.
302
+ *
303
+ * @param ruleName - The name of the rule to remove
304
+ */
305
+ unregister(ruleName: string): void;
306
+ /**
307
+ * Remove all registered rules, resetting the normalizer to its initial state.
308
+ * Also clears the phrase trie.
309
+ */
310
+ clear(): void;
311
+ /**
312
+ * Priority-sorted view of {@link rules} (descending priority; registration
313
+ * order preserved for ties, since {@link Array.prototype.sort} is stable).
314
+ * Cached until the next mutation. See {@link sortedRulesCache}.
315
+ */
316
+ private getSortedRules;
317
+ /**
318
+ * Get the number of currently registered rules (excludes phrase trie entries).
319
+ */
320
+ get ruleCount(): number;
321
+ /**
322
+ * Register a multi-word phrase for fusion into a single compound token.
323
+ *
324
+ * This is the preferred way to add phrase patterns. It inserts into the
325
+ * internal {@link PhraseTrie}, which collapses all phrase rules into a
326
+ * single O(depth) trie walk per position, no separate rule scanning.
327
+ *
328
+ * @param phrase - Multi-word phrase (e.g., "to the power of", "abyssal whip")
329
+ * @param tokenType - Target token type after fusion (e.g., "CARET", "ITEM")
330
+ */
331
+ addPhrase(phrase: string, tokenType: string): void;
332
+ /**
333
+ * Check whether a word can start any registered phrase.
334
+ *
335
+ * Used by {@link implicitMultiplyRule} to suppress `*` insertion
336
+ * before phrase-starting identifiers (e.g., "2 power of 3" → `2 ^ 3`,
337
+ * not `2 * power of 3`). Delegates to {@link PhraseTrie.canStart}.
338
+ */
339
+ /**
340
+ * Get all registered phrases and their target token types.
341
+ *
342
+ * Exposes the full phrase trie structure for diagnostic rendering
343
+ * in the playground's NormalizerTab. Returns ALL registered phrases,
344
+ * not just the ones that matched in the last evaluation.
345
+ */
346
+ getPhrases(): Record<string, string>;
347
+ canStartPhrase(word: string): boolean;
348
+ /**
349
+ * Normalize a token stream by applying all registered rules.
350
+ *
351
+ * ## Algorithm
352
+ * Applies rules greedily left-to-right in multiple passes:
353
+ * 1. Sort rules by priority (descending)
354
+ * 2. Walk the token stream left to right
355
+ * 3. At each position, try rules in priority order
356
+ * 4. On match: consume matched tokens, insert replacements, restart from insert point
357
+ * 5. On no match: pass token through unchanged
358
+ * 6. Repeat until a full pass produces no changes, or maxPasses is reached
359
+ *
360
+ * ## Fusion tracking
361
+ * When a rule consumes more tokens than it produces, the normalizer calls
362
+ * `onFusion` with a {@link TokenFusion} record for diagnostic collection.
363
+ * This populates {@link NormalizerOutput.fusions} in the playground pipeline view.
364
+ *
365
+ * ## Safety
366
+ * If the normalized token count exceeds {@link NormalizerOptions.maxTokens},
367
+ * an Error is thrown to prevent memory exhaustion from runaway rule expansion.
368
+ *
369
+ * @param tokens - Raw tokens from the lexer
370
+ * @param onFusion - Optional fusion callback (overrides {@link NormalizerOptions.onFusion})
371
+ * @returns Normalized tokens ready for parsing
372
+ * @throws {Error} If the normalized token count exceeds maxTokens
373
+ */
374
+ normalize(tokens: Token[], onFusion?: (fusion: TokenFusion) => void): Token[];
375
+ }
376
+
377
+ export { type NormalizerMatch as N, type TokenFusion as T, type NormalizerRule as a, type NormalizerOptions as b, TokenNormalizer as c, createFusedToken as d };