solve-engine 2.23.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 (226) hide show
  1. package/README.md +82 -32
  2. package/dist/{BytecodeBuilder-aqVa7Plx.d.cts → BytecodeBuilder-DSWKZi4f.d.cts} +11 -9
  3. package/dist/{BytecodeBuilder-aqVa7Plx.d.ts → BytecodeBuilder-DSWKZi4f.d.ts} +11 -9
  4. package/dist/{Configuration-BGQn-cJ8.d.cts → Configuration-DnzYPmoK.d.cts} +33 -0
  5. package/dist/{Configuration-BGQn-cJ8.d.ts → Configuration-DnzYPmoK.d.ts} +33 -0
  6. package/dist/{EngineError-DTk7I7hZ.d.cts → EngineError-fR1K1rvx.d.cts} +6 -0
  7. package/dist/{EngineError-DTk7I7hZ.d.ts → EngineError-fR1K1rvx.d.ts} +6 -0
  8. package/dist/{Lexer-CqagTewQ.d.ts → Lexer-Bmctxl0n.d.ts} +84 -35
  9. package/dist/{Lexer-CCHFDcgP.d.cts → Lexer-aB95pjoX.d.cts} +84 -35
  10. package/dist/{PackageCompatibility-BGXSFuL9.d.ts → PackageCompatibility-BludTs7v.d.ts} +1 -1
  11. package/dist/{PackageCompatibility-BZCqaTOO.d.cts → PackageCompatibility-CUVg8tmt.d.cts} +1 -1
  12. package/dist/{PackageRegistry-BRoVOYzg.d.ts → PackageRegistry-DT-x32OM.d.ts} +67 -14
  13. package/dist/{PackageRegistry-rMGnTh7W.d.cts → PackageRegistry-Dvn4i2QK.d.cts} +67 -14
  14. package/dist/{Parselet-Bkbp9CKD.d.cts → Parselet-BawfGuTF.d.cts} +8 -2
  15. package/dist/{Parselet-B-WyUtX4.d.ts → Parselet-DqTbbtWA.d.ts} +8 -2
  16. package/dist/{ScopeManager-gB9UengK.d.ts → ScopeManager-B636am7Y.d.ts} +39 -8
  17. package/dist/{ScopeManager-bCYewWVt.d.cts → ScopeManager-CdwX0VTw.d.cts} +39 -8
  18. package/dist/{TokenNormalizer-OTPS0Otq.d.ts → TokenNormalizer-CLBtiE8M.d.ts} +45 -26
  19. package/dist/{TokenNormalizer-MXaKLJ_m.d.cts → TokenNormalizer-CjTDmHgB.d.cts} +45 -26
  20. package/dist/{VMCheckpoints-DGar9Yg8.d.cts → VMCheckpoints-CYqeq8IV.d.cts} +3 -3
  21. package/dist/{VMCheckpoints-BDRY1Kx8.d.ts → VMCheckpoints-DDlxNssY.d.ts} +3 -3
  22. package/dist/{Value-DCTqTSeP.d.cts → Value-Ds3Gy07C.d.cts} +1 -1
  23. package/dist/{Value-DCTqTSeP.d.ts → Value-Ds3Gy07C.d.ts} +1 -1
  24. package/dist/{WorkerError-DGBNM3gA.d.cts → WorkerError-B1dF3VYT.d.cts} +1 -1
  25. package/dist/{WorkerError-DpZgKRks.d.ts → WorkerError-dneODN-u.d.ts} +1 -1
  26. package/dist/{chunk-YAONCEAW.cjs → chunk-4BAUQE6A.cjs} +3 -3
  27. package/dist/{chunk-YAONCEAW.cjs.map → chunk-4BAUQE6A.cjs.map} +1 -1
  28. package/dist/chunk-4BRTUFTH.js +2 -0
  29. package/dist/chunk-4BRTUFTH.js.map +1 -0
  30. package/dist/chunk-4NZXCHQL.cjs +2 -0
  31. package/dist/chunk-4NZXCHQL.cjs.map +1 -0
  32. package/dist/chunk-57PSJQQW.js +3 -0
  33. package/dist/chunk-57PSJQQW.js.map +1 -0
  34. package/dist/chunk-634ILVMP.cjs +2 -0
  35. package/dist/chunk-634ILVMP.cjs.map +1 -0
  36. package/dist/{chunk-HMC7FJO7.js → chunk-63ZOOUOL.js} +2 -2
  37. package/dist/{chunk-HMC7FJO7.js.map → chunk-63ZOOUOL.js.map} +1 -1
  38. package/dist/chunk-72PYPENZ.js +2 -0
  39. package/dist/chunk-72PYPENZ.js.map +1 -0
  40. package/dist/chunk-77KI7AKJ.cjs +2 -0
  41. package/dist/chunk-77KI7AKJ.cjs.map +1 -0
  42. package/dist/chunk-7DP57BA7.js +2 -0
  43. package/dist/chunk-7DP57BA7.js.map +1 -0
  44. package/dist/chunk-7N3IJSWF.js +5 -0
  45. package/dist/chunk-7N3IJSWF.js.map +1 -0
  46. package/dist/chunk-BMXYR5LI.cjs +2 -0
  47. package/dist/chunk-BMXYR5LI.cjs.map +1 -0
  48. package/dist/chunk-DUYTXLO2.js +2 -0
  49. package/dist/chunk-DUYTXLO2.js.map +1 -0
  50. package/dist/chunk-EL2XUOWX.js +2 -0
  51. package/dist/chunk-EL2XUOWX.js.map +1 -0
  52. package/dist/chunk-FYAMQFZZ.cjs +3 -0
  53. package/dist/chunk-FYAMQFZZ.cjs.map +1 -0
  54. package/dist/chunk-FYTB7SDP.cjs +2 -0
  55. package/dist/chunk-FYTB7SDP.cjs.map +1 -0
  56. package/dist/chunk-GBCN7FNV.cjs +2 -0
  57. package/dist/chunk-GBCN7FNV.cjs.map +1 -0
  58. package/dist/{chunk-7E7NXKQ5.js → chunk-IJHL3LMH.js} +2 -2
  59. package/dist/chunk-IJHL3LMH.js.map +1 -0
  60. package/dist/{chunk-4ETS224G.js → chunk-JFZ5SLF7.js} +2 -2
  61. package/dist/{chunk-4ETS224G.js.map → chunk-JFZ5SLF7.js.map} +1 -1
  62. package/dist/chunk-JSM3O7EM.cjs +2 -0
  63. package/dist/chunk-JSM3O7EM.cjs.map +1 -0
  64. package/dist/chunk-MZMA32NE.js +3 -0
  65. package/dist/chunk-MZMA32NE.js.map +1 -0
  66. package/dist/chunk-N4SSR7Q6.js +2 -0
  67. package/dist/chunk-N4SSR7Q6.js.map +1 -0
  68. package/dist/{chunk-TL545PXW.cjs → chunk-NEGZZB7F.cjs} +2 -2
  69. package/dist/{chunk-TL545PXW.cjs.map → chunk-NEGZZB7F.cjs.map} +1 -1
  70. package/dist/{chunk-3GR46UOX.js → chunk-NMU5E7W7.js} +2 -2
  71. package/dist/{chunk-3GR46UOX.js.map → chunk-NMU5E7W7.js.map} +1 -1
  72. package/dist/chunk-OB2VX4IL.js +2 -0
  73. package/dist/chunk-OB2VX4IL.js.map +1 -0
  74. package/dist/{chunk-MRRMIBHE.js → chunk-OONQ3V3I.js} +2 -2
  75. package/dist/{chunk-MRRMIBHE.js.map → chunk-OONQ3V3I.js.map} +1 -1
  76. package/dist/{chunk-N37PUZJR.cjs → chunk-RAPEZR5L.cjs} +2 -2
  77. package/dist/{chunk-N37PUZJR.cjs.map → chunk-RAPEZR5L.cjs.map} +1 -1
  78. package/dist/{chunk-KEA5HRL3.js → chunk-RDYXH7ML.js} +2 -2
  79. package/dist/{chunk-KEA5HRL3.js.map → chunk-RDYXH7ML.js.map} +1 -1
  80. package/dist/{chunk-3SSMOVWB.cjs → chunk-SAF3B3AX.cjs} +2 -2
  81. package/dist/{chunk-3SSMOVWB.cjs.map → chunk-SAF3B3AX.cjs.map} +1 -1
  82. package/dist/chunk-SPOVFRZO.cjs +5 -0
  83. package/dist/chunk-SPOVFRZO.cjs.map +1 -0
  84. package/dist/{chunk-75JLWK4L.cjs → chunk-SQHBOWES.cjs} +2 -2
  85. package/dist/{chunk-75JLWK4L.cjs.map → chunk-SQHBOWES.cjs.map} +1 -1
  86. package/dist/chunk-UF5XMZTT.js +2 -0
  87. package/dist/chunk-UF5XMZTT.js.map +1 -0
  88. package/dist/chunk-UV3EUIBT.cjs +3 -0
  89. package/dist/chunk-UV3EUIBT.cjs.map +1 -0
  90. package/dist/chunk-VMSCRVNX.cjs +2 -0
  91. package/dist/chunk-VMSCRVNX.cjs.map +1 -0
  92. package/dist/{chunk-J7ABVGZH.cjs → chunk-WGP4UUO5.cjs} +2 -2
  93. package/dist/{chunk-J7ABVGZH.cjs.map → chunk-WGP4UUO5.cjs.map} +1 -1
  94. package/dist/chunk-XBGK3TTU.cjs +3 -0
  95. package/dist/chunk-XBGK3TTU.cjs.map +1 -0
  96. package/dist/chunk-XHQYHXRE.js +3 -0
  97. package/dist/chunk-XHQYHXRE.js.map +1 -0
  98. package/dist/chunk-XX55RR4Q.js +3 -0
  99. package/dist/chunk-XX55RR4Q.js.map +1 -0
  100. package/dist/chunk-YBQPEWT7.cjs +3 -0
  101. package/dist/chunk-YBQPEWT7.cjs.map +1 -0
  102. package/dist/{chunk-CHSO3DCS.js → chunk-ZDCZ5IDD.js} +3 -3
  103. package/dist/{chunk-CHSO3DCS.js.map → chunk-ZDCZ5IDD.js.map} +1 -1
  104. package/dist/{chunk-LNZNJSRW.cjs → chunk-ZVPZQ66D.cjs} +2 -2
  105. package/dist/chunk-ZVPZQ66D.cjs.map +1 -0
  106. package/dist/constants.cjs +1 -1
  107. package/dist/constants.d.cts +1 -1
  108. package/dist/constants.d.ts +1 -1
  109. package/dist/constants.js +1 -1
  110. package/dist/engine.cjs +1 -1
  111. package/dist/engine.d.cts +12 -12
  112. package/dist/engine.d.ts +12 -12
  113. package/dist/engine.js +1 -1
  114. package/dist/errors.cjs +1 -1
  115. package/dist/errors.d.cts +3 -3
  116. package/dist/errors.d.ts +3 -3
  117. package/dist/errors.js +1 -1
  118. package/dist/format.cjs +1 -1
  119. package/dist/format.d.cts +1 -1
  120. package/dist/format.d.ts +1 -1
  121. package/dist/format.js +1 -1
  122. package/dist/index.cjs +1 -1
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +14 -12
  125. package/dist/index.d.ts +14 -12
  126. package/dist/index.js +1 -1
  127. package/dist/index.js.map +1 -1
  128. package/dist/language.d.cts +11 -11
  129. package/dist/language.d.ts +11 -11
  130. package/dist/lexer.cjs +1 -1
  131. package/dist/lexer.cjs.map +1 -1
  132. package/dist/lexer.d.cts +8 -7
  133. package/dist/lexer.d.ts +8 -7
  134. package/dist/lexer.js +1 -1
  135. package/dist/lexer.js.map +1 -1
  136. package/dist/normalizer.cjs +1 -1
  137. package/dist/normalizer.d.cts +16 -10
  138. package/dist/normalizer.d.ts +16 -10
  139. package/dist/normalizer.js +1 -1
  140. package/dist/packages.cjs +1 -1
  141. package/dist/packages.d.cts +10 -10
  142. package/dist/packages.d.ts +10 -10
  143. package/dist/packages.js +1 -1
  144. package/dist/parser.cjs +1 -1
  145. package/dist/parser.d.cts +4 -4
  146. package/dist/parser.d.ts +4 -4
  147. package/dist/parser.js +1 -1
  148. package/dist/{pipeline-B4wf3M1h.d.cts → pipeline-69q2tsLE.d.cts} +1 -1
  149. package/dist/{pipeline-QIT4iD8f.d.ts → pipeline-DTqGLPsV.d.ts} +1 -1
  150. package/dist/resolvers.cjs +1 -1
  151. package/dist/resolvers.d.cts +19 -3
  152. package/dist/resolvers.d.ts +19 -3
  153. package/dist/resolvers.js +1 -1
  154. package/dist/testing.cjs +2 -2
  155. package/dist/testing.d.cts +11 -11
  156. package/dist/testing.d.ts +11 -11
  157. package/dist/testing.js +1 -1
  158. package/dist/uom.cjs +1 -1
  159. package/dist/uom.d.cts +2 -2
  160. package/dist/uom.d.ts +2 -2
  161. package/dist/uom.js +1 -1
  162. package/dist/vm.cjs +1 -1
  163. package/dist/vm.d.cts +8 -8
  164. package/dist/vm.d.ts +8 -8
  165. package/dist/vm.js +1 -1
  166. package/dist/worker.cjs +2 -2
  167. package/dist/worker.cjs.map +1 -1
  168. package/dist/worker.d.cts +34 -25
  169. package/dist/worker.d.ts +34 -25
  170. package/dist/worker.js +1 -1
  171. package/dist/worker.js.map +1 -1
  172. package/package.json +5 -5
  173. package/dist/chunk-42JXN3BH.js +0 -5
  174. package/dist/chunk-42JXN3BH.js.map +0 -1
  175. package/dist/chunk-4DJJIUQF.js +0 -2
  176. package/dist/chunk-4DJJIUQF.js.map +0 -1
  177. package/dist/chunk-6YM67DWH.cjs +0 -3
  178. package/dist/chunk-6YM67DWH.cjs.map +0 -1
  179. package/dist/chunk-7E7NXKQ5.js.map +0 -1
  180. package/dist/chunk-7SZBEE5R.cjs +0 -2
  181. package/dist/chunk-7SZBEE5R.cjs.map +0 -1
  182. package/dist/chunk-A5M4QZIK.js +0 -2
  183. package/dist/chunk-A5M4QZIK.js.map +0 -1
  184. package/dist/chunk-AF7HQN6I.js +0 -2
  185. package/dist/chunk-AF7HQN6I.js.map +0 -1
  186. package/dist/chunk-ALY4ZMYO.js +0 -2
  187. package/dist/chunk-ALY4ZMYO.js.map +0 -1
  188. package/dist/chunk-BRJ4RT5U.js +0 -2
  189. package/dist/chunk-BRJ4RT5U.js.map +0 -1
  190. package/dist/chunk-CO2BI6WL.cjs +0 -3
  191. package/dist/chunk-CO2BI6WL.cjs.map +0 -1
  192. package/dist/chunk-EN3CDYOS.cjs +0 -2
  193. package/dist/chunk-EN3CDYOS.cjs.map +0 -1
  194. package/dist/chunk-EQDT42JG.js +0 -3
  195. package/dist/chunk-EQDT42JG.js.map +0 -1
  196. package/dist/chunk-FMMT5C5H.cjs +0 -3
  197. package/dist/chunk-FMMT5C5H.cjs.map +0 -1
  198. package/dist/chunk-FP6Z6PYR.js +0 -2
  199. package/dist/chunk-FP6Z6PYR.js.map +0 -1
  200. package/dist/chunk-H3JXNH7X.cjs +0 -3
  201. package/dist/chunk-H3JXNH7X.cjs.map +0 -1
  202. package/dist/chunk-HZOLIL5T.js +0 -3
  203. package/dist/chunk-HZOLIL5T.js.map +0 -1
  204. package/dist/chunk-JVMINMAB.js +0 -2
  205. package/dist/chunk-JVMINMAB.js.map +0 -1
  206. package/dist/chunk-JYLNQOPU.cjs +0 -2
  207. package/dist/chunk-JYLNQOPU.cjs.map +0 -1
  208. package/dist/chunk-KRXO3MOW.cjs +0 -2
  209. package/dist/chunk-KRXO3MOW.cjs.map +0 -1
  210. package/dist/chunk-KVLJDOZ7.cjs +0 -5
  211. package/dist/chunk-KVLJDOZ7.cjs.map +0 -1
  212. package/dist/chunk-LNZNJSRW.cjs.map +0 -1
  213. package/dist/chunk-O3BDXOQJ.js +0 -2
  214. package/dist/chunk-O3BDXOQJ.js.map +0 -1
  215. package/dist/chunk-PIZQIQVM.js +0 -3
  216. package/dist/chunk-PIZQIQVM.js.map +0 -1
  217. package/dist/chunk-QTSDIDAS.js +0 -3
  218. package/dist/chunk-QTSDIDAS.js.map +0 -1
  219. package/dist/chunk-SGIDBZ4T.cjs +0 -2
  220. package/dist/chunk-SGIDBZ4T.cjs.map +0 -1
  221. package/dist/chunk-UNKSCVIL.cjs +0 -2
  222. package/dist/chunk-UNKSCVIL.cjs.map +0 -1
  223. package/dist/chunk-USQ4KL6S.cjs +0 -2
  224. package/dist/chunk-USQ4KL6S.cjs.map +0 -1
  225. package/dist/chunk-XDXIPVZB.cjs +0 -2
  226. package/dist/chunk-XDXIPVZB.cjs.map +0 -1
