solve-engine 2.23.0 → 2.24.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 (202) hide show
  1. package/README.md +82 -32
  2. package/dist/{Configuration-BGQn-cJ8.d.cts → Configuration-DnzYPmoK.d.cts} +33 -0
  3. package/dist/{Configuration-BGQn-cJ8.d.ts → Configuration-DnzYPmoK.d.ts} +33 -0
  4. package/dist/{EngineError-DTk7I7hZ.d.cts → EngineError-D1kXsjIj.d.cts} +4 -0
  5. package/dist/{EngineError-DTk7I7hZ.d.ts → EngineError-D1kXsjIj.d.ts} +4 -0
  6. package/dist/{PackageCompatibility-BGXSFuL9.d.ts → PackageCompatibility-Bq82y0ZN.d.ts} +1 -1
  7. package/dist/{PackageCompatibility-BZCqaTOO.d.cts → PackageCompatibility-CeAPVHZr.d.cts} +1 -1
  8. package/dist/{PackageRegistry-rMGnTh7W.d.cts → PackageRegistry-B9-7fJGH.d.cts} +16 -11
  9. package/dist/{PackageRegistry-BRoVOYzg.d.ts → PackageRegistry-CDEgfhT_.d.ts} +16 -11
  10. package/dist/{Parselet-Bkbp9CKD.d.cts → Parselet-4oH5OuRt.d.cts} +1 -1
  11. package/dist/{Parselet-B-WyUtX4.d.ts → Parselet-BwDqKFfK.d.ts} +1 -1
  12. package/dist/{ScopeManager-gB9UengK.d.ts → ScopeManager-BQBhlDAu.d.ts} +38 -7
  13. package/dist/{ScopeManager-bCYewWVt.d.cts → ScopeManager-DrXB3Nvm.d.cts} +38 -7
  14. package/dist/{VMCheckpoints-DGar9Yg8.d.cts → VMCheckpoints-BK32PTl2.d.cts} +2 -2
  15. package/dist/{VMCheckpoints-BDRY1Kx8.d.ts → VMCheckpoints-C5jC92o3.d.ts} +2 -2
  16. package/dist/{Value-DCTqTSeP.d.cts → Value-Ds3Gy07C.d.cts} +1 -1
  17. package/dist/{Value-DCTqTSeP.d.ts → Value-Ds3Gy07C.d.ts} +1 -1
  18. package/dist/{WorkerError-DpZgKRks.d.ts → WorkerError-CVQbs6_D.d.ts} +1 -1
  19. package/dist/{WorkerError-DGBNM3gA.d.cts → WorkerError-DaNWQFp1.d.cts} +1 -1
  20. package/dist/chunk-2BXNZM3G.js +2 -0
  21. package/dist/chunk-2BXNZM3G.js.map +1 -0
  22. package/dist/chunk-2JSNWYYA.cjs +3 -0
  23. package/dist/chunk-2JSNWYYA.cjs.map +1 -0
  24. package/dist/{chunk-J7ABVGZH.cjs → chunk-2VE4OW4A.cjs} +2 -2
  25. package/dist/{chunk-J7ABVGZH.cjs.map → chunk-2VE4OW4A.cjs.map} +1 -1
  26. package/dist/{chunk-HMC7FJO7.js → chunk-3SHWTGTP.js} +2 -2
  27. package/dist/{chunk-HMC7FJO7.js.map → chunk-3SHWTGTP.js.map} +1 -1
  28. package/dist/{chunk-EN3CDYOS.cjs → chunk-5GL4SAVH.cjs} +2 -2
  29. package/dist/{chunk-EN3CDYOS.cjs.map → chunk-5GL4SAVH.cjs.map} +1 -1
  30. package/dist/{chunk-SGIDBZ4T.cjs → chunk-72N3ZRVE.cjs} +2 -2
  31. package/dist/chunk-72N3ZRVE.cjs.map +1 -0
  32. package/dist/{chunk-CHSO3DCS.js → chunk-7EP36NXE.js} +3 -3
  33. package/dist/{chunk-CHSO3DCS.js.map → chunk-7EP36NXE.js.map} +1 -1
  34. package/dist/chunk-A4JV7HRB.js +3 -0
  35. package/dist/chunk-A4JV7HRB.js.map +1 -0
  36. package/dist/chunk-AUOE7MFI.cjs +2 -0
  37. package/dist/chunk-AUOE7MFI.cjs.map +1 -0
  38. package/dist/chunk-BDF4VCQX.js +3 -0
  39. package/dist/chunk-BDF4VCQX.js.map +1 -0
  40. package/dist/{chunk-3SSMOVWB.cjs → chunk-CDNJZBM3.cjs} +2 -2
  41. package/dist/{chunk-3SSMOVWB.cjs.map → chunk-CDNJZBM3.cjs.map} +1 -1
  42. package/dist/{chunk-YAONCEAW.cjs → chunk-CMZSK6FY.cjs} +3 -3
  43. package/dist/{chunk-YAONCEAW.cjs.map → chunk-CMZSK6FY.cjs.map} +1 -1
  44. package/dist/chunk-D2Q2VFJ6.js +2 -0
  45. package/dist/chunk-D2Q2VFJ6.js.map +1 -0
  46. package/dist/{chunk-75JLWK4L.cjs → chunk-DA7M6H63.cjs} +2 -2
  47. package/dist/{chunk-75JLWK4L.cjs.map → chunk-DA7M6H63.cjs.map} +1 -1
  48. package/dist/chunk-ELQQKZN3.cjs +3 -0
  49. package/dist/{chunk-PIZQIQVM.js.map → chunk-ELQQKZN3.cjs.map} +1 -1
  50. package/dist/chunk-F7QIBC4B.cjs +3 -0
  51. package/dist/chunk-F7QIBC4B.cjs.map +1 -0
  52. package/dist/chunk-GP4H5OIT.js +3 -0
  53. package/dist/chunk-GP4H5OIT.js.map +1 -0
  54. package/dist/chunk-HQ7BKXG7.js +2 -0
  55. package/dist/chunk-HQ7BKXG7.js.map +1 -0
  56. package/dist/{chunk-KRXO3MOW.cjs → chunk-IHAZL4EF.cjs} +2 -2
  57. package/dist/chunk-IHAZL4EF.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-3GR46UOX.js → chunk-IPSOWFCA.js} +2 -2
  61. package/dist/{chunk-3GR46UOX.js.map → chunk-IPSOWFCA.js.map} +1 -1
  62. package/dist/chunk-LM6ZQUEU.js +2 -0
  63. package/dist/chunk-LM6ZQUEU.js.map +1 -0
  64. package/dist/{chunk-N37PUZJR.cjs → chunk-LTJV3VJE.cjs} +2 -2
  65. package/dist/{chunk-N37PUZJR.cjs.map → chunk-LTJV3VJE.cjs.map} +1 -1
  66. package/dist/chunk-MWWAKZOD.cjs +3 -0
  67. package/dist/chunk-MWWAKZOD.cjs.map +1 -0
  68. package/dist/chunk-NUF66H57.js +5 -0
  69. package/dist/chunk-NUF66H57.js.map +1 -0
  70. package/dist/{chunk-MRRMIBHE.js → chunk-OMHRBKAT.js} +2 -2
  71. package/dist/{chunk-MRRMIBHE.js.map → chunk-OMHRBKAT.js.map} +1 -1
  72. package/dist/{chunk-4ETS224G.js → chunk-PTQCHMYA.js} +2 -2
  73. package/dist/{chunk-4ETS224G.js.map → chunk-PTQCHMYA.js.map} +1 -1
  74. package/dist/{chunk-4DJJIUQF.js → chunk-PWWQZFAE.js} +2 -2
  75. package/dist/chunk-PWWQZFAE.js.map +1 -0
  76. package/dist/chunk-RE6AIU6Y.js +3 -0
  77. package/dist/chunk-RE6AIU6Y.js.map +1 -0
  78. package/dist/chunk-RECRD45C.cjs +5 -0
  79. package/dist/chunk-RECRD45C.cjs.map +1 -0
  80. package/dist/{chunk-O3BDXOQJ.js → chunk-RENO2AWO.js} +2 -2
  81. package/dist/{chunk-O3BDXOQJ.js.map → chunk-RENO2AWO.js.map} +1 -1
  82. package/dist/chunk-RMCN5WFO.cjs +2 -0
  83. package/dist/chunk-RMCN5WFO.cjs.map +1 -0
  84. package/dist/{chunk-TL545PXW.cjs → chunk-RN3ISTM3.cjs} +2 -2
  85. package/dist/{chunk-TL545PXW.cjs.map → chunk-RN3ISTM3.cjs.map} +1 -1
  86. package/dist/{chunk-JYLNQOPU.cjs → chunk-S46R5QZP.cjs} +2 -2
  87. package/dist/{chunk-JYLNQOPU.cjs.map → chunk-S46R5QZP.cjs.map} +1 -1
  88. package/dist/{chunk-USQ4KL6S.cjs → chunk-UPH22K2T.cjs} +2 -2
  89. package/dist/chunk-UPH22K2T.cjs.map +1 -0
  90. package/dist/{chunk-KEA5HRL3.js → chunk-UXR7JIPX.js} +2 -2
  91. package/dist/{chunk-KEA5HRL3.js.map → chunk-UXR7JIPX.js.map} +1 -1
  92. package/dist/chunk-UY3ID6JF.cjs +2 -0
  93. package/dist/chunk-UY3ID6JF.cjs.map +1 -0
  94. package/dist/{chunk-JVMINMAB.js → chunk-WFOQRPA6.js} +2 -2
  95. package/dist/{chunk-JVMINMAB.js.map → chunk-WFOQRPA6.js.map} +1 -1
  96. package/dist/{chunk-AF7HQN6I.js → chunk-XFJABR3B.js} +2 -2
  97. package/dist/chunk-XFJABR3B.js.map +1 -0
  98. package/dist/{chunk-LNZNJSRW.cjs → chunk-ZVPZQ66D.cjs} +2 -2
  99. package/dist/chunk-ZVPZQ66D.cjs.map +1 -0
  100. package/dist/constants.cjs +1 -1
  101. package/dist/constants.d.cts +1 -1
  102. package/dist/constants.d.ts +1 -1
  103. package/dist/constants.js +1 -1
  104. package/dist/engine.cjs +1 -1
  105. package/dist/engine.d.cts +9 -9
  106. package/dist/engine.d.ts +9 -9
  107. package/dist/engine.js +1 -1
  108. package/dist/errors.cjs +1 -1
  109. package/dist/errors.d.cts +3 -3
  110. package/dist/errors.d.ts +3 -3
  111. package/dist/errors.js +1 -1
  112. package/dist/format.cjs +1 -1
  113. package/dist/format.d.cts +1 -1
  114. package/dist/format.d.ts +1 -1
  115. package/dist/format.js +1 -1
  116. package/dist/index.cjs +1 -1
  117. package/dist/index.cjs.map +1 -1
  118. package/dist/index.d.cts +11 -9
  119. package/dist/index.d.ts +11 -9
  120. package/dist/index.js +1 -1
  121. package/dist/index.js.map +1 -1
  122. package/dist/language.d.cts +8 -8
  123. package/dist/language.d.ts +8 -8
  124. package/dist/lexer.cjs +1 -1
  125. package/dist/lexer.js +1 -1
  126. package/dist/normalizer.cjs +1 -1
  127. package/dist/normalizer.d.cts +14 -8
  128. package/dist/normalizer.d.ts +14 -8
  129. package/dist/normalizer.js +1 -1
  130. package/dist/packages.cjs +1 -1
  131. package/dist/packages.d.cts +7 -7
  132. package/dist/packages.d.ts +7 -7
  133. package/dist/packages.js +1 -1
  134. package/dist/parser.cjs +1 -1
  135. package/dist/parser.d.cts +3 -3
  136. package/dist/parser.d.ts +3 -3
  137. package/dist/parser.js +1 -1
  138. package/dist/{pipeline-B4wf3M1h.d.cts → pipeline-69q2tsLE.d.cts} +1 -1
  139. package/dist/{pipeline-QIT4iD8f.d.ts → pipeline-DTqGLPsV.d.ts} +1 -1
  140. package/dist/resolvers.cjs +1 -1
  141. package/dist/resolvers.d.cts +18 -2
  142. package/dist/resolvers.d.ts +18 -2
  143. package/dist/resolvers.js +1 -1
  144. package/dist/testing.cjs +2 -2
  145. package/dist/testing.d.cts +8 -8
  146. package/dist/testing.d.ts +8 -8
  147. package/dist/testing.js +1 -1
  148. package/dist/uom.cjs +1 -1
  149. package/dist/uom.d.cts +1 -1
  150. package/dist/uom.d.ts +1 -1
  151. package/dist/uom.js +1 -1
  152. package/dist/vm.cjs +1 -1
  153. package/dist/vm.d.cts +7 -7
  154. package/dist/vm.d.ts +7 -7
  155. package/dist/vm.js +1 -1
  156. package/dist/worker.cjs +2 -2
  157. package/dist/worker.cjs.map +1 -1
  158. package/dist/worker.d.cts +31 -22
  159. package/dist/worker.d.ts +31 -22
  160. package/dist/worker.js +1 -1
  161. package/dist/worker.js.map +1 -1
  162. package/package.json +1 -1
  163. package/dist/chunk-42JXN3BH.js +0 -5
  164. package/dist/chunk-42JXN3BH.js.map +0 -1
  165. package/dist/chunk-4DJJIUQF.js.map +0 -1
  166. package/dist/chunk-6YM67DWH.cjs +0 -3
  167. package/dist/chunk-6YM67DWH.cjs.map +0 -1
  168. package/dist/chunk-7E7NXKQ5.js.map +0 -1
  169. package/dist/chunk-7SZBEE5R.cjs +0 -2
  170. package/dist/chunk-7SZBEE5R.cjs.map +0 -1
  171. package/dist/chunk-A5M4QZIK.js +0 -2
  172. package/dist/chunk-A5M4QZIK.js.map +0 -1
  173. package/dist/chunk-AF7HQN6I.js.map +0 -1
  174. package/dist/chunk-ALY4ZMYO.js +0 -2
  175. package/dist/chunk-ALY4ZMYO.js.map +0 -1
  176. package/dist/chunk-BRJ4RT5U.js +0 -2
  177. package/dist/chunk-BRJ4RT5U.js.map +0 -1
  178. package/dist/chunk-CO2BI6WL.cjs +0 -3
  179. package/dist/chunk-CO2BI6WL.cjs.map +0 -1
  180. package/dist/chunk-EQDT42JG.js +0 -3
  181. package/dist/chunk-EQDT42JG.js.map +0 -1
  182. package/dist/chunk-FMMT5C5H.cjs +0 -3
  183. package/dist/chunk-FMMT5C5H.cjs.map +0 -1
  184. package/dist/chunk-FP6Z6PYR.js +0 -2
  185. package/dist/chunk-FP6Z6PYR.js.map +0 -1
  186. package/dist/chunk-H3JXNH7X.cjs +0 -3
  187. package/dist/chunk-H3JXNH7X.cjs.map +0 -1
  188. package/dist/chunk-HZOLIL5T.js +0 -3
  189. package/dist/chunk-HZOLIL5T.js.map +0 -1
  190. package/dist/chunk-KRXO3MOW.cjs.map +0 -1
  191. package/dist/chunk-KVLJDOZ7.cjs +0 -5
  192. package/dist/chunk-KVLJDOZ7.cjs.map +0 -1
  193. package/dist/chunk-LNZNJSRW.cjs.map +0 -1
  194. package/dist/chunk-PIZQIQVM.js +0 -3
  195. package/dist/chunk-QTSDIDAS.js +0 -3
  196. package/dist/chunk-QTSDIDAS.js.map +0 -1
  197. package/dist/chunk-SGIDBZ4T.cjs.map +0 -1
  198. package/dist/chunk-UNKSCVIL.cjs +0 -2
  199. package/dist/chunk-UNKSCVIL.cjs.map +0 -1
  200. package/dist/chunk-USQ4KL6S.cjs.map +0 -1
  201. package/dist/chunk-XDXIPVZB.cjs +0 -2
  202. 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
 
