solve-engine 2.24.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/dist/{BytecodeBuilder-aqVa7Plx.d.cts → BytecodeBuilder-DSWKZi4f.d.cts} +11 -9
  2. package/dist/{BytecodeBuilder-aqVa7Plx.d.ts → BytecodeBuilder-DSWKZi4f.d.ts} +11 -9
  3. package/dist/{EngineError-D1kXsjIj.d.cts → EngineError-fR1K1rvx.d.cts} +2 -0
  4. package/dist/{EngineError-D1kXsjIj.d.ts → EngineError-fR1K1rvx.d.ts} +2 -0
  5. package/dist/{Lexer-CqagTewQ.d.ts → Lexer-Bmctxl0n.d.ts} +84 -35
  6. package/dist/{Lexer-CCHFDcgP.d.cts → Lexer-aB95pjoX.d.cts} +84 -35
  7. package/dist/{PackageCompatibility-Bq82y0ZN.d.ts → PackageCompatibility-BludTs7v.d.ts} +1 -1
  8. package/dist/{PackageCompatibility-CeAPVHZr.d.cts → PackageCompatibility-CUVg8tmt.d.cts} +1 -1
  9. package/dist/{PackageRegistry-CDEgfhT_.d.ts → PackageRegistry-DT-x32OM.d.ts} +54 -6
  10. package/dist/{PackageRegistry-B9-7fJGH.d.cts → PackageRegistry-Dvn4i2QK.d.cts} +54 -6
  11. package/dist/{Parselet-4oH5OuRt.d.cts → Parselet-BawfGuTF.d.cts} +7 -1
  12. package/dist/{Parselet-BwDqKFfK.d.ts → Parselet-DqTbbtWA.d.ts} +7 -1
  13. package/dist/{ScopeManager-BQBhlDAu.d.ts → ScopeManager-B636am7Y.d.ts} +2 -2
  14. package/dist/{ScopeManager-DrXB3Nvm.d.cts → ScopeManager-CdwX0VTw.d.cts} +2 -2
  15. package/dist/{TokenNormalizer-OTPS0Otq.d.ts → TokenNormalizer-CLBtiE8M.d.ts} +45 -26
  16. package/dist/{TokenNormalizer-MXaKLJ_m.d.cts → TokenNormalizer-CjTDmHgB.d.cts} +45 -26
  17. package/dist/{VMCheckpoints-BK32PTl2.d.cts → VMCheckpoints-CYqeq8IV.d.cts} +2 -2
  18. package/dist/{VMCheckpoints-C5jC92o3.d.ts → VMCheckpoints-DDlxNssY.d.ts} +2 -2
  19. package/dist/{WorkerError-DaNWQFp1.d.cts → WorkerError-B1dF3VYT.d.cts} +1 -1
  20. package/dist/{WorkerError-CVQbs6_D.d.ts → WorkerError-dneODN-u.d.ts} +1 -1
  21. package/dist/{chunk-CMZSK6FY.cjs → chunk-4BAUQE6A.cjs} +3 -3
  22. package/dist/{chunk-CMZSK6FY.cjs.map → chunk-4BAUQE6A.cjs.map} +1 -1
  23. package/dist/chunk-4BRTUFTH.js +2 -0
  24. package/dist/chunk-4BRTUFTH.js.map +1 -0
  25. package/dist/{chunk-UY3ID6JF.cjs → chunk-4NZXCHQL.cjs} +2 -2
  26. package/dist/chunk-4NZXCHQL.cjs.map +1 -0
  27. package/dist/chunk-57PSJQQW.js +3 -0
  28. package/dist/chunk-57PSJQQW.js.map +1 -0
  29. package/dist/chunk-634ILVMP.cjs +2 -0
  30. package/dist/{chunk-AUOE7MFI.cjs.map → chunk-634ILVMP.cjs.map} +1 -1
  31. package/dist/{chunk-3SHWTGTP.js → chunk-63ZOOUOL.js} +2 -2
  32. package/dist/{chunk-3SHWTGTP.js.map → chunk-63ZOOUOL.js.map} +1 -1
  33. package/dist/chunk-72PYPENZ.js +2 -0
  34. package/dist/chunk-72PYPENZ.js.map +1 -0
  35. package/dist/chunk-77KI7AKJ.cjs +2 -0
  36. package/dist/chunk-77KI7AKJ.cjs.map +1 -0
  37. package/dist/chunk-7DP57BA7.js +2 -0
  38. package/dist/chunk-7DP57BA7.js.map +1 -0
  39. package/dist/chunk-7N3IJSWF.js +5 -0
  40. package/dist/chunk-7N3IJSWF.js.map +1 -0
  41. package/dist/chunk-BMXYR5LI.cjs +2 -0
  42. package/dist/chunk-BMXYR5LI.cjs.map +1 -0
  43. package/dist/{chunk-HQ7BKXG7.js → chunk-DUYTXLO2.js} +2 -2
  44. package/dist/chunk-DUYTXLO2.js.map +1 -0
  45. package/dist/chunk-EL2XUOWX.js +2 -0
  46. package/dist/chunk-EL2XUOWX.js.map +1 -0
  47. package/dist/{chunk-MWWAKZOD.cjs → chunk-FYAMQFZZ.cjs} +3 -3
  48. package/dist/{chunk-MWWAKZOD.cjs.map → chunk-FYAMQFZZ.cjs.map} +1 -1
  49. package/dist/chunk-FYTB7SDP.cjs +2 -0
  50. package/dist/chunk-FYTB7SDP.cjs.map +1 -0
  51. package/dist/chunk-GBCN7FNV.cjs +2 -0
  52. package/dist/chunk-GBCN7FNV.cjs.map +1 -0
  53. package/dist/{chunk-PTQCHMYA.js → chunk-JFZ5SLF7.js} +2 -2
  54. package/dist/{chunk-PTQCHMYA.js.map → chunk-JFZ5SLF7.js.map} +1 -1
  55. package/dist/{chunk-RMCN5WFO.cjs → chunk-JSM3O7EM.cjs} +2 -2
  56. package/dist/{chunk-RMCN5WFO.cjs.map → chunk-JSM3O7EM.cjs.map} +1 -1
  57. package/dist/chunk-MZMA32NE.js +3 -0
  58. package/dist/chunk-MZMA32NE.js.map +1 -0
  59. package/dist/{chunk-D2Q2VFJ6.js → chunk-N4SSR7Q6.js} +2 -2
  60. package/dist/{chunk-D2Q2VFJ6.js.map → chunk-N4SSR7Q6.js.map} +1 -1
  61. package/dist/{chunk-RN3ISTM3.cjs → chunk-NEGZZB7F.cjs} +2 -2
  62. package/dist/{chunk-RN3ISTM3.cjs.map → chunk-NEGZZB7F.cjs.map} +1 -1
  63. package/dist/{chunk-IPSOWFCA.js → chunk-NMU5E7W7.js} +2 -2
  64. package/dist/{chunk-IPSOWFCA.js.map → chunk-NMU5E7W7.js.map} +1 -1
  65. package/dist/chunk-OB2VX4IL.js +2 -0
  66. package/dist/chunk-OB2VX4IL.js.map +1 -0
  67. package/dist/{chunk-OMHRBKAT.js → chunk-OONQ3V3I.js} +2 -2
  68. package/dist/{chunk-OMHRBKAT.js.map → chunk-OONQ3V3I.js.map} +1 -1
  69. package/dist/{chunk-LTJV3VJE.cjs → chunk-RAPEZR5L.cjs} +2 -2
  70. package/dist/{chunk-LTJV3VJE.cjs.map → chunk-RAPEZR5L.cjs.map} +1 -1
  71. package/dist/{chunk-UXR7JIPX.js → chunk-RDYXH7ML.js} +2 -2
  72. package/dist/{chunk-UXR7JIPX.js.map → chunk-RDYXH7ML.js.map} +1 -1
  73. package/dist/{chunk-CDNJZBM3.cjs → chunk-SAF3B3AX.cjs} +2 -2
  74. package/dist/{chunk-CDNJZBM3.cjs.map → chunk-SAF3B3AX.cjs.map} +1 -1
  75. package/dist/chunk-SPOVFRZO.cjs +5 -0
  76. package/dist/chunk-SPOVFRZO.cjs.map +1 -0
  77. package/dist/{chunk-DA7M6H63.cjs → chunk-SQHBOWES.cjs} +2 -2
  78. package/dist/{chunk-DA7M6H63.cjs.map → chunk-SQHBOWES.cjs.map} +1 -1
  79. package/dist/{chunk-2BXNZM3G.js → chunk-UF5XMZTT.js} +2 -2
  80. package/dist/{chunk-2BXNZM3G.js.map → chunk-UF5XMZTT.js.map} +1 -1
  81. package/dist/chunk-UV3EUIBT.cjs +3 -0
  82. package/dist/chunk-UV3EUIBT.cjs.map +1 -0
  83. package/dist/chunk-VMSCRVNX.cjs +2 -0
  84. package/dist/chunk-VMSCRVNX.cjs.map +1 -0
  85. package/dist/{chunk-2VE4OW4A.cjs → chunk-WGP4UUO5.cjs} +2 -2
  86. package/dist/{chunk-2VE4OW4A.cjs.map → chunk-WGP4UUO5.cjs.map} +1 -1
  87. package/dist/{chunk-F7QIBC4B.cjs → chunk-XBGK3TTU.cjs} +3 -3
  88. package/dist/{chunk-F7QIBC4B.cjs.map → chunk-XBGK3TTU.cjs.map} +1 -1
  89. package/dist/{chunk-GP4H5OIT.js → chunk-XHQYHXRE.js} +3 -3
  90. package/dist/{chunk-GP4H5OIT.js.map → chunk-XHQYHXRE.js.map} +1 -1
  91. package/dist/{chunk-RE6AIU6Y.js → chunk-XX55RR4Q.js} +3 -3
  92. package/dist/{chunk-RE6AIU6Y.js.map → chunk-XX55RR4Q.js.map} +1 -1
  93. package/dist/chunk-YBQPEWT7.cjs +3 -0
  94. package/dist/chunk-YBQPEWT7.cjs.map +1 -0
  95. package/dist/{chunk-7EP36NXE.js → chunk-ZDCZ5IDD.js} +3 -3
  96. package/dist/{chunk-7EP36NXE.js.map → chunk-ZDCZ5IDD.js.map} +1 -1
  97. package/dist/constants.cjs +1 -1
  98. package/dist/constants.js +1 -1
  99. package/dist/engine.cjs +1 -1
  100. package/dist/engine.d.cts +9 -9
  101. package/dist/engine.d.ts +9 -9
  102. package/dist/engine.js +1 -1
  103. package/dist/errors.cjs +1 -1
  104. package/dist/errors.d.cts +3 -3
  105. package/dist/errors.d.ts +3 -3
  106. package/dist/errors.js +1 -1
  107. package/dist/format.cjs +1 -1
  108. package/dist/format.js +1 -1
  109. package/dist/index.cjs +1 -1
  110. package/dist/index.d.cts +9 -9
  111. package/dist/index.d.ts +9 -9
  112. package/dist/index.js +1 -1
  113. package/dist/language.d.cts +8 -8
  114. package/dist/language.d.ts +8 -8
  115. package/dist/lexer.cjs +1 -1
  116. package/dist/lexer.cjs.map +1 -1
  117. package/dist/lexer.d.cts +8 -7
  118. package/dist/lexer.d.ts +8 -7
  119. package/dist/lexer.js +1 -1
  120. package/dist/lexer.js.map +1 -1
  121. package/dist/normalizer.cjs +1 -1
  122. package/dist/normalizer.d.cts +2 -2
  123. package/dist/normalizer.d.ts +2 -2
  124. package/dist/normalizer.js +1 -1
  125. package/dist/packages.cjs +1 -1
  126. package/dist/packages.d.cts +7 -7
  127. package/dist/packages.d.ts +7 -7
  128. package/dist/packages.js +1 -1
  129. package/dist/parser.cjs +1 -1
  130. package/dist/parser.d.cts +2 -2
  131. package/dist/parser.d.ts +2 -2
  132. package/dist/parser.js +1 -1
  133. package/dist/resolvers.d.cts +1 -1
  134. package/dist/resolvers.d.ts +1 -1
  135. package/dist/testing.cjs +2 -2
  136. package/dist/testing.d.cts +8 -8
  137. package/dist/testing.d.ts +8 -8
  138. package/dist/testing.js +1 -1
  139. package/dist/uom.cjs +1 -1
  140. package/dist/uom.d.cts +1 -1
  141. package/dist/uom.d.ts +1 -1
  142. package/dist/uom.js +1 -1
  143. package/dist/vm.cjs +1 -1
  144. package/dist/vm.d.cts +5 -5
  145. package/dist/vm.d.ts +5 -5
  146. package/dist/vm.js +1 -1
  147. package/dist/worker.cjs +2 -2
  148. package/dist/worker.d.cts +8 -8
  149. package/dist/worker.d.ts +8 -8
  150. package/dist/worker.js +1 -1
  151. package/package.json +5 -5
  152. package/dist/chunk-2JSNWYYA.cjs +0 -3
  153. package/dist/chunk-2JSNWYYA.cjs.map +0 -1
  154. package/dist/chunk-5GL4SAVH.cjs +0 -2
  155. package/dist/chunk-5GL4SAVH.cjs.map +0 -1
  156. package/dist/chunk-72N3ZRVE.cjs +0 -2
  157. package/dist/chunk-72N3ZRVE.cjs.map +0 -1
  158. package/dist/chunk-A4JV7HRB.js +0 -3
  159. package/dist/chunk-A4JV7HRB.js.map +0 -1
  160. package/dist/chunk-AUOE7MFI.cjs +0 -2
  161. package/dist/chunk-BDF4VCQX.js +0 -3
  162. package/dist/chunk-BDF4VCQX.js.map +0 -1
  163. package/dist/chunk-ELQQKZN3.cjs +0 -3
  164. package/dist/chunk-ELQQKZN3.cjs.map +0 -1
  165. package/dist/chunk-HQ7BKXG7.js.map +0 -1
  166. package/dist/chunk-IHAZL4EF.cjs +0 -2
  167. package/dist/chunk-IHAZL4EF.cjs.map +0 -1
  168. package/dist/chunk-LM6ZQUEU.js +0 -2
  169. package/dist/chunk-LM6ZQUEU.js.map +0 -1
  170. package/dist/chunk-NUF66H57.js +0 -5
  171. package/dist/chunk-NUF66H57.js.map +0 -1
  172. package/dist/chunk-PWWQZFAE.js +0 -2
  173. package/dist/chunk-PWWQZFAE.js.map +0 -1
  174. package/dist/chunk-RECRD45C.cjs +0 -5
  175. package/dist/chunk-RECRD45C.cjs.map +0 -1
  176. package/dist/chunk-RENO2AWO.js +0 -2
  177. package/dist/chunk-RENO2AWO.js.map +0 -1
  178. package/dist/chunk-S46R5QZP.cjs +0 -2
  179. package/dist/chunk-S46R5QZP.cjs.map +0 -1
  180. package/dist/chunk-UPH22K2T.cjs +0 -2
  181. package/dist/chunk-UPH22K2T.cjs.map +0 -1
  182. package/dist/chunk-UY3ID6JF.cjs.map +0 -1
  183. package/dist/chunk-WFOQRPA6.js +0 -2
  184. package/dist/chunk-WFOQRPA6.js.map +0 -1
  185. package/dist/chunk-XFJABR3B.js +0 -2
  186. package/dist/chunk-XFJABR3B.js.map +0 -1