package/README.md CHANGED
@@ -52,8 +52,14 @@ const value = engine.evaluateExpression("2 + 2 * 10");
52
52
  console.log(value.toNumber()); // 22
53
53
  ```
54
54
 
55
- `evaluateExpression` throws an `EngineError` (see `solve-engine/errors`) on a parse or
56
- evaluation failure, wrap calls with untrusted input in a `try`/`catch`.
55
+ Two kinds of failure, deliberately different. A line the parser cannot read
56
+ (`10 +`), or one that names a variable no line defined, throws an `EngineError`
57
+ (see `solve-engine/errors`), so wrap calls on untrusted input in a `try`/`catch`.
58
+ A line the engine can run but cannot answer (an impossible conversion, a rate it
59
+ has no data for, a live value still loading) comes back as a `Value` whose
60
+ `isError()` or `isPending()` says so, never as a throw, which is the right shape
61
+ for input being typed one character at a time. Check `isFault()` before
62
+ `toNumber()`: a faulted value reads as `0` through it.
57
63
 
58
64
  For line-oriented input (e.g. a document made of multiple expressions, some referencing
59
65
  variables defined on earlier lines), use `evaluateLine`/`parseDocument` instead, see the
@@ -104,13 +110,15 @@ they are:
104
110
 
105
111
  | Subpath | Purpose |
106
112
  |---|---|
107
- | `solve-engine` | Start here, `ExpressionEngine`, `createEngine`, `IEnginePackage`. |
113
+ | `solve-engine` | Start here: `createEngine`, `ExpressionEngine`, `defineFunction`, `IEnginePackage`, and the result surface `Value`, `ValueType`, `formatValue`. |
108
114
  | `solve-engine/engine` | `ExpressionEngine` and its supporting types (`Explanation`, `EngineSnapshot`, etc.) directly, without the package-registration wrapper. |
109
- | `solve-engine/vm` | The bytecode VM: `Value`/`ValueType`, opcode dispatch, `allocatePluginFunctionIndex`. |
110
- | `solve-engine/format` | Turning a `Value` into a display string (numbers, dates, units, vectors, ...). |
115
+ | `solve-engine/vm` | The bytecode VM: `Value`/`ValueType`, the value constructors, opcode dispatch. |
116
+ | `solve-engine/format` | Turning a `Value` into a display string (numbers, dates, units, vectors, ...) and the formatting settings. |
111
117
  | `solve-engine/language` | Editor-agnostic language service: token categories, completions, highlighting. |
112
118
  | `solve-engine/packages` | The built-in packages (arithmetic, datetime, time, dice, uom, currency, vector, conditionals, converters, mathphrases, ...). |
113
- | `solve-engine/constants` | Engine configuration types and defaults (`EngineConfig`, `VMConfig`, ...). |
119
+ | `solve-engine/constants` | Engine configuration types and defaults (`EngineConfig`, `VMConfig`, `NetworkConfig`, ...). |
120
+ | `solve-engine/worker` | Off-main-thread evaluation: `createWorkerEngine`, `startWorkerRuntime`, the transports and the result DTOs. |
121
+ | `solve-engine/testing` | A test kit for package authors: `createTestEngine`, `expectExpression`, `expectPackage`. |
114
122
 
115
123
  The following subpaths are **advanced-public**, everything a third-party package author
116
124
  needs to extend the engine, but with a looser stability contract than the tier above (these
@@ -132,32 +140,70 @@ Anything not listed above (`telemetry`, `cache`, `diagnostics`, `types`, `worker
132
140
  internal and not part of the package's public contract, it may change or disappear between