@@ -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.
@@ -103,6 +103,8 @@ declare const CoreErrorCodes: {
103
103
  readonly INTERNAL_MISSING_FUNCTION_BODY: "INTERNAL_MISSING_FUNCTION_BODY";
104
104
  /** `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
105
  readonly INTERNAL_MISSING_ANONYMOUS_BODY: "INTERNAL_MISSING_ANONYMOUS_BODY";
106
+ /** `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. */
107
+ readonly UNKNOWN_BUILTIN_FUNCTION: "UNKNOWN_BUILTIN_FUNCTION";
106
108
  /** An operand byte read past the end of the stream: the program ends in the middle of an instruction. */
107
109
  readonly MALFORMED_BYTECODE_TRUNCATED: "MALFORMED_BYTECODE_TRUNCATED";
108
110
  /** A constant-pool operand indexing a `numbers`/`strings` entry that does not exist, or that is not of the pool's type. */
@@ -164,6 +166,8 @@ declare const CoreErrorCodes: {
164
166
  readonly UNKNOWN_SAVINGS_PERIOD: "UNKNOWN_SAVINGS_PERIOD";
165
167
  /** 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
168
  readonly INVALID_TIME_LITERAL: "INVALID_TIME_LITERAL";
169
+ /** 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. */
170
+ readonly NETWORK_DISABLED: "NETWORK_DISABLED";
167
171
  /** `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
172
  readonly SNAPSHOT_VERSION_MISMATCH: "SNAPSHOT_VERSION_MISMATCH";
169
173
  /** 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. */
@@ -103,6 +103,8 @@ declare const CoreErrorCodes: {
103
103
  readonly INTERNAL_MISSING_FUNCTION_BODY: "INTERNAL_MISSING_FUNCTION_BODY";
104
104
  /** `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
105
  readonly INTERNAL_MISSING_ANONYMOUS_BODY: "INTERNAL_MISSING_ANONYMOUS_BODY";
106
+ /** `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. */
107
+ readonly UNKNOWN_BUILTIN_FUNCTION: "UNKNOWN_BUILTIN_FUNCTION";
106
108
  /** An operand byte read past the end of the stream: the program ends in the middle of an instruction. */
107
109
  readonly MALFORMED_BYTECODE_TRUNCATED: "MALFORMED_BYTECODE_TRUNCATED";
108
110
  /** A constant-pool operand indexing a `numbers`/`strings` entry that does not exist, or that is not of the pool's type. */
@@ -164,6 +166,8 @@ declare const CoreErrorCodes: {
164
166
  readonly UNKNOWN_SAVINGS_PERIOD: "UNKNOWN_SAVINGS_PERIOD";
165
167
  /** 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
168
  readonly INVALID_TIME_LITERAL: "INVALID_TIME_LITERAL";
169
+ /** 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. */
170
+ readonly NETWORK_DISABLED: "NETWORK_DISABLED";
167
171
  /** `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
172
  readonly SNAPSHOT_VERSION_MISMATCH: "SNAPSHOT_VERSION_MISMATCH";
169
173
  /** 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,4 @@
1
- import { I as IEnginePackage } from './PackageRegistry-BRoVOYzg.js';
1
+ import { I as IEnginePackage } from './PackageRegistry-CDEgfhT_.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-rMGnTh7W.cjs';
1
+ import { I as IEnginePackage } from './PackageRegistry-B9-7fJGH.cjs';
2
2
 
3
3
  /**
4
4
  * Package load-time compatibility checking, the "detect overlapping
@@ -1,15 +1,15 @@
1
- import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from './Parselet-Bkbp9CKD.cjs';
2
- import { V as Value, b as ValueType } from './Value-DCTqTSeP.cjs';
1
+ import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from './Parselet-4oH5OuRt.cjs';
2
+ import { V as Value, a as ValueType } from './Value-Ds3Gy07C.cjs';
3
3
  import { M as MarkdownLineType, L as Lexer, e as TokenCategory, c as LexerVocabulary } from './Lexer-CCHFDcgP.cjs';
4
4
  import { IAsyncResolver } from './resolvers.cjs';
5
5
  import { T as TokenFusion, c as TokenNormalizer, a as NormalizerRule } from './TokenNormalizer-MXaKLJ_m.cjs';
6
- import { D as DependencyGraph, V as VM, a as DagSnapshot, b as EngineContext, S as ScopeManager, P as PluginFunctionHandler } from './ScopeManager-bCYewWVt.cjs';
6
+ import { D as DependencyGraph, V as VM, a as DagSnapshot, b as EngineContext, S as ScopeManager, P as PluginFunctionHandler } from './ScopeManager-DrXB3Nvm.cjs';
7
7
  import { a as BytecodeProgram } from './BytecodeBuilder-aqVa7Plx.cjs';
8
8
  import { QueryClient } from '@tanstack/query-core';
9
- import { E as EngineError } from './EngineError-DTk7I7hZ.cjs';
10
- import { a as DiagnosticReportJSON, D as DiagnosticPipeline } from './pipeline-B4wf3M1h.cjs';
9
+ import { E as EngineError } from './EngineError-D1kXsjIj.cjs';
10
+ import { a as DiagnosticReportJSON, D as DiagnosticPipeline } from './pipeline-69q2tsLE.cjs';
11
11
  import { T as Token } from './Token-CbP_OutD.cjs';
12
- import { E as EngineConfigOverride, a as EngineConfig } from './Configuration-BGQn-cJ8.cjs';
12
+ import { E as EngineConfigOverride, a as EngineConfig } from './Configuration-DnzYPmoK.cjs';
13
13
 
14
14
  /**
15
15
  * One line's compiled bytecode, plus the variables it reads and writes.
@@ -728,9 +728,10 @@ declare class AsyncResolutionBatcher {
728
728
  * Nullable rather than a constructor parameter because it is cleared by
729
729
  * `clearAll()` and re-wired on re-subscribe, so it cannot be readonly. That
730
730
  * makes it easy to miss, which is why {@link warnIfUnwired} exists: leaving
731
- * it unset means async values resolve into the cache and are never shown,
732
- * with nothing to indicate why. A host that genuinely does not want async
733
- * results should not register async resolvers at all.
731
+ * it unset, with nobody reading {@link getEventStream} either, means async
732
+ * values resolve into the cache and are never shown, with nothing to
733
+ * indicate why. A host that genuinely does not want async results should
734
+ * not register async resolvers at all.
734
735
  */
735
736
  onLineResult: ((lineNumber: number, value: Value) => void) | null;
736
737
  /**
@@ -742,11 +743,15 @@ declare class AsyncResolutionBatcher {
742
743
  */
743
744
  private warnedAboutMissingHook;
744
745
  /**
745
- * Warn once if an async result resolved with no {@link onLineResult} wired.
746
+ * Warn once if an async result resolved with nobody to receive it: no
747
+ * {@link onLineResult} wired and no reader on {@link getEventStream}.
746
748
  *
747
749
  * The failure this catches is silent by nature: the value arrives, the cache
748
750
  * updates, and the line keeps showing pending forever. Without this a host
749
- * author has no thread to pull on.
751
+ * author has no thread to pull on. A host reading the event stream is not
752
+ * in that position: the stream is the documented way to learn which lines
753
+ * to re-evaluate, and this used to warn at it anyway, telling it to set a
754
+ * hook it did not need.
750
755
  */
751
756
  private warnIfUnwired;
752
757
  /** High-water mark used when (re)creating the event stream. */
@@ -1,15 +1,15 @@
1
- import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from './Parselet-B-WyUtX4.js';
2
- import { V as Value, b as ValueType } from './Value-DCTqTSeP.js';
1
+ import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from './Parselet-BwDqKFfK.js';
2
+ import { V as Value, a as ValueType } from './Value-Ds3Gy07C.js';
3
3
  import { M as MarkdownLineType, L as Lexer, e as TokenCategory, c as LexerVocabulary } from './Lexer-CqagTewQ.js';
4
4
  import { IAsyncResolver } from './resolvers.js';
5
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-gB9UengK.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
7
  import { a as BytecodeProgram } from './BytecodeBuilder-aqVa7Plx.js';
8
8
  import { QueryClient } from '@tanstack/query-core';
9
- import { E as EngineError } from './EngineError-DTk7I7hZ.js';
10
- import { a as DiagnosticReportJSON, D as DiagnosticPipeline } from './pipeline-QIT4iD8f.js';
9
+ import { E as EngineError } from './EngineError-D1kXsjIj.js';
10
+ import { a as DiagnosticReportJSON, D as DiagnosticPipeline } from './pipeline-DTqGLPsV.js';
11
11
  import { T as Token } from './Token-CbP_OutD.js';
12
- import { E as EngineConfigOverride, a as EngineConfig } from './Configuration-BGQn-cJ8.js';
12
+ import { E as EngineConfigOverride, a as EngineConfig } from './Configuration-DnzYPmoK.js';
13
13
 
14
14
  /**
15
15
  * One line's compiled bytecode, plus the variables it reads and writes.
@@ -728,9 +728,10 @@ declare class AsyncResolutionBatcher {
728
728
  * Nullable rather than a constructor parameter because it is cleared by
729
729
  * `clearAll()` and re-wired on re-subscribe, so it cannot be readonly. That
730
730
  * makes it easy to miss, which is why {@link warnIfUnwired} exists: leaving
731
- * it unset means async values resolve into the cache and are never shown,
732
- * with nothing to indicate why. A host that genuinely does not want async
733
- * results should not register async resolvers at all.
731
+ * it unset, with nobody reading {@link getEventStream} either, means async
732
+ * values resolve into the cache and are never shown, with nothing to
733
+ * indicate why. A host that genuinely does not want async results should
734
+ * not register async resolvers at all.
734
735
  */
735
736
  onLineResult: ((lineNumber: number, value: Value) => void) | null;
736
737
  /**
@@ -742,11 +743,15 @@ declare class AsyncResolutionBatcher {
742
743
  */
743
744
  private warnedAboutMissingHook;
744
745
  /**
745
- * Warn once if an async result resolved with no {@link onLineResult} wired.
746
+ * Warn once if an async result resolved with nobody to receive it: no
747
+ * {@link onLineResult} wired and no reader on {@link getEventStream}.
746
748
  *
747
749
  * The failure this catches is silent by nature: the value arrives, the cache
748
750
  * updates, and the line keeps showing pending forever. Without this a host
749
- * author has no thread to pull on.
751
+ * author has no thread to pull on. A host reading the event stream is not
752
+ * in that position: the stream is the documented way to learn which lines
753
+ * to re-evaluate, and this used to warn at it anyway, telling it to set a
754
+ * hook it did not need.
750
755
  */
751
756
  private warnIfUnwired;
752
757
  /** High-water mark used when (re)creating the event stream. */
@@ -1,6 +1,6 @@
1
1
  import { B as BytecodeBuilder } from './BytecodeBuilder-aqVa7Plx.cjs';
2
2
  import { T as Token } from './Token-CbP_OutD.cjs';
3
- import { D as DiagnosticPipeline } from './pipeline-B4wf3M1h.cjs';
3
+ import { D as DiagnosticPipeline } from './pipeline-69q2tsLE.cjs';
4
4
 
5
5
  /**
6
6
  * Dual-keyed ParseletRegistry, accepts both string token types and
@@ -1,6 +1,6 @@
1
1
  import { B as BytecodeBuilder } from './BytecodeBuilder-aqVa7Plx.js';
2
2
  import { T as Token } from './Token-CbP_OutD.js';
3
- import { D as DiagnosticPipeline } from './pipeline-QIT4iD8f.js';
3
+ import { D as DiagnosticPipeline } from './pipeline-DTqGLPsV.js';
4
4
 
5
5
  /**
6
6
  * Dual-keyed ParseletRegistry, accepts both string token types and
@@ -1,7 +1,7 @@
1
- import { V as Value } from './Value-DCTqTSeP.js';
1
+ import { V as Value } from './Value-Ds3Gy07C.js';
2
2
  import { U as UserFunctionDef, A as AnonymousBodyDef, a as BytecodeProgram } from './BytecodeBuilder-aqVa7Plx.js';
3
- import { E as EngineError } from './EngineError-DTk7I7hZ.js';
4
- import { D as DiagnosticPipeline } from './pipeline-QIT4iD8f.js';
3
+ import { E as EngineError } from './EngineError-D1kXsjIj.js';
4
+ import { D as DiagnosticPipeline } from './pipeline-DTqGLPsV.js';
5
5
 
6
6
  /**
7
7
  * Create a new VM instance with the given opcode registry and configurable limits.
@@ -76,6 +76,13 @@ interface Bytecode {
76
76
  interface LineExecutionContext {
77
77
  /** 1-based current line number, or -1 when there is no real document (see class doc above). */
78
78
  lineIndex: number;
79
+ /**
80
+ * Whether this engine may fetch live data (`network.enabled`). A plugin
81
+ * function that reads a resolver's cache uses it to say "live data is
82
+ * switched off" when the cache is empty, rather than the "not preflighted"
83
+ * message that describes a different fault. Absent means enabled.
84
+ */
85
+ networkEnabled?: boolean;
79
86
  /** Look up another line's cached result by 1-based line number. `undefined` = not evaluated yet (or out of range), distinct from a line that evaluated to an actual `undefined`-like Value, which can't happen (every Value type has a concrete representation). */
80
87
  getLineResult?: (lineNumber: number) => Value | undefined;
81
88
  /** Whether line `lineNumber` is a blank line or a `#` heading, the stopping condition for "total above"/"sum above"/"average above" aggregation. */
@@ -250,6 +257,17 @@ interface EngineContext {
250
257
  * nowhere.
251
258
  */
252
259
  readonly pluginFunctionOwners: Record<number, string>;
260
+ /**
261
+ * Whether this engine may fetch live data, from `network.enabled` in the
262
+ * engine's configuration.
263
+ *
264
+ * Lives on the context rather than only on the engine because the VM is
265
+ * where a plugin function's promise is first seen and where a currency
266
+ * conversion discovers it has no rate. Both need to answer "live data is
267
+ * switched off" rather than "no rate available" or "result discarded", and
268
+ * the context is the one object every VM already holds.
269
+ */
270
+ readonly networkEnabled: boolean;
253
271
  }
254
272
 
255
273
  /**
@@ -356,15 +374,28 @@ declare class OpRegistry {
356
374
  * and abort signal management for async cancellation.
357
375
  */
358
376
  interface VM {
377
+ /**
378
+ * Push a value.
379
+ *
380
+ * @throws `STACK_LIMIT_EXCEEDED` (EXECUTION) when the stack is already at
381
+ * `maxStackDepth`. It used to drop the value silently, so a handler's
382
+ * result vanished and the next opcode read whatever lay beneath.
383
+ */
359
384
  push(value: Value): void;
385
+ /**
386
+ * Pop the top of the stack.
387
+ *
388
+ * @throws `STACK_UNDERFLOW` (INTERNAL) if the stack is empty. It used to
389
+ * answer with a plain `0`, which is indistinguishable from a real zero
390
+ * and turned a mismatched push/pop count into a wrong number.
391
+ */
360
392
  pop(): Value;
361
393
  /**
362
394
  * Pop the top of the stack as a number.
363
395
  *
364
- * @throws `STACK_UNDERFLOW` (INTERNAL) if the stack is empty. Unlike
365
- * `pop()`, which answers an empty stack with `0`, this reports it: a
366
- * handler asking for a number has no way to tell that `0` from a real
367
- * one. Every Value converts, so there is no operand-type throw here.
396
+ * @throws `STACK_UNDERFLOW` (INTERNAL) if the stack is empty, exactly as
397
+ * `pop()` does. Every Value converts, so there is no operand-type throw
398
+ * here.
368
399
  */
369
400
  popNumber(): number;
370
401
  /**
@@ -1,7 +1,7 @@
1
- import { V as Value } from './Value-DCTqTSeP.cjs';
1
+ import { V as Value } from './Value-Ds3Gy07C.cjs';
2
2
  import { U as UserFunctionDef, A as AnonymousBodyDef, a as BytecodeProgram } from './BytecodeBuilder-aqVa7Plx.cjs';
3
- import { E as EngineError } from './EngineError-DTk7I7hZ.cjs';
4
- import { D as DiagnosticPipeline } from './pipeline-B4wf3M1h.cjs';
3
+ import { E as EngineError } from './EngineError-D1kXsjIj.cjs';
4
+ import { D as DiagnosticPipeline } from './pipeline-69q2tsLE.cjs';
5
5
 
6
6
  /**
7
7
  * Create a new VM instance with the given opcode registry and configurable limits.
@@ -76,6 +76,13 @@ interface Bytecode {
76
76
  interface LineExecutionContext {
77
77
  /** 1-based current line number, or -1 when there is no real document (see class doc above). */
78
78
  lineIndex: number;
79
+ /**
80
+ * Whether this engine may fetch live data (`network.enabled`). A plugin
81
+ * function that reads a resolver's cache uses it to say "live data is
82
+ * switched off" when the cache is empty, rather than the "not preflighted"
83
+ * message that describes a different fault. Absent means enabled.
84
+ */
85
+ networkEnabled?: boolean;
79
86
  /** Look up another line's cached result by 1-based line number. `undefined` = not evaluated yet (or out of range), distinct from a line that evaluated to an actual `undefined`-like Value, which can't happen (every Value type has a concrete representation). */
80
87
  getLineResult?: (lineNumber: number) => Value | undefined;
81
88
  /** Whether line `lineNumber` is a blank line or a `#` heading, the stopping condition for "total above"/"sum above"/"average above" aggregation. */
@@ -250,6 +257,17 @@ interface EngineContext {
250
257
  * nowhere.
251
258
  */
252
259
  readonly pluginFunctionOwners: Record<number, string>;
260
+ /**
261
+ * Whether this engine may fetch live data, from `network.enabled` in the
262
+ * engine's configuration.
263
+ *
264
+ * Lives on the context rather than only on the engine because the VM is
265
+ * where a plugin function's promise is first seen and where a currency
266
+ * conversion discovers it has no rate. Both need to answer "live data is
267
+ * switched off" rather than "no rate available" or "result discarded", and
268
+ * the context is the one object every VM already holds.
269
+ */
270
+ readonly networkEnabled: boolean;
253
271
  }
254
272
 
255
273
  /**
@@ -356,15 +374,28 @@ declare class OpRegistry {
356
374
  * and abort signal management for async cancellation.
357
375
  */
358
376
  interface VM {
377
+ /**
378
+ * Push a value.
379
+ *
380
+ * @throws `STACK_LIMIT_EXCEEDED` (EXECUTION) when the stack is already at
381
+ * `maxStackDepth`. It used to drop the value silently, so a handler's
382
+ * result vanished and the next opcode read whatever lay beneath.
383
+ */
359
384
  push(value: Value): void;
385
+ /**
386
+ * Pop the top of the stack.
387
+ *
388
+ * @throws `STACK_UNDERFLOW` (INTERNAL) if the stack is empty. It used to
389
+ * answer with a plain `0`, which is indistinguishable from a real zero
390
+ * and turned a mismatched push/pop count into a wrong number.
391
+ */
360
392
  pop(): Value;
361
393
  /**
362
394
  * Pop the top of the stack as a number.
363
395
  *
364
- * @throws `STACK_UNDERFLOW` (INTERNAL) if the stack is empty. Unlike
365
- * `pop()`, which answers an empty stack with `0`, this reports it: a
366
- * handler asking for a number has no way to tell that `0` from a real
367
- * one. Every Value converts, so there is no operand-type throw here.
396
+ * @throws `STACK_UNDERFLOW` (INTERNAL) if the stack is empty, exactly as
397
+ * `pop()` does. Every Value converts, so there is no operand-type throw
398
+ * here.
368
399
  */
369
400
  popNumber(): number;
370
401
  /**
@@ -1,5 +1,5 @@
1
- import { V as Value } from './Value-DCTqTSeP.cjs';
2
- import { V as VM } from './ScopeManager-bCYewWVt.cjs';
1
+ import { V as Value } from './Value-Ds3Gy07C.cjs';
2
+ import { V as VM } from './ScopeManager-DrXB3Nvm.cjs';
3
3
  import { U as UserFunctionDef } from './BytecodeBuilder-aqVa7Plx.cjs';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { V as Value } from './Value-DCTqTSeP.js';
2
- import { V as VM } from './ScopeManager-gB9UengK.js';
1
+ import { V as Value } from './Value-Ds3Gy07C.js';
2
+ import { V as VM } from './ScopeManager-BQBhlDAu.js';
3
3
  import { U as UserFunctionDef } from './BytecodeBuilder-aqVa7Plx.js';
4
4
 
5
5
  /**
@@ -533,4 +533,4 @@ declare function colVectorValue(data: readonly number[]): Value;
533
533
  /** Create a Range value, a first-class integer range `min:max`, both bounds inclusive. */
534
534
  declare function rangeValue(min: number, max: number): Value;
535
535
 
536
- export { type ColourData as C, type IpCidrData as I, type MatrixData as M, type RangeData as R, type SplitData as S, Value as V, type MatrixEntry as a, ValueType as b, bigIntValue as c, colVectorValue as d, rowVectorValue as e, type ChartData as f, type SymbolicNode as g, hexValue as h, type ColourFormat as i, matrixValue as m, numberValue as n, rangeValue as r, stringValue as s, uomValue as u };
536
+ export { type ColourData as C, type IpCidrData as I, type MatrixData as M, type RangeData as R, type SplitData as S, Value as V, ValueType as a, type MatrixEntry as b, bigIntValue as c, colVectorValue as d, rowVectorValue as e, type ChartData as f, type SymbolicNode as g, hexValue as h, type ColourFormat as i, matrixValue as m, numberValue as n, rangeValue as r, stringValue as s, uomValue as u };
@@ -533,4 +533,4 @@ declare function colVectorValue(data: readonly number[]): Value;
533
533
  /** Create a Range value, a first-class integer range `min:max`, both bounds inclusive. */
534
534
  declare function rangeValue(min: number, max: number): Value;
535
535
 
536
- export { type ColourData as C, type IpCidrData as I, type MatrixData as M, type RangeData as R, type SplitData as S, Value as V, type MatrixEntry as a, ValueType as b, bigIntValue as c, colVectorValue as d, rowVectorValue as e, type ChartData as f, type SymbolicNode as g, hexValue as h, type ColourFormat as i, matrixValue as m, numberValue as n, rangeValue as r, stringValue as s, uomValue as u };
536
+ export { type ColourData as C, type IpCidrData as I, type MatrixData as M, type RangeData as R, type SplitData as S, Value as V, ValueType as a, type MatrixEntry as b, bigIntValue as c, colVectorValue as d, rowVectorValue as e, type ChartData as f, type SymbolicNode as g, hexValue as h, type ColourFormat as i, matrixValue as m, numberValue as n, rangeValue as r, stringValue as s, uomValue as u };