@@ -199,6 +199,8 @@ declare class BytecodeBuilder {
199
199
  private numbers;
200
200
  private strings;
201
201
  private stringIndex;
202
+ /** Value (or {@link NEGATIVE_ZERO_KEY}) to its slot in `numbers`, so a repeated literal is emitted once. */
203
+ private numberIndex;
202
204
  private _hasAsync;
203
205
  private userFunctionBodies;
204
206
  private anonymousBodies;
@@ -222,17 +224,17 @@ declare class BytecodeBuilder {
222
224
  /** Emit an {@link OpCode} instruction. */
223
225
  emitOpcode(op: OpCode): void;
224
226
  /**
225
- * Emit a numeric literal: appends `n` to the program's constant pool and
226
- * writes its index into the opcode stream (read back by the VM as e.g.
227
- * `PUSH_NUMBER <idx>`).
227
+ * Emit a numeric literal: interns `n` into the program's constant pool
228
+ * (deduplicated, like {@link emitString}) and writes its index into the
229
+ * opcode stream (read back by the VM as e.g. `PUSH_NUMBER <idx>`).
228
230
  *
229
- * Numeric constants are NOT deduplicated (unlike {@link emitString})
230
- * every call appends a new entry, so an expression with more than
231
- * {@link MAX_CONSTANT_POOL_INDEX}+1 distinct numeric-literal occurrences
232
- * throws rather than silently wrapping the index (see
233
- * `MAX_CONSTANT_POOL_INDEX`'s doc for what that would otherwise do).
231
+ * Deduplicated so that the 256-entry pool counts distinct values rather
232
+ * than occurrences: a long line of repeated literals used to exhaust it
233
+ * long before it held 256 different numbers. Negative zero keeps its own
234
+ * slot, because `1 / -0` is not `1 / 0`; NaN shares one, since every NaN
235
+ * reads the same.
234
236
  *
235
- * @throws If the constant pool would exceed 256 entries.
237
+ * @throws If the pool would exceed 256 distinct entries.
236
238
  */
237
239
  emitNumber(n: number): void;
238
240
  /**
@@ -199,6 +199,8 @@ declare class BytecodeBuilder {
199
199
  private numbers;
200
200
  private strings;
201
201
  private stringIndex;
202
+ /** Value (or {@link NEGATIVE_ZERO_KEY}) to its slot in `numbers`, so a repeated literal is emitted once. */
203
+ private numberIndex;
202
204
  private _hasAsync;
203
205
  private userFunctionBodies;
204
206
  private anonymousBodies;
@@ -222,17 +224,17 @@ declare class BytecodeBuilder {
222
224
  /** Emit an {@link OpCode} instruction. */
223
225
  emitOpcode(op: OpCode): void;
224
226
  /**
225
- * Emit a numeric literal: appends `n` to the program's constant pool and
226
- * writes its index into the opcode stream (read back by the VM as e.g.
227
- * `PUSH_NUMBER <idx>`).
227
+ * Emit a numeric literal: interns `n` into the program's constant pool
228
+ * (deduplicated, like {@link emitString}) and writes its index into the
229
+ * opcode stream (read back by the VM as e.g. `PUSH_NUMBER <idx>`).
228
230
  *
229
- * Numeric constants are NOT deduplicated (unlike {@link emitString})
230
- * every call appends a new entry, so an expression with more than
231
- * {@link MAX_CONSTANT_POOL_INDEX}+1 distinct numeric-literal occurrences
232
- * throws rather than silently wrapping the index (see
233
- * `MAX_CONSTANT_POOL_INDEX`'s doc for what that would otherwise do).
231
+ * Deduplicated so that the 256-entry pool counts distinct values rather
232
+ * than occurrences: a long line of repeated literals used to exhaust it
233
+ * long before it held 256 different numbers. Negative zero keeps its own
234
+ * slot, because `1 / -0` is not `1 / 0`; NaN shares one, since every NaN
235
+ * reads the same.
234
236
  *
235
- * @throws If the constant pool would exceed 256 entries.
237
+ * @throws If the pool would exceed 256 distinct entries.
236
238
  */
237
239
  emitNumber(n: number): void;
238
240
  /**
@@ -38,6 +38,8 @@ declare const CoreErrorCodes: {
38
38
  readonly UNEXPECTED_TRAILING_TOKEN: "UNEXPECTED_TRAILING_TOKEN";
39
39
  readonly PARSE_ERROR: "PARSE_ERROR";
40
40
  readonly TOO_MANY_NUMERIC_CONSTANTS: "TOO_MANY_NUMERIC_CONSTANTS";
41
+ /** A raw bytecode operand outside 0 to 255, or a jump patch outside the emitted stream (`parser/BytecodeBuilder.ts`'s `emitIndex`, `emitByte`, `patchJump`). `build()` keeps one byte per operand, so 300 used to become 44 with no error and the program read the wrong constant or plugin function. A package-authoring fault, reported at compile time. */
42
+ readonly BYTECODE_OPERAND_OUT_OF_RANGE: "BYTECODE_OPERAND_OUT_OF_RANGE";
41
43
  readonly TOO_MANY_STRING_CONSTANTS: "TOO_MANY_STRING_CONSTANTS";
42
44
  readonly NO_MATCHING_PHRASE_ALTERNATIVE: "NO_MATCHING_PHRASE_ALTERNATIVE";
43
45
  readonly INVALID_PHRASE_PATTERN: "INVALID_PHRASE_PATTERN";
@@ -38,6 +38,8 @@ declare const CoreErrorCodes: {
38
38
  readonly UNEXPECTED_TRAILING_TOKEN: "UNEXPECTED_TRAILING_TOKEN";
39
39
  readonly PARSE_ERROR: "PARSE_ERROR";
40
40
  readonly TOO_MANY_NUMERIC_CONSTANTS: "TOO_MANY_NUMERIC_CONSTANTS";
41
+ /** A raw bytecode operand outside 0 to 255, or a jump patch outside the emitted stream (`parser/BytecodeBuilder.ts`'s `emitIndex`, `emitByte`, `patchJump`). `build()` keeps one byte per operand, so 300 used to become 44 with no error and the program read the wrong constant or plugin function. A package-authoring fault, reported at compile time. */
42
+ readonly BYTECODE_OPERAND_OUT_OF_RANGE: "BYTECODE_OPERAND_OUT_OF_RANGE";
41
43
  readonly TOO_MANY_STRING_CONSTANTS: "TOO_MANY_STRING_CONSTANTS";
42
44
  readonly NO_MATCHING_PHRASE_ALTERNATIVE: "NO_MATCHING_PHRASE_ALTERNATIVE";
43
45
  readonly INVALID_PHRASE_PATTERN: "INVALID_PHRASE_PATTERN";
@@ -1,4 +1,5 @@
1
1
  import { T as Token } from './Token-CbP_OutD.js';
2
+ import { E as EngineError } from './EngineError-fR1K1rvx.js';
2
3
 
3
4
  /** Trie node for multi-word phrase matching. */
4
5
  interface PhraseNode {
@@ -101,6 +102,14 @@ interface ScanLineResult {
101
102
  tokens: Token[];
102
103
  /** Inline solve spans found in this line (empty if none). */
103
104
  inlineSolves: InlineSolveSpan[];
105
+ /**
106
+ * The structured error the tokeniser raised on this line, if it raised one
107
+ * (an unterminated string literal is the one it can raise today). The line
108
+ * carries no tokens then, and the rest of the document is unaffected: a
109
+ * half-typed quote used to escape {@link ExpressionLexer.scanDocument} as
110
+ * a throw and take every other line's result down with it.
111
+ */
112
+ error?: EngineError;
104
113
  }
105
114
  /**
106
115
  * A token, as the lexer produces it.
@@ -208,16 +217,6 @@ interface LexerVocabulary {
208
217
  */
209
218
  declare class ExpressionLexer {
210
219
  private static readonly CHAR_CLASS;
211
- /**
212
- * Configured TokenLookup from TokenClassRegistry. When set, replaces
213
- * the internal keyword map and unit set with registry-built equivalents.
214
- * Enables data-driven keyword/unit registration across locale keywords,
215
- * provider keywords, and plugins.
216
- *
217
- * Set at construction time via the constructor parameter. Plugin-registered
218
- * keywords/units (via registerVocabulary()) are checked alongside
219
- * the configuredLookup, neither source is bypassed.
220
- */ private configuredLookup;
221
220
  private input;
222
221
  private pos;
223
222
  private len;
@@ -227,7 +226,16 @@ declare class ExpressionLexer {
227
226
  private keywordMap;
228
227
  private mergedKeywords;
229
228
  private mergedUnits;
230
- private pluginKeywordMap;
229
+ /**
230
+ * Who registered each plugin keyword, unit and two-character operator, in
231
+ * registration order. Two packages may claim the same word (the
232
+ * compatibility index warns, and the last registered wins, as before); the
233
+ * owner lists make unregistration exact, so removing one package hands the
234
+ * word back to the other rather than deleting it for both.
235
+ */
236
+ private pluginKeywordOwners;
237
+ private pluginUnitOwners;
238
+ private pluginOperatorOwners;
231
239
  private pluginOperators;
232
240
  private pluginUnits;
233
241
  private hasPluginOps;
@@ -235,7 +243,7 @@ declare class ExpressionLexer {
235
243
  private locale;
236
244
  /**
237
245
  * Inline solve spans collected during the most recent tokenization pass.
238
- * Populated by [Symbol.iterator]() and consumed by scanDocument().
246
+ * Populated by tokenizeInto() and consumed by scanDocument().
239
247
  */
240
248
  _inlineSolveSpans: InlineSolveSpan[];
241
249
  private pluginRawLinePatterns;
@@ -260,7 +268,14 @@ declare class ExpressionLexer {
260
268
  private matchRawLine;
261
269
  /** Rebuild merged keyword and unit collections after plugin registration. */
262
270
  private rebuildMergedUnits;
263
- constructor(localeCode?: string, lookup?: TokenLookup);
271
+ /**
272
+ * @param localeCode - Locale whose keyword table seeds the lexer.
273
+ * @param _lookup - Ignored. The lexer built its keyword, unit and phrase
274
+ * tables from the locale and the registered packages and never read the
275
+ * lookup it was handed; the parameter stays so existing callers compile.
276
+ * @deprecated Removed in 3.0.
277
+ */
278
+ constructor(localeCode?: string, _lookup?: TokenLookup);
264
279
  /**
265
280
  * Register a plugin to extend the lexer with custom tokens.
266
281
  *
@@ -276,6 +291,10 @@ declare class ExpressionLexer {
276
291
  * that conflicts with a built-in one.
277
292
  */
278
293
  registerVocabulary(plugin: LexerVocabulary): void;
294
+ /** Record `owner`'s claim on `key`. The newest claim is the one in force. */
295
+ private claim;
296
+ /** Drop `owner`'s claim on `key`, and return the claim now in force, if any is left. */
297
+ private release;
279
298
  /**
280
299
  * Unregister a plugin, removing its custom tokens from the lexer.
281
300
  *
@@ -307,9 +326,9 @@ declare class ExpressionLexer {
307
326
  * lines that classifyLine() marks as having inline solves.
308
327
  *
309
328
  * Tokenization is scoped to each line by temporarily restricting
310
- * `this.len` to the line end position, so the [Symbol.iterator]
311
- * generator naturally stops at the line boundary. After tokenization,
312
- * `this.len` is restored and `this.pos` advances past the newline.
329
+ * `this.len` to the line end position, so tokenizeInto() naturally
330
+ * stops at the line boundary. After tokenization, `this.len` is
331
+ * restored and `this.pos` advances past the newline.
313
332
  *
314
333
  * @param text The full document text (with newlines).
315
334
  * @returns Array of ScanLineResult, one per line, in document order.
@@ -318,9 +337,7 @@ declare class ExpressionLexer {
318
337
  /**
319
338
  * Tokenize an expression string into an array of Tokens.
320
339
  *
321
- * Delegates to the lazy [Symbol.iterator]() generator and collects all
322
- * yielded tokens via Array.from(). For memory-sensitive use cases, prefer
323
- * iterating the lexer directly with for...of to avoid array allocation.
340
+ * Runs {@link tokenizeInto} into a fresh array.
324
341
  *
325
342
  * Optimizations:
326
343
  * - CHAR_CLASS jump table (Uint8Array) → switch on small integers
@@ -332,20 +349,31 @@ declare class ExpressionLexer {
332
349
  */
333
350
  tokenizeAll(): Token[];
334
351
  /**
335
- * Lazy token-by-token generator. Yields each token without allocating an
336
- * intermediate Token[] array. Supports for...of and spread usage.
352
+ * Scan the current input and append every token to `out`.
353
+ *
354
+ * This is the scanner itself: {@link tokenizeAll} and the iterator are
355
+ * thin wrappers over it. It pushes into an array the caller owns rather
356
+ * than yielding, because a generator paid a resume per token and
357
+ * `Array.from` a second pass on top, and because a caller that catches a
358
+ * tokeniser fault (highlighting a line with an unterminated quote) still
359
+ * holds the tokens read before it.
337
360
  *
338
- * Usage:
339
- * for (const t of lexer) { ... } // lazy, no array allocation
340
- * const tokens = [...lexer]; // materializes via spread
341
- * const tokens = lexer.tokenizeAll(); // materializes via Array.from()
361
+ * IMPORTANT: `this.len` is read ONCE on entry (const len = this.len).
362
+ * `scanDocument()` relies on this to scope a pass to a single line by
363
+ * restricting `this.len` to the line end before calling. Do not re-read
364
+ * `this.len` mid-loop without also updating `scanDocument()`.
342
365
  *
343
- * IMPORTANT: This generator captures `this.len` ONCE at creation time
344
- * (const len = this.len). `scanDocument()` relies on this behavior to
345
- * scope tokenization to a single line by temporarily restricting
346
- * `this.len` to the line end position before creating the iterator.
347
- * Do NOT refactor to re-read `this.len` mid-loop without also updating
348
- * `scanDocument()`.
366
+ * @param out - The array to append to. Left as it was if the input is empty.
367
+ */
368
+ tokenizeInto(out: Token[]): void;
369
+ /**
370
+ * Iterate the tokens of the current input.
371
+ *
372
+ * Runs {@link tokenizeInto} first, so the tokens are the same as before,
373
+ * but a fault part way through the line is thrown from the first
374
+ * `next()` rather than at the token it occurred on. A caller that wants
375
+ * the tokens read before a fault calls {@link tokenizeInto} with its own
376
+ * array.
349
377
  */
350
378
  [Symbol.iterator](): Generator<Token, void, undefined>;
351
379
  /**
@@ -456,6 +484,27 @@ declare class ExpressionLexer {
456
484
  * `#12345` (length 5), `#ff0000zz` (trailing word) do not.
457
485
  */
458
486
  private matchHexColourEnd;
487
+ /**
488
+ * Whether an inline-solve opener (`s` + backtick) occurs in `[from, end)`.
489
+ *
490
+ * Bounded by hand rather than written as `input.indexOf("s\`", from)`. This
491
+ * used to be the `indexOf` form with an `idx < end` check afterwards, which
492
+ * is correct but not bounded: when {@link scanDocument} is classifying,
493
+ * `this.input` is the whole document, so a line with no marker scanned to
494
+ * the END OF THE DOCUMENT before the check could reject the hit. Every line
495
+ * paid for every line after it, and a whole-document parse was quadratic in
496
+ * line count (measured at 28.9 us per line at 10,000 lines against 10.1 us
497
+ * at 1,000, and two thirds of the parse's self time in a profile). A prose
498
+ * document with many candidate `s` characters was worse still. The
499
+ * wikilink close in {@link indexOfWithin} had the same shape.
500
+ */
501
+ private hasInlineSolveWithin;
502
+ /**
503
+ * `input.indexOf(needle, from)` restricted to `[from, end)`, returning -1
504
+ * when the needle does not occur wholly before `end`. See
505
+ * {@link hasInlineSolveWithin} for why the plain `indexOf` was not enough.
506
+ */
507
+ private indexOfWithin;
459
508
  private classifyFromPositions;
460
509
  /**
461
510
  * Classify a single line of markdown text.
@@ -530,11 +579,11 @@ declare class Lexer {
530
579
  private tokenIdx;
531
580
  /**
532
581
  * @param localeCode - Locale code (e.g., "en", "de"). Defaults to "en".
533
- * @param tokenLookup - Optional TokenLookup from TokenClassRegistry.
534
- * When provided, configures ExpressionLexer to use registry-built
535
- * keyword/unit/phrase lookups instead of internal instance maps.
582
+ * @param _tokenLookup - Ignored. The lexer never read the lookup it was
583
+ * handed; the parameter stays so existing callers compile.
584
+ * @deprecated Removed in 3.0.
536
585
  */
537
- constructor(localeCode?: string, tokenLookup?: TokenLookup);
586
+ constructor(localeCode?: string, _tokenLookup?: TokenLookup);
538
587
  reset(input: string, state?: LexerState): void;
539
588
  /**
540
589
  * Classify a single line of markdown text (Phase B).
@@ -1,4 +1,5 @@
1
1
  import { T as Token } from './Token-CbP_OutD.cjs';
2
+ import { E as EngineError } from './EngineError-fR1K1rvx.cjs';
2
3
 
3
4
  /** Trie node for multi-word phrase matching. */
4
5
  interface PhraseNode {
@@ -101,6 +102,14 @@ interface ScanLineResult {
101
102
  tokens: Token[];
102
103
  /** Inline solve spans found in this line (empty if none). */
103
104
  inlineSolves: InlineSolveSpan[];
105
+ /**
106
+ * The structured error the tokeniser raised on this line, if it raised one
107
+ * (an unterminated string literal is the one it can raise today). The line
108
+ * carries no tokens then, and the rest of the document is unaffected: a
109
+ * half-typed quote used to escape {@link ExpressionLexer.scanDocument} as
110
+ * a throw and take every other line's result down with it.
111
+ */
112
+ error?: EngineError;
104
113
  }
105
114
  /**
106
115
  * A token, as the lexer produces it.
@@ -208,16 +217,6 @@ interface LexerVocabulary {
208
217
  */
209
218
  declare class ExpressionLexer {
210
219
  private static readonly CHAR_CLASS;
211
- /**
212
- * Configured TokenLookup from TokenClassRegistry. When set, replaces
213
- * the internal keyword map and unit set with registry-built equivalents.
214
- * Enables data-driven keyword/unit registration across locale keywords,
215
- * provider keywords, and plugins.
216
- *
217
- * Set at construction time via the constructor parameter. Plugin-registered
218
- * keywords/units (via registerVocabulary()) are checked alongside
219
- * the configuredLookup, neither source is bypassed.
220
- */ private configuredLookup;
221
220
  private input;
222
221
  private pos;
223
222
  private len;
@@ -227,7 +226,16 @@ declare class ExpressionLexer {
227
226
  private keywordMap;
228
227
  private mergedKeywords;
229
228
  private mergedUnits;
230
- private pluginKeywordMap;
229
+ /**
230
+ * Who registered each plugin keyword, unit and two-character operator, in
231
+ * registration order. Two packages may claim the same word (the
232
+ * compatibility index warns, and the last registered wins, as before); the
233
+ * owner lists make unregistration exact, so removing one package hands the
234
+ * word back to the other rather than deleting it for both.
235
+ */
236
+ private pluginKeywordOwners;
237
+ private pluginUnitOwners;
238
+ private pluginOperatorOwners;
231
239
  private pluginOperators;
232
240
  private pluginUnits;
233
241
  private hasPluginOps;
@@ -235,7 +243,7 @@ declare class ExpressionLexer {
235
243
  private locale;
236
244
  /**
237
245
  * Inline solve spans collected during the most recent tokenization pass.
238
- * Populated by [Symbol.iterator]() and consumed by scanDocument().
246
+ * Populated by tokenizeInto() and consumed by scanDocument().
239
247
  */
240
248
  _inlineSolveSpans: InlineSolveSpan[];
241
249
  private pluginRawLinePatterns;
@@ -260,7 +268,14 @@ declare class ExpressionLexer {
260
268
  private matchRawLine;
261
269
  /** Rebuild merged keyword and unit collections after plugin registration. */
262
270
  private rebuildMergedUnits;
263
- constructor(localeCode?: string, lookup?: TokenLookup);
271
+ /**
272
+ * @param localeCode - Locale whose keyword table seeds the lexer.
273
+ * @param _lookup - Ignored. The lexer built its keyword, unit and phrase
274
+ * tables from the locale and the registered packages and never read the
275
+ * lookup it was handed; the parameter stays so existing callers compile.
276
+ * @deprecated Removed in 3.0.
277
+ */
278
+ constructor(localeCode?: string, _lookup?: TokenLookup);
264
279
  /**
265
280
  * Register a plugin to extend the lexer with custom tokens.
266
281
  *
@@ -276,6 +291,10 @@ declare class ExpressionLexer {
276
291
  * that conflicts with a built-in one.
277
292
  */
278
293
  registerVocabulary(plugin: LexerVocabulary): void;
294
+ /** Record `owner`'s claim on `key`. The newest claim is the one in force. */
295
+ private claim;
296
+ /** Drop `owner`'s claim on `key`, and return the claim now in force, if any is left. */
297
+ private release;
279
298
  /**
280
299
  * Unregister a plugin, removing its custom tokens from the lexer.
281
300
  *
@@ -307,9 +326,9 @@ declare class ExpressionLexer {
307
326
  * lines that classifyLine() marks as having inline solves.
308
327
  *
309
328
  * Tokenization is scoped to each line by temporarily restricting
310
- * `this.len` to the line end position, so the [Symbol.iterator]
311
- * generator naturally stops at the line boundary. After tokenization,
312
- * `this.len` is restored and `this.pos` advances past the newline.
329
+ * `this.len` to the line end position, so tokenizeInto() naturally
330
+ * stops at the line boundary. After tokenization, `this.len` is
331
+ * restored and `this.pos` advances past the newline.
313
332
  *
314
333
  * @param text The full document text (with newlines).
315
334
  * @returns Array of ScanLineResult, one per line, in document order.
@@ -318,9 +337,7 @@ declare class ExpressionLexer {
318
337
  /**
319
338
  * Tokenize an expression string into an array of Tokens.
320
339
  *
321
- * Delegates to the lazy [Symbol.iterator]() generator and collects all
322
- * yielded tokens via Array.from(). For memory-sensitive use cases, prefer
323
- * iterating the lexer directly with for...of to avoid array allocation.
340
+ * Runs {@link tokenizeInto} into a fresh array.
324
341
  *
325
342
  * Optimizations:
326
343
  * - CHAR_CLASS jump table (Uint8Array) → switch on small integers
@@ -332,20 +349,31 @@ declare class ExpressionLexer {
332
349
  */
333
350
  tokenizeAll(): Token[];
334
351
  /**
335
- * Lazy token-by-token generator. Yields each token without allocating an
336
- * intermediate Token[] array. Supports for...of and spread usage.
352
+ * Scan the current input and append every token to `out`.
353
+ *
354
+ * This is the scanner itself: {@link tokenizeAll} and the iterator are
355
+ * thin wrappers over it. It pushes into an array the caller owns rather
356
+ * than yielding, because a generator paid a resume per token and
357
+ * `Array.from` a second pass on top, and because a caller that catches a
358
+ * tokeniser fault (highlighting a line with an unterminated quote) still
359
+ * holds the tokens read before it.
337
360
  *
338
- * Usage:
339
- * for (const t of lexer) { ... } // lazy, no array allocation
340
- * const tokens = [...lexer]; // materializes via spread
341
- * const tokens = lexer.tokenizeAll(); // materializes via Array.from()
361
+ * IMPORTANT: `this.len` is read ONCE on entry (const len = this.len).
362
+ * `scanDocument()` relies on this to scope a pass to a single line by
363
+ * restricting `this.len` to the line end before calling. Do not re-read
364
+ * `this.len` mid-loop without also updating `scanDocument()`.
342
365
  *
343
- * IMPORTANT: This generator captures `this.len` ONCE at creation time
344
- * (const len = this.len). `scanDocument()` relies on this behavior to
345
- * scope tokenization to a single line by temporarily restricting
346
- * `this.len` to the line end position before creating the iterator.
347
- * Do NOT refactor to re-read `this.len` mid-loop without also updating
348
- * `scanDocument()`.
366
+ * @param out - The array to append to. Left as it was if the input is empty.
367
+ */
368
+ tokenizeInto(out: Token[]): void;
369
+ /**
370
+ * Iterate the tokens of the current input.
371
+ *
372
+ * Runs {@link tokenizeInto} first, so the tokens are the same as before,
373
+ * but a fault part way through the line is thrown from the first
374
+ * `next()` rather than at the token it occurred on. A caller that wants
375
+ * the tokens read before a fault calls {@link tokenizeInto} with its own
376
+ * array.
349
377
  */
350
378
  [Symbol.iterator](): Generator<Token, void, undefined>;
351
379
  /**
@@ -456,6 +484,27 @@ declare class ExpressionLexer {
456
484
  * `#12345` (length 5), `#ff0000zz` (trailing word) do not.
457
485
  */
458
486
  private matchHexColourEnd;
487
+ /**
488
+ * Whether an inline-solve opener (`s` + backtick) occurs in `[from, end)`.
489
+ *
490
+ * Bounded by hand rather than written as `input.indexOf("s\`", from)`. This
491
+ * used to be the `indexOf` form with an `idx < end` check afterwards, which
492
+ * is correct but not bounded: when {@link scanDocument} is classifying,
493
+ * `this.input` is the whole document, so a line with no marker scanned to
494
+ * the END OF THE DOCUMENT before the check could reject the hit. Every line
495
+ * paid for every line after it, and a whole-document parse was quadratic in
496
+ * line count (measured at 28.9 us per line at 10,000 lines against 10.1 us
497
+ * at 1,000, and two thirds of the parse's self time in a profile). A prose
498
+ * document with many candidate `s` characters was worse still. The
499
+ * wikilink close in {@link indexOfWithin} had the same shape.
500
+ */
501
+ private hasInlineSolveWithin;
502
+ /**
503
+ * `input.indexOf(needle, from)` restricted to `[from, end)`, returning -1
504
+ * when the needle does not occur wholly before `end`. See
505
+ * {@link hasInlineSolveWithin} for why the plain `indexOf` was not enough.
506
+ */
507
+ private indexOfWithin;
459
508
  private classifyFromPositions;
460
509
  /**
461
510
  * Classify a single line of markdown text.
@@ -530,11 +579,11 @@ declare class Lexer {
530
579
  private tokenIdx;
531
580
  /**
532
581
  * @param localeCode - Locale code (e.g., "en", "de"). Defaults to "en".
533
- * @param tokenLookup - Optional TokenLookup from TokenClassRegistry.
534
- * When provided, configures ExpressionLexer to use registry-built
535
- * keyword/unit/phrase lookups instead of internal instance maps.
582
+ * @param _tokenLookup - Ignored. The lexer never read the lookup it was
583
+ * handed; the parameter stays so existing callers compile.
584
+ * @deprecated Removed in 3.0.
536
585
  */
537
- constructor(localeCode?: string, tokenLookup?: TokenLookup);
586
+ constructor(localeCode?: string, _tokenLookup?: TokenLookup);
538
587
  reset(input: string, state?: LexerState): void;
539
588
  /**
540
589
  * Classify a single line of markdown text (Phase B).
@@ -1,4 +1,4 @@
1
- import { I as IEnginePackage } from './PackageRegistry-CDEgfhT_.js';
1
+ import { I as IEnginePackage } from './PackageRegistry-DT-x32OM.js';
2
2
 
3
3
  /**
4
4
  * Package load-time compatibility checking, the "detect overlapping
@@ -1,4 +1,4 @@
1
- import { I as IEnginePackage } from './PackageRegistry-B9-7fJGH.cjs';
1
+ import { I as IEnginePackage } from './PackageRegistry-Dvn4i2QK.cjs';
2
2
 
3
3
  /**
4
4
  * Package load-time compatibility checking, the "detect overlapping
@@ -1,12 +1,12 @@
1
- import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from './Parselet-BwDqKFfK.js';
1
+ import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from './Parselet-DqTbbtWA.js';
2
2
  import { V as Value, a as ValueType } from './Value-Ds3Gy07C.js';
3
- import { M as MarkdownLineType, L as Lexer, e as TokenCategory, c as LexerVocabulary } from './Lexer-CqagTewQ.js';
3
+ import { M as MarkdownLineType, L as Lexer, e as TokenCategory, c as LexerVocabulary } from './Lexer-Bmctxl0n.js';
4
4
  import { IAsyncResolver } from './resolvers.js';
5
- import { T as TokenFusion, c as TokenNormalizer, a as NormalizerRule } from './TokenNormalizer-OTPS0Otq.js';
6
- import { D as DependencyGraph, V as VM, a as DagSnapshot, b as EngineContext, S as ScopeManager, P as PluginFunctionHandler } from './ScopeManager-BQBhlDAu.js';
7
- import { a as BytecodeProgram } from './BytecodeBuilder-aqVa7Plx.js';
5
+ import { T as TokenFusion, c as TokenNormalizer, a as NormalizerRule } from './TokenNormalizer-CLBtiE8M.js';
6
+ import { D as DependencyGraph, V as VM, a as DagSnapshot, b as EngineContext, S as ScopeManager, P as PluginFunctionHandler } from './ScopeManager-B636am7Y.js';
7
+ import { a as BytecodeProgram } from './BytecodeBuilder-DSWKZi4f.js';
8
8
  import { QueryClient } from '@tanstack/query-core';
9
- import { E as EngineError } from './EngineError-D1kXsjIj.js';
9
+ import { E as EngineError } from './EngineError-fR1K1rvx.js';
10
10
  import { a as DiagnosticReportJSON, D as DiagnosticPipeline } from './pipeline-DTqGLPsV.js';
11
11
  import { T as Token } from './Token-CbP_OutD.js';
12
12
  import { E as EngineConfigOverride, a as EngineConfig } from './Configuration-DnzYPmoK.js';
@@ -1640,6 +1640,13 @@ declare class ExpressionEngine {
1640
1640
  * every `name(` call word instead of one rule per package.
1641
1641
  */
1642
1642
  private callFusions;
1643
+ /**
1644
+ * Which packages claim each call-fusion word, in registration order. The
1645
+ * newest claim is the one {@link callFusions} holds; removing a package
1646
+ * hands the word back to the previous claimant rather than deleting it
1647
+ * for both.
1648
+ */
1649
+ private callFusionOwners;
1643
1650
  /**
1644
1651
  * Incremental index behind the package-compatibility check. Kept in step
1645
1652
  * with {@link registeredPackages} so each `registerPackage` costs O(that
@@ -1806,6 +1813,45 @@ declare class ExpressionEngine {
1806
1813
  /** TanStack Query client, injected into resolvers for cache reads/writes. */
1807
1814
  readonly queryClient: QueryClient;
1808
1815
  private bytecodeCache;
1816
+ /**
1817
+ * What the front half of the pipeline produced for a cached program: the
1818
+ * normalised tokens the program was compiled from and the reads and
1819
+ * writes extracted from them. Keyed like {@link bytecodeCache} and kept
1820
+ * in step with it (same eviction, same clears), so a hit here means a hit
1821
+ * there.
1822
+ *
1823
+ * A bytecode-cache hit used to pay for the whole front half anyway: the
1824
+ * length check, lexing, normalising, the complexity score, the three
1825
+ * effectful-grammar tries and the reads/writes extraction all ran before
1826
+ * `bytecodeCache.get`, and on a cache-hit evaluation of an ordinary line
1827
+ * lexing and normalising were about two fifths of the cost. Everything
1828
+ * the later stages read from the front half is remembered here instead,
1829
+ * so a hit skips straight to the async preflight and the VM. Entries
1830
+ * restored from a snapshot carry a program but no front half; the first
1831
+ * evaluation after a restore runs the full front half once and fills it.
1832
+ * The memory is a few hundred bytes per cached expression, bounded by the
1833
+ * same `defaultCacheSize` as the programs.
1834
+ */
1835
+ private compiledFrontHalf;
1836
+ /**
1837
+ * Expressions whose last parse threw, with what the front half produced
1838
+ * for them. A line that does not parse is the ordinary state of a line
1839
+ * being typed, and every re-evaluation of the document lexed, normalised
1840
+ * and re-parsed it, paying for the throw each time. The entry is used
1841
+ * only after the effectful-grammar tries, which still run because they
1842
+ * read the VM (a stored equation, a running total), so nothing that
1843
+ * depends on document state is skipped: only the pure front half and the
1844
+ * parse that is known to fail. The error object is the one first thrown,
1845
+ * so its timestamp is the first failure's. Cleared with the compiled
1846
+ * caches, because the vocabulary and parselet changes that would change
1847
+ * a program can change whether a line parses at all. Bounded by the same
1848
+ * `defaultCacheSize` as the programs.
1849
+ */
1850
+ private failedParses;
1851
+ /** Whether `expression` can be answered without lexing or normalising: both compiled caches hold it, or its parse is known to fail. */
1852
+ private frontHalfCached;
1853
+ /** Drop every cached program, its front half and every remembered parse failure. Every invalidation goes through here so the three cannot drift. */
1854
+ private clearCompiledCache;
1809
1855
  /**
1810
1856
  * Insert into the bytecode cache, evicting the oldest entry when full.
1811
1857
  *
@@ -1818,6 +1864,8 @@ declare class ExpressionEngine {
1818
1864
  * the bytecode-cache benefit on re-evaluation regardless of config.
1819
1865
  */
1820
1866
  private cacheBytecode;
1867
+ /** Remember a parse failure, evicting the oldest when full: bounded like the program cache. */
1868
+ private rememberFailedParse;
1821
1869
  private pluginFunctionIndexByName;
1822
1870
  private builderPool;
1823
1871
  private builderPoolIndex;