133
141
  minor versions without notice.
134
142
 
143
+ ## Adding a function
144
+
145
+ The shortest way to teach the engine a new name is `defineFunction`: declare the
146
+ name, the argument types and the return type, and get back a package that plugs
147
+ in beside the built-ins. No parser, no bytecode.
148
+
149
+ ```typescript
150
+ import { createEngine, defineFunction } from "solve-engine";
151
+
152
+ const vat = defineFunction({
153
+ name: "vat",
154
+ args: [{ name: "amount", type: "number" }],
155
+ returns: "number",
156
+ call: (amount) => amount * 1.2,
157
+ });
158
+
159
+ const engine = createEngine({ extraPackages: [vat] });
160
+ engine.evaluateExpression("vat(100)").toNumber(); // 120
161
+ ```
162
+
163
+ Arguments are a fixed list of `number`, `string` or `boolean`, and `call` is
164
+ synchronous; a mismatched call produces a structured error naming the argument.
165
+ Anything beyond that shape (a unit-bearing argument, a phrase rather than a
166
+ call, live data) is a package proper.
167
+
135
168
  ## Authoring a package
136
169
 
137
- A **package** (`IEnginePackage`) is a plain data descriptor bundling everything needed to
138
- extend the engine with a new domain: custom tokens, parselets, VM opcode handlers, and
139
- optional async resolvers. See
140
- [`solve-engine/api`'s `IEnginePackage`](https://github.com/LiamRiddell/solve-engine/blob/main/packages/engine/src/api/PackageRegistry.ts) for the full field list
141
- with inline documentation and examples for each field.
170
+ A **package** (`IEnginePackage`) is a plain data descriptor bundling everything
171
+ needed to extend the engine with a new domain: token vocabulary, normaliser
172
+ rules, parselets, plugin functions, `as` converters, completions, and optional
173
+ async resolvers. The [package author guides](https://liamriddell.github.io/solve-engine/packages/authoring-a-package/)
174
+ walk through each extension point; [`IEnginePackage`](https://github.com/LiamRiddell/solve-engine/blob/main/packages/engine/src/api/PackageRegistry.ts)
175
+ carries the full field list with inline documentation.
142
176
 
143
- Minimal shape:
177
+ Minimal shape, a keyword that calls a function:
144
178
 
145
179
  ```typescript
146
- import { allocatePluginFunctionIndex } from "solve-engine/vm";
147
180
  import type { IEnginePackage } from "solve-engine";
148
-
149
- const MY_FN_IDX = allocatePluginFunctionIndex();
181
+ import type { PrefixParselet } from "solve-engine/parser";
182
+ import { stringValue } from "solve-engine/vm";
183
+
184
+ const reverseParselet: PrefixParselet = {
185
+ category: "Text",
186
+ parse(parser, _token, builder) {
187
+ parser.consume("LPAREN");
188
+ parser.parseExpression(0, builder);
189
+ parser.consume("RPAREN");
190
+ // By name; the engine assigns the index when the package registers.
191
+ builder.emitPluginCall("reverse", 1);
192
+ },
193
+ };
150
194
 
151
195
  export const MY_PACKAGE: IEnginePackage = {
152
- name: "MyPackage",
153
- // engineVersion: "^0.1.0", // optional, see below
154
- prefixParselets: [{ tokenType: "MY_FUNC", parselet: new MyParselet() }],
155
- pluginFunctions: [{ index: MY_FN_IDX, handler: (args) => /* ... */ }],
196
+ name: "my-package",
197
+ // engineVersion: "^2.0.0", // optional, see below
198
+ lexerVocabulary: { keywords: { reverse: "REVERSE_FN" } },
199
+ prefixParselets: { REVERSE_FN: reverseParselet },
200
+ pluginFunctions: { reverse: ([text]) => stringValue([...String(text.value)].reverse().join("")) },
156
201
  };
157
202
  ```
158
203
 
159
- Register it either as one of the packages passed to the `ExpressionEngine` constructor, or
160
- at runtime via `ExpressionEngine.registerPackage()` / `unregisterPackage()`.
204
+ Register it as one of the packages passed to the `ExpressionEngine` constructor
205
+ (or `createEngine`'s `extraPackages`), or at runtime via
206
+ `ExpressionEngine.registerPackage()` / `unregisterPackage()`.
161
207
 
162
208
  ### Declaring engine-version compatibility
163
209
 
@@ -210,17 +256,21 @@ published package, see `files` in `package.json`, only `dist/` ships):
210
256
 
211
257
  ## Known limitations
212
258
 
213
- **Cross-instance isolation is partial.** Plugin functions and the opcode registry are
214
- now owned per `ExpressionEngine`, so two engines with different package sets no longer
215
- interfere across those. The lexer and the currency exchange rates
216
- are still module-level singletons, so full isolation between two engines in one process
217
- cannot yet be assumed. Tracked as "L1, EngineContext"; three of its five migrations have
218
- landed and the remaining two are a prerequisite for 1.0.0 proper.
219
-
220
- **Async results need a host hook.** `AsyncResolutionBatcher.onLineResult` is the only
221
- mechanism that patches a resolved async value back into the document model, and it is not
222
- wired inside the package. A host that does not supply it gets async values that never
223
- resolve, with no error to explain why.
259
+ **Exchange rates are shared across engines, by design.** Plugin functions, the
260
+ opcode registry and the lexer are owned per `ExpressionEngine`, so two engines
261
+ with different package sets do not interfere. The currency rate cache is the one
262
+ deliberate exception: rates are market data with a fifteen-minute freshness
263
+ window, and two engines in one process fetching the same pair separately could
264
+ disagree about it. A host that primes rates (`currencyExchangeService.primeRates`)
265
+ primes them for every engine in the process.
266
+
267
+ **A live value reaches the document only through the event stream.** When a
268
+ fetch lands, the engine does not push the new value at you; it emits a
269
+ `lines-updated` event on `engine.getEventStream()` naming the lines to
270
+ re-evaluate. A host that reads neither that stream nor sets
271
+ `AsyncResolutionBatcher.onLineResult` sees the line stay pending, and a warning
272
+ says so once. The [async guide](https://liamriddell.github.io/solve-engine/guide/async-and-live-data/)
273
+ shows the loop.
224
274
 
225
275
  ## Development
226
276
 
@@ -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
  /**
@@ -207,6 +207,37 @@ interface BackgroundRefreshConfig {
207
207
  /** Master switch. When false, no timers run and every value refreshes only on re-evaluation. */
208
208
  readonly enabled: boolean;
209
209
  }
210
+ /**
211
+ * Whether the engine may reach the network at all.
212
+ *
213
+ * On by default, which is the historic behaviour: a city name or a currency
214
+ * pair in a line fetches live data from a public endpoint as soon as the
215
+ * line evaluates. A host embedding the engine somewhere that must not make
216
+ * outbound requests (an offline document, a sandboxed evaluation, a tenant
217
+ * with egress rules) switches it off here and gets the same engine with no
218
+ * egress: every live-data form answers with a `NETWORK_DISABLED` error
219
+ * naming the setting, instead of a request.
220
+ *
221
+ * The gate closes before any request is made. It stops every async
222
+ * resolver a package registers (weather, currency, stocks, crypto,
223
+ * knowledge, and any host-supplied one) unless the resolver declares
224
+ * itself `local` (it reads engine state, never a network; the engine's own
225
+ * global-variable resolver is the one built-in case). It also refuses the
226
+ * result of a plugin function that returns a promise. That last case is a
227
+ * boundary rather than a guarantee: such a function has already run by the
228
+ * time the engine sees the promise, so a package that fetches inside a
229
+ * plugin function rather than through an async resolver may still have
230
+ * started its request. The documented shape for live data is the resolver
231
+ * (`createQueryResolver`), which this gate stops cold.
232
+ *
233
+ * Rates a host primes by hand (`currencyExchangeService.primeRates`) keep
234
+ * working with the network off, which is what an offline host with its own
235
+ * rate table wants.
236
+ */
237
+ interface NetworkConfig {
238
+ /** Master switch. When false, no async resolver runs and no live value is fetched. */
239
+ readonly enabled: boolean;
240
+ }
210
241
  /**
211
242
  * Virtual Machine configuration.
212
243
  * Controls the internal bytecode VM that executes compiled expressions.
@@ -321,6 +352,8 @@ interface EngineConfig {
321
352
  readonly diagnostic: DiagnosticConfig;
322
353
  /** Proactive background refresh of live async values */
323
354
  readonly backgroundRefresh: BackgroundRefreshConfig;
355
+ /** Whether live data may be fetched at all. See {@link NetworkConfig}. */
356
+ readonly network: NetworkConfig;
324
357
  }
325
358
  /**
326
359
  * A partial {@link EngineConfig} override, one section at a time.
@@ -207,6 +207,37 @@ interface BackgroundRefreshConfig {
207
207
  /** Master switch. When false, no timers run and every value refreshes only on re-evaluation. */
208
208
  readonly enabled: boolean;
209
209
  }
210
+ /**
211
+ * Whether the engine may reach the network at all.
212
+ *
213
+ * On by default, which is the historic behaviour: a city name or a currency
214
+ * pair in a line fetches live data from a public endpoint as soon as the
215
+ * line evaluates. A host embedding the engine somewhere that must not make
216
+ * outbound requests (an offline document, a sandboxed evaluation, a tenant
217
+ * with egress rules) switches it off here and gets the same engine with no
218
+ * egress: every live-data form answers with a `NETWORK_DISABLED` error
219
+ * naming the setting, instead of a request.
220
+ *
221
+ * The gate closes before any request is made. It stops every async
222
+ * resolver a package registers (weather, currency, stocks, crypto,
223
+ * knowledge, and any host-supplied one) unless the resolver declares
224
+ * itself `local` (it reads engine state, never a network; the engine's own
225
+ * global-variable resolver is the one built-in case). It also refuses the
226
+ * result of a plugin function that returns a promise. That last case is a
227
+ * boundary rather than a guarantee: such a function has already run by the
228
+ * time the engine sees the promise, so a package that fetches inside a
229
+ * plugin function rather than through an async resolver may still have
230
+ * started its request. The documented shape for live data is the resolver
231
+ * (`createQueryResolver`), which this gate stops cold.
232
+ *
233
+ * Rates a host primes by hand (`currencyExchangeService.primeRates`) keep
234
+ * working with the network off, which is what an offline host with its own
235
+ * rate table wants.
236
+ */
237
+ interface NetworkConfig {
238
+ /** Master switch. When false, no async resolver runs and no live value is fetched. */
239
+ readonly enabled: boolean;
240
+ }
210
241
  /**
211
242
  * Virtual Machine configuration.
212
243
  * Controls the internal bytecode VM that executes compiled expressions.
@@ -321,6 +352,8 @@ interface EngineConfig {
321
352
  readonly diagnostic: DiagnosticConfig;
322
353
  /** Proactive background refresh of live async values */
323
354
  readonly backgroundRefresh: BackgroundRefreshConfig;
355
+ /** Whether live data may be fetched at all. See {@link NetworkConfig}. */
356
+ readonly network: NetworkConfig;
324
357
  }
325
358
  /**
326
359
  * A partial {@link EngineConfig} override, one section at a time.
@@ -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";
@@ -103,6 +105,8 @@ declare const CoreErrorCodes: {
103
105
  readonly INTERNAL_MISSING_FUNCTION_BODY: "INTERNAL_MISSING_FUNCTION_BODY";
104
106
  /** `MAP_INVOKE`/`REDUCE_INVOKE`'s anonymous-body-index lookup failing. Same class as `INTERNAL_MISSING_FUNCTION_BODY` above, for the `anonymousBodies` side-table instead of `userFunctionBodies`. */
105
107
  readonly INTERNAL_MISSING_ANONYMOUS_BODY: "INTERNAL_MISSING_ANONYMOUS_BODY";
108
+ /** `CALL_BUILTIN` naming an index no builtin is registered at. It used to pop its arguments and push nothing, so the opcode after it read a neighbour's operand as its own; now an Error value on the stack, the shape `map`/`reduce` already used for the same fault. Bytecode-level, so unreachable from source without a compiler or snapshot fault. */
109
+ readonly UNKNOWN_BUILTIN_FUNCTION: "UNKNOWN_BUILTIN_FUNCTION";
106
110
  /** An operand byte read past the end of the stream: the program ends in the middle of an instruction. */
107
111
  readonly MALFORMED_BYTECODE_TRUNCATED: "MALFORMED_BYTECODE_TRUNCATED";
108
112
  /** A constant-pool operand indexing a `numbers`/`strings` entry that does not exist, or that is not of the pool's type. */
@@ -164,6 +168,8 @@ declare const CoreErrorCodes: {
164
168
  readonly UNKNOWN_SAVINGS_PERIOD: "UNKNOWN_SAVINGS_PERIOD";
165
169
  /** Colon-separated numbers that are not a time any clock can show ("24:00", "9:60", "100:5"). Raised by the labeled-line fallback, which used to answer them with whatever stood after the colon. */
166
170
  readonly INVALID_TIME_LITERAL: "INVALID_TIME_LITERAL";
171
+ /** A live-data form evaluated on an engine whose host switched the network off (`network.enabled: false`, see `constants/Configuration.ts`'s `NetworkConfig`). A recoverable Error value, raised by the VM for a currency conversion with no primed rate and for a plugin function that returned a promise, and by `createQueryResolver`'s plugin function when its preflight was skipped. Names the setting, so the reader knows it is policy rather than an outage. */
172
+ readonly NETWORK_DISABLED: "NETWORK_DISABLED";
167
173
  /** `fromJSON()` handed an object that is not a snapshot at all, or whose serialised-shape version does not match this engine's reader. The versioning gate that refuses an incompatible snapshot clearly rather than restoring it wrongly. See `engine/EngineSnapshot.ts`'s `assertRestorable()`. */
168
174
  readonly SNAPSHOT_VERSION_MISMATCH: "SNAPSHOT_VERSION_MISMATCH";
169
175
  /** A snapshot with the right envelope but internally inconsistent contents (an unrecognised number sentinel, an unknown value tag). Distinct from a version mismatch: the format is right, the payload is not. */
@@ -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";
@@ -103,6 +105,8 @@ declare const CoreErrorCodes: {
103
105
  readonly INTERNAL_MISSING_FUNCTION_BODY: "INTERNAL_MISSING_FUNCTION_BODY";
104
106
  /** `MAP_INVOKE`/`REDUCE_INVOKE`'s anonymous-body-index lookup failing. Same class as `INTERNAL_MISSING_FUNCTION_BODY` above, for the `anonymousBodies` side-table instead of `userFunctionBodies`. */
105
107
  readonly INTERNAL_MISSING_ANONYMOUS_BODY: "INTERNAL_MISSING_ANONYMOUS_BODY";
108
+ /** `CALL_BUILTIN` naming an index no builtin is registered at. It used to pop its arguments and push nothing, so the opcode after it read a neighbour's operand as its own; now an Error value on the stack, the shape `map`/`reduce` already used for the same fault. Bytecode-level, so unreachable from source without a compiler or snapshot fault. */
109
+ readonly UNKNOWN_BUILTIN_FUNCTION: "UNKNOWN_BUILTIN_FUNCTION";
106
110
  /** An operand byte read past the end of the stream: the program ends in the middle of an instruction. */
107
111
  readonly MALFORMED_BYTECODE_TRUNCATED: "MALFORMED_BYTECODE_TRUNCATED";
108
112
  /** A constant-pool operand indexing a `numbers`/`strings` entry that does not exist, or that is not of the pool's type. */
@@ -164,6 +168,8 @@ declare const CoreErrorCodes: {
164
168
  readonly UNKNOWN_SAVINGS_PERIOD: "UNKNOWN_SAVINGS_PERIOD";
165
169
  /** Colon-separated numbers that are not a time any clock can show ("24:00", "9:60", "100:5"). Raised by the labeled-line fallback, which used to answer them with whatever stood after the colon. */
166
170
  readonly INVALID_TIME_LITERAL: "INVALID_TIME_LITERAL";
171
+ /** A live-data form evaluated on an engine whose host switched the network off (`network.enabled: false`, see `constants/Configuration.ts`'s `NetworkConfig`). A recoverable Error value, raised by the VM for a currency conversion with no primed rate and for a plugin function that returned a promise, and by `createQueryResolver`'s plugin function when its preflight was skipped. Names the setting, so the reader knows it is policy rather than an outage. */
172
+ readonly NETWORK_DISABLED: "NETWORK_DISABLED";
167
173
  /** `fromJSON()` handed an object that is not a snapshot at all, or whose serialised-shape version does not match this engine's reader. The versioning gate that refuses an incompatible snapshot clearly rather than restoring it wrongly. See `engine/EngineSnapshot.ts`'s `assertRestorable()`. */
168
174
  readonly SNAPSHOT_VERSION_MISMATCH: "SNAPSHOT_VERSION_MISMATCH";
169
175
  /** A snapshot with the right envelope but internally inconsistent contents (an unrecognised number sentinel, an unknown value tag). Distinct from a version mismatch: the format is right, the payload is not. */
@@ -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).