solve-engine 1.0.0-beta.0 → 1.0.0-beta.2

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 (253) hide show
  1. package/dist/BytecodeBuilder-3egMyuDi.d.cts +274 -0
  2. package/dist/BytecodeBuilder-3egMyuDi.d.ts +274 -0
  3. package/dist/Configuration-fVpd-HC-.d.cts +228 -0
  4. package/dist/Configuration-fVpd-HC-.d.ts +228 -0
  5. package/dist/EngineError-DvPSnhZi.d.cts +304 -0
  6. package/dist/EngineError-DvPSnhZi.d.ts +304 -0
  7. package/dist/Lexer-BJdhlnej.d.cts +531 -0
  8. package/dist/Lexer-D9l4Yrk2.d.ts +531 -0
  9. package/dist/NormalizerRule-BrVoVjmP.d.cts +163 -0
  10. package/dist/NormalizerRule-CEjf1FyD.d.ts +163 -0
  11. package/dist/PackageRegistry-D-Tv_7ca.d.cts +2572 -0
  12. package/dist/PackageRegistry-DaH4aIKP.d.ts +2572 -0
  13. package/dist/Parselet-CK8cQu2a.d.cts +299 -0
  14. package/dist/Parselet-ConOIdRO.d.ts +299 -0
  15. package/dist/ScopeManager-B5k0nk05.d.ts +559 -0
  16. package/dist/ScopeManager-CKfQtl7l.d.cts +559 -0
  17. package/dist/Token-0jpvUdPY.d.cts +245 -0
  18. package/dist/Token-0jpvUdPY.d.ts +245 -0
  19. package/dist/VMCheckpoints-BmDwIXgK.d.ts +171 -0
  20. package/dist/VMCheckpoints-CqFuLwrT.d.cts +171 -0
  21. package/dist/Value-KtAQNHur.d.cts +295 -0
  22. package/dist/Value-KtAQNHur.d.ts +295 -0
  23. package/dist/chunk-2RWAXT6O.js +3 -0
  24. package/dist/chunk-2RWAXT6O.js.map +1 -0
  25. package/dist/chunk-34RRD7PC.js +1492 -0
  26. package/dist/chunk-34RRD7PC.js.map +1 -0
  27. package/dist/chunk-3PPFLFH4.js +5097 -0
  28. package/dist/chunk-3PPFLFH4.js.map +1 -0
  29. package/dist/chunk-3VGDNEE3.js +84 -0
  30. package/dist/chunk-3VGDNEE3.js.map +1 -0
  31. package/dist/chunk-3YNVWKR2.cjs +370 -0
  32. package/dist/chunk-3YNVWKR2.cjs.map +1 -0
  33. package/dist/chunk-47LRVGOT.cjs +316 -0
  34. package/dist/chunk-47LRVGOT.cjs.map +1 -0
  35. package/dist/chunk-4MG4XKO2.js +1644 -0
  36. package/dist/chunk-4MG4XKO2.js.map +1 -0
  37. package/dist/chunk-4QADQTWS.js +2392 -0
  38. package/dist/chunk-4QADQTWS.js.map +1 -0
  39. package/dist/chunk-53B6KDDJ.cjs +1651 -0
  40. package/dist/chunk-53B6KDDJ.cjs.map +1 -0
  41. package/dist/chunk-5X2PTP6F.cjs +104 -0
  42. package/dist/chunk-5X2PTP6F.cjs.map +1 -0
  43. package/dist/chunk-5YEMOYSE.js +264 -0
  44. package/dist/chunk-5YEMOYSE.js.map +1 -0
  45. package/dist/chunk-64W6GLLZ.js +481 -0
  46. package/dist/chunk-64W6GLLZ.js.map +1 -0
  47. package/dist/chunk-6BKTCEUP.cjs +233 -0
  48. package/dist/chunk-6BKTCEUP.cjs.map +1 -0
  49. package/dist/chunk-6NTVRDQV.cjs +1642 -0
  50. package/dist/chunk-6NTVRDQV.cjs.map +1 -0
  51. package/dist/chunk-6RDSQURE.js +290 -0
  52. package/dist/chunk-6RDSQURE.js.map +1 -0
  53. package/dist/chunk-75JP4BZX.js +93 -0
  54. package/dist/chunk-75JP4BZX.js.map +1 -0
  55. package/dist/chunk-B7LXJ6LC.cjs +36 -0
  56. package/dist/chunk-B7LXJ6LC.cjs.map +1 -0
  57. package/dist/chunk-BVAR5J2F.js +257 -0
  58. package/dist/chunk-BVAR5J2F.js.map +1 -0
  59. package/dist/chunk-C4XZV6E7.cjs +269 -0
  60. package/dist/chunk-C4XZV6E7.cjs.map +1 -0
  61. package/dist/chunk-CCJPSHGI.cjs +40 -0
  62. package/dist/chunk-CCJPSHGI.cjs.map +1 -0
  63. package/dist/chunk-CLL7RUQV.cjs +316 -0
  64. package/dist/chunk-CLL7RUQV.cjs.map +1 -0
  65. package/dist/chunk-DM3LMRBC.js +329 -0
  66. package/dist/chunk-DM3LMRBC.js.map +1 -0
  67. package/dist/chunk-DUEZFTK2.js +38 -0
  68. package/dist/chunk-DUEZFTK2.js.map +1 -0
  69. package/dist/chunk-EBSPLUW4.cjs +489 -0
  70. package/dist/chunk-EBSPLUW4.cjs.map +1 -0
  71. package/dist/chunk-EHAHVROS.cjs +261 -0
  72. package/dist/chunk-EHAHVROS.cjs.map +1 -0
  73. package/dist/chunk-EIGTWK5N.js +167 -0
  74. package/dist/chunk-EIGTWK5N.js.map +1 -0
  75. package/dist/chunk-EPOXXJBK.js +96 -0
  76. package/dist/chunk-EPOXXJBK.js.map +1 -0
  77. package/dist/chunk-FEAIOR7N.cjs +91 -0
  78. package/dist/chunk-FEAIOR7N.cjs.map +1 -0
  79. package/dist/chunk-G535KJEG.js +312 -0
  80. package/dist/chunk-G535KJEG.js.map +1 -0
  81. package/dist/chunk-GCM5JHMX.js +88 -0
  82. package/dist/chunk-GCM5JHMX.js.map +1 -0
  83. package/dist/chunk-GOLDJNMZ.js +368 -0
  84. package/dist/chunk-GOLDJNMZ.js.map +1 -0
  85. package/dist/chunk-GW32KPCU.cjs +360 -0
  86. package/dist/chunk-GW32KPCU.cjs.map +1 -0
  87. package/dist/chunk-HTXVVJRA.cjs +1549 -0
  88. package/dist/chunk-HTXVVJRA.cjs.map +1 -0
  89. package/dist/chunk-HWSZJQCI.js +311 -0
  90. package/dist/chunk-HWSZJQCI.js.map +1 -0
  91. package/dist/chunk-IF532O7C.js +101 -0
  92. package/dist/chunk-IF532O7C.js.map +1 -0
  93. package/dist/chunk-J73SJHR3.cjs +2415 -0
  94. package/dist/chunk-J73SJHR3.cjs.map +1 -0
  95. package/dist/chunk-JBSYC7BB.cjs +7473 -0
  96. package/dist/chunk-JBSYC7BB.cjs.map +1 -0
  97. package/dist/chunk-KVILKGMS.js +228 -0
  98. package/dist/chunk-KVILKGMS.js.map +1 -0
  99. package/dist/chunk-LIPPNDBE.js +258 -0
  100. package/dist/chunk-LIPPNDBE.js.map +1 -0
  101. package/dist/chunk-LR7YASZF.cjs +172 -0
  102. package/dist/chunk-LR7YASZF.cjs.map +1 -0
  103. package/dist/chunk-M4N5GA5Z.cjs +309 -0
  104. package/dist/chunk-M4N5GA5Z.cjs.map +1 -0
  105. package/dist/chunk-M5LX5AOO.js +7459 -0
  106. package/dist/chunk-M5LX5AOO.js.map +1 -0
  107. package/dist/chunk-MZOVP3WI.cjs +96 -0
  108. package/dist/chunk-MZOVP3WI.cjs.map +1 -0
  109. package/dist/chunk-N5YFR4UP.cjs +10 -0
  110. package/dist/chunk-N5YFR4UP.cjs.map +1 -0
  111. package/dist/chunk-NGBBNL4K.cjs +4 -0
  112. package/dist/chunk-NGBBNL4K.cjs.map +1 -0
  113. package/dist/chunk-NH2O2AUR.js +146 -0
  114. package/dist/chunk-NH2O2AUR.js.map +1 -0
  115. package/dist/chunk-NLNDMWZC.js +31 -0
  116. package/dist/chunk-NLNDMWZC.js.map +1 -0
  117. package/dist/chunk-NMCRQP3Z.cjs +778 -0
  118. package/dist/chunk-NMCRQP3Z.cjs.map +1 -0
  119. package/dist/chunk-NMD5VRN4.cjs +1327 -0
  120. package/dist/chunk-NMD5VRN4.cjs.map +1 -0
  121. package/dist/chunk-OC6LTXYS.cjs +286 -0
  122. package/dist/chunk-OC6LTXYS.cjs.map +1 -0
  123. package/dist/chunk-OT6OJY7C.cjs +114 -0
  124. package/dist/chunk-OT6OJY7C.cjs.map +1 -0
  125. package/dist/chunk-PFSDGGYL.js +8 -0
  126. package/dist/chunk-PFSDGGYL.js.map +1 -0
  127. package/dist/chunk-QNJ4ACRT.cjs +155 -0
  128. package/dist/chunk-QNJ4ACRT.cjs.map +1 -0
  129. package/dist/chunk-QWY3VEZN.js +1637 -0
  130. package/dist/chunk-QWY3VEZN.js.map +1 -0
  131. package/dist/chunk-RFYD5TJE.js +111 -0
  132. package/dist/chunk-RFYD5TJE.js.map +1 -0
  133. package/dist/chunk-RIN643A3.js +354 -0
  134. package/dist/chunk-RIN643A3.js.map +1 -0
  135. package/dist/chunk-RZCWSXTA.cjs +5122 -0
  136. package/dist/chunk-RZCWSXTA.cjs.map +1 -0
  137. package/dist/chunk-SDGRK7EP.js +774 -0
  138. package/dist/chunk-SDGRK7EP.js.map +1 -0
  139. package/dist/chunk-VEKIWQ3F.cjs +39 -0
  140. package/dist/chunk-VEKIWQ3F.cjs.map +1 -0
  141. package/dist/chunk-WQTTOGXC.cjs +99 -0
  142. package/dist/chunk-WQTTOGXC.cjs.map +1 -0
  143. package/dist/chunk-X6K3HUOY.cjs +86 -0
  144. package/dist/chunk-X6K3HUOY.cjs.map +1 -0
  145. package/dist/chunk-XJCMXB2E.cjs +333 -0
  146. package/dist/chunk-XJCMXB2E.cjs.map +1 -0
  147. package/dist/chunk-XVWCOTR6.js +1324 -0
  148. package/dist/chunk-XVWCOTR6.js.map +1 -0
  149. package/dist/chunk-YLJ4XMA6.js +33 -0
  150. package/dist/chunk-YLJ4XMA6.js.map +1 -0
  151. package/dist/chunk-Z2XP767T.cjs +35 -0
  152. package/dist/chunk-Z2XP767T.cjs.map +1 -0
  153. package/dist/chunk-Z4X2RWOF.js +37 -0
  154. package/dist/chunk-Z4X2RWOF.js.map +1 -0
  155. package/dist/constants.cjs +22 -0
  156. package/dist/constants.cjs.map +1 -0
  157. package/dist/constants.d.cts +12 -0
  158. package/dist/constants.d.ts +12 -0
  159. package/dist/constants.js +5 -0
  160. package/dist/constants.js.map +1 -0
  161. package/dist/engine.cjs +79 -0
  162. package/dist/engine.cjs.map +1 -0
  163. package/dist/engine.d.cts +587 -0
  164. package/dist/engine.d.ts +587 -0
  165. package/dist/engine.js +34 -0
  166. package/dist/engine.js.map +1 -0
  167. package/dist/errors.cjs +81 -0
  168. package/dist/errors.cjs.map +1 -0
  169. package/dist/errors.d.cts +95 -0
  170. package/dist/errors.d.ts +95 -0
  171. package/dist/errors.js +4 -0
  172. package/dist/errors.js.map +1 -0
  173. package/dist/format.cjs +171 -0
  174. package/dist/format.cjs.map +1 -0
  175. package/dist/format.d.cts +51 -0
  176. package/dist/format.d.ts +51 -0
  177. package/dist/format.js +168 -0
  178. package/dist/format.js.map +1 -0
  179. package/dist/index.cjs +97 -0
  180. package/dist/index.cjs.map +1 -0
  181. package/dist/index.d.cts +152 -0
  182. package/dist/index.d.ts +152 -0
  183. package/dist/index.js +75 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/language.cjs +308 -0
  186. package/dist/language.cjs.map +1 -0
  187. package/dist/language.d.cts +110 -0
  188. package/dist/language.d.ts +110 -0
  189. package/dist/language.js +287 -0
  190. package/dist/language.js.map +1 -0
  191. package/dist/lexer.cjs +64 -0
  192. package/dist/lexer.cjs.map +1 -0
  193. package/dist/lexer.d.cts +56 -0
  194. package/dist/lexer.d.ts +56 -0
  195. package/dist/lexer.js +11 -0
  196. package/dist/lexer.js.map +1 -0
  197. package/dist/normalizer.cjs +40 -0
  198. package/dist/normalizer.cjs.map +1 -0
  199. package/dist/normalizer.d.cts +364 -0
  200. package/dist/normalizer.d.ts +364 -0
  201. package/dist/normalizer.js +11 -0
  202. package/dist/normalizer.js.map +1 -0
  203. package/dist/packages.cjs +121 -0
  204. package/dist/packages.cjs.map +1 -0
  205. package/dist/packages.d.cts +691 -0
  206. package/dist/packages.d.ts +691 -0
  207. package/dist/packages.js +24 -0
  208. package/dist/packages.js.map +1 -0
  209. package/dist/parser.cjs +55 -0
  210. package/dist/parser.cjs.map +1 -0
  211. package/dist/parser.d.cts +46 -0
  212. package/dist/parser.d.ts +46 -0
  213. package/dist/parser.js +10 -0
  214. package/dist/parser.js.map +1 -0
  215. package/dist/pipeline-CrsRPmsv.d.ts +285 -0
  216. package/dist/pipeline-DPUQt7yL.d.cts +285 -0
  217. package/dist/resolvers.cjs +13 -0
  218. package/dist/resolvers.cjs.map +1 -0
  219. package/dist/resolvers.d.cts +111 -0
  220. package/dist/resolvers.d.ts +111 -0
  221. package/dist/resolvers.js +4 -0
  222. package/dist/resolvers.js.map +1 -0
  223. package/dist/services.cjs +21 -0
  224. package/dist/services.cjs.map +1 -0
  225. package/dist/services.d.cts +30 -0
  226. package/dist/services.d.ts +30 -0
  227. package/dist/services.js +4 -0
  228. package/dist/services.js.map +1 -0
  229. package/dist/uom.cjs +56 -0
  230. package/dist/uom.cjs.map +1 -0
  231. package/dist/uom.d.cts +224 -0
  232. package/dist/uom.d.ts +224 -0
  233. package/dist/uom.js +11 -0
  234. package/dist/uom.js.map +1 -0
  235. package/dist/utilities.cjs +32 -0
  236. package/dist/utilities.cjs.map +1 -0
  237. package/dist/utilities.d.cts +113 -0
  238. package/dist/utilities.d.ts +113 -0
  239. package/dist/utilities.js +7 -0
  240. package/dist/utilities.js.map +1 -0
  241. package/dist/variables.cjs +17 -0
  242. package/dist/variables.cjs.map +1 -0
  243. package/dist/variables.d.cts +87 -0
  244. package/dist/variables.d.ts +87 -0
  245. package/dist/variables.js +4 -0
  246. package/dist/variables.js.map +1 -0
  247. package/dist/vm.cjs +284 -0
  248. package/dist/vm.cjs.map +1 -0
  249. package/dist/vm.d.cts +68 -0
  250. package/dist/vm.d.ts +68 -0
  251. package/dist/vm.js +194 -0
  252. package/dist/vm.js.map +1 -0
  253. package/package.json +4 -4
@@ -0,0 +1,2572 @@
1
+ import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from './Parselet-CK8cQu2a.cjs';
2
+ import { V as Value } from './Value-KtAQNHur.cjs';
3
+ import { IVariableSource } from './variables.cjs';
4
+ import { M as MarkdownLineType, L as Lexer, e as TokenCategory, c as LexerVocabulary } from './Lexer-BJdhlnej.cjs';
5
+ import { IAsyncResolver } from './resolvers.cjs';
6
+ import { T as TokenFusion, N as NormalizerRule } from './NormalizerRule-BrVoVjmP.cjs';
7
+ import { D as DependencyGraph, V as VM, a as DagSnapshot, b as EngineContext, S as ScopeManager, L as LineExecutionContext } from './ScopeManager-CKfQtl7l.cjs';
8
+ import { a as BytecodeProgram } from './BytecodeBuilder-3egMyuDi.cjs';
9
+ import { QueryClient } from '@tanstack/query-core';
10
+ import { E as EngineError } from './EngineError-DvPSnhZi.cjs';
11
+ import { a as DiagnosticReportJSON, D as DiagnosticPipeline } from './pipeline-DPUQt7yL.cjs';
12
+ import { T as Token } from './Token-0jpvUdPY.cjs';
13
+ import { D as DEFAULT_CONFIG, E as EngineConfig } from './Configuration-fVpd-HC-.cjs';
14
+
15
+ /**
16
+ * One line's compiled bytecode, plus the variables it reads and writes.
17
+ *
18
+ * The reads and writes are what let the dependency graph re-evaluate only the
19
+ * lines an edit actually affected.
20
+ */
21
+ declare class LineCacheEntry {
22
+ result: Value;
23
+ bytecode: BytecodeProgram;
24
+ readVariables: string[];
25
+ writeVariable: string | null;
26
+ constructor(result: Value, bytecode: BytecodeProgram, readVariables: string[], writeVariable: string | null);
27
+ }
28
+ /**
29
+ * Per-line result + bytecode cache.
30
+ *
31
+ * Entries are stored in a two-level map (line number → expression → entry)
32
+ * so per-line operations, getEntryForLine, removeAllForLine, are O(1)
33
+ * lookups instead of scans over every cached key. Entries with no
34
+ * expression are stored under the empty-string key.
35
+ *
36
+ * The string keys exposed by keys()/forEach() keep the historical
37
+ * "line" / "line:expression" format for diagnostics consumers.
38
+ *
39
+ * INVARIANT: at most one entry per line number at a time. `set()` stores
40
+ * the expression alongside the entry ONLY as a same-key staleness check for
41
+ * `get(line, expression)`, a line's previous text is never meaningfully
42
+ * cacheable once the line has moved on, since `get()`/`getEntryForLine()`
43
+ * are only ever called with the line's CURRENT text (see
44
+ * `ExpressionEngine.reEvaluateLine()`). Before this invariant was enforced,
45
+ * every distinct keystroke state of a line accumulated its own entry here
46
+ * forever (nothing ever called `remove()`/`removeAllForLine()` per-edit,
47
+ * only a full `clear()` on document switch), an unbounded, session-long
48
+ * memory leak reachable via ordinary typing, and a latent correctness bug
49
+ * in `getEntryForLine()`, which picks "first in insertion order" and could
50
+ * silently return a STALE entry from an old edit of the line instead of
51
+ * its current one once more than one entry had piled up.
52
+ */
53
+ declare class LineCache {
54
+ /** line number → (expression, or "" for expressionless entries) → entry */
55
+ private byLine;
56
+ private count;
57
+ private static exprKey;
58
+ private static displayKey;
59
+ get(line: number, expression?: string): LineCacheEntry | undefined;
60
+ /** Find any cache entry for the given line number, regardless of expression suffix */
61
+ getEntryForLine(line: number): LineCacheEntry | undefined;
62
+ set(line: number, entry: LineCacheEntry, expression?: string): void;
63
+ has(line: number, expression?: string): boolean;
64
+ remove(line: number, expression?: string): void;
65
+ removeAllForLine(line: number): void;
66
+ clearLine(line: number): void;
67
+ /**
68
+ * Number of entries in the cache. Useful for diagnostics.
69
+ */
70
+ get size(): number;
71
+ clear(): void;
72
+ /**
73
+ * Iterate all cache entries for diagnostics/debugging.
74
+ */
75
+ forEach(callback: (key: string, entry: LineCacheEntry) => void): void;
76
+ /**
77
+ * Get all entry keys.
78
+ */
79
+ keys(): string[];
80
+ }
81
+
82
+ /**
83
+ * Persistent per-line state tracked by the DocumentModel.
84
+ *
85
+ * Each line receives an immutable `lineId` that survives structural edits
86
+ * (insertions, deletions, line shifts). This allows caches, the dependency
87
+ * graph, and VM checkpoints to reference lines by ID instead of by volatile
88
+ * line numbers.
89
+ *
90
+ * ── Multi-expression support ─────────────────────────────────────────
91
+ * A single document line may contain multiple inline solves (`s\`...\``).
92
+ * To support this without breaking the 1:1 line-to-DAG-node contract:
93
+ * - `expressions[]` holds all extracted expression strings (1 entry for
94
+ * full-line expressions, N entries for N inline solves).
95
+ * - `bytecodes[]` holds compiled bytecode in corresponding order.
96
+ * - `results[]` holds evaluation **result groups** in corresponding order.
97
+ * Each element is a `Value[]`, a group of Values produced by that expression.
98
+ * For variable definitions, the group always has exactly 1 element.
99
+ * For multi-output expressions (e.g., currency conversion), the group
100
+ * may have multiple Values (one per target unit).
101
+ * - `reads[]` and `writes[]` are aggregated across ALL expressions on
102
+ * the line, the DAG treats the line as a single dependency node.
103
+ * - `inlineSolveCount` is 0 for full-line expressions, >0 for inline solves.
104
+ */
105
+ interface LineState {
106
+ /** Immutable unique identifier, survives all structural edits. */
107
+ readonly lineId: number;
108
+ /** djb2 hash of the line text, used for O(1) change detection. */
109
+ textHash: number;
110
+ /** The full line text (may include markdown). */
111
+ text: string;
112
+ /**
113
+ * Extracted expression texts.
114
+ * - Full-line expressions: one entry (the trimmed text).
115
+ * - Inline solve lines: one entry per `s\`...\`` span, in left-to-right order.
116
+ * - Markdown-only lines: empty array.
117
+ */
118
+ expressions: string[];
119
+ /**
120
+ * Compiled bytecode for each expression, in corresponding order.
121
+ * Parallel to {@link expressions}.
122
+ */
123
+ bytecodes: BytecodeProgram[];
124
+ /** Variables this line reads (for dependency tracking). Aggregated across all expressions. */
125
+ reads: string[];
126
+ /** Variables this line writes (empty if not a variable definition). Aggregated across all expressions. */
127
+ writes: string[];
128
+ /**
129
+ * Evaluation result groups for each expression, in corresponding order.
130
+ * Each `Value[]` is a group of Values produced by one expression.
131
+ * Variable definitions always produce exactly 1 Value per group.
132
+ * Multi-output expressions (e.g. currency conversion) may produce N Values.
133
+ */
134
+ results: Value[][];
135
+ /**
136
+ * Convenience accessor for the first expression's first result.
137
+ * Equals `results[0]?.[0] ?? null`. For single-expression lines,
138
+ * this is the primary result. For multi-expression (inline solve) lines,
139
+ * prefer accessing `results[i][j]` directly.
140
+ */
141
+ result: Value | null;
142
+ /** True if this line needs re-evaluation. */
143
+ dirty: boolean;
144
+ /** True if any expression on this line defines a variable (never evict bytecode). */
145
+ isVariableDef: boolean;
146
+ /** True if this line contains only markdown (no evaluable expression). */
147
+ isEmpty: boolean;
148
+ /**
149
+ * Number of inline solve expressions on this line.
150
+ * 0 = full-line expression (or markdown-only).
151
+ * >0 = N inline solves embedded in markdown text.
152
+ */
153
+ inlineSolveCount: number;
154
+ }
155
+ /** The visible line range, used to decide which lines are evaluated eagerly. */
156
+ interface ViewportRange {
157
+ startLine: number;
158
+ endLine: number;
159
+ }
160
+ /** Describes a structural change to the document's line list. */
161
+ interface LineChange {
162
+ /** 1-based line number where the change starts. */
163
+ startLine: number;
164
+ /** Number of lines deleted (0 for pure insertion). */
165
+ deleteCount: number;
166
+ /** New line texts inserted in place of deleted lines. */
167
+ insertLines: string[];
168
+ }
169
+ /** Result of applying changes to the document model. */
170
+ interface ApplyChangesResult {
171
+ /** Line IDs of newly inserted lines. */
172
+ inserted: number[];
173
+ /** Line IDs that were removed. */
174
+ removed: number[];
175
+ }
176
+ /**
177
+ * Persistent document model with O(log N) line lookups and structural edits.
178
+ *
179
+ * Design:
180
+ * - Each line has an immutable `lineId` (monotonically increasing counter).
181
+ * - `LineState` objects are stored in a `Map<lineId, LineState>` for O(1) access.
182
+ * - Line ordering is maintained in a `SegmentTree` (order-statistic Treap) that
183
+ * supports O(log N) insert, delete, and get-at-index operations.
184
+ * - A lazy position cache (`Map<lineId, number>`) provides O(1) position lookups
185
+ * after the first `getLinePosition()` call and is invalidated on structural edits.
186
+ *
187
+ * Key invariant: line IDs never change, only their positions in the order tree.
188
+ * This means cached bytecode, dependency graph entries, and VM checkpoints
189
+ * keyed by lineId remain valid across all structural edits.
190
+ */
191
+ declare class DocumentModel {
192
+ /** Persistent line ID → LineState. */
193
+ private lines;
194
+ /** Order-statistic treap representing the current document line order. */
195
+ private orderTree;
196
+ /** Monotonically increasing counter for new line IDs. */
197
+ private nextLineId;
198
+ /**
199
+ * Lazy position cache: lineId → 1-based position.
200
+ * Built on first `getLinePosition()` call, invalidated on structural edits.
201
+ */
202
+ private _positionCache;
203
+ /**
204
+ * Line IDs currently marked dirty, maintained alongside every
205
+ * `state.dirty` mutation (in this class and in every other module that
206
+ * holds a direct `LineState` reference: ThreeTierEvaluator, PageManager).
207
+ * Lets {@link hasAnyDirtyLineBefore} answer "is anything before position
208
+ * X dirty" in O(d log N), d = current dirty count, typically tiny once
209
+ * a document has settled after its initial evaluation, instead of
210
+ * O(N log N), which used to mean every scroll event re-walked the WHOLE
211
+ * document via `getLineAt()` regardless of how little of it was actually
212
+ * dirty. Benchmarked: ~10.6ms per setViewport() call scrolled near the
213
+ * bottom of a 20k-line document before this fix.
214
+ */
215
+ private dirtyLineIds;
216
+ /**
217
+ * Initialize or replace the entire document from a text blob.
218
+ * Clears all existing state and assigns new persistent line IDs.
219
+ */
220
+ setDocument(text: string): void;
221
+ /**
222
+ * Apply one or more line-level changes to the document.
223
+ *
224
+ * **Precondition:** Changes must be **non-overlapping** in their line ranges.
225
+ * If two changes target the same or adjacent lines, the reverse-order
226
+ * processing may produce incorrect results because the first-applied
227
+ * change shifts the line numbers that the second change references.
228
+ *
229
+ * Changes are applied in **reverse order** (highest startLine first) so
230
+ * that earlier changes in the document don't shift the indices of later
231
+ * changes during processing.
232
+ *
233
+ * Returns both the newly inserted line IDs and the removed line IDs.
234
+ * Callers should use `removed` to clean up the dependency graph and
235
+ * other data structures keyed by lineId.
236
+ */
237
+ applyChanges(changes: LineChange[]): ApplyChangesResult;
238
+ /**
239
+ * Insert new lines at the given 1-based position.
240
+ * Convenience wrapper around applyChanges.
241
+ */
242
+ insertLines(atLine: number, texts: string[]): number[];
243
+ /**
244
+ * Delete lines in the given 1-based range [startLine, endLine] inclusive.
245
+ * Convenience wrapper around applyChanges.
246
+ */
247
+ deleteLines(startLine: number, endLine: number): number[];
248
+ /**
249
+ * Update the text of a single line in place.
250
+ * If the text hash differs, marks the line dirty and clears its
251
+ * bytecode/result so it gets re-evaluated.
252
+ *
253
+ * Returns true if the text actually changed (hash mismatch).
254
+ */
255
+ editLine(lineNumber: number, newText: string): boolean;
256
+ /**
257
+ * Get the LineState at the given 1-based line position. O(1).
258
+ */
259
+ getLineAt(position: number): LineState | undefined;
260
+ /**
261
+ * Get the 1-based position of a line by its persistent ID.
262
+ * Returns -1 if the line ID is not in the document.
263
+ *
264
+ * Uses a lazy position cache: O(N) on first call after structural edit,
265
+ * O(1) on subsequent calls. The cache is invalidated by any structural edit.
266
+ */
267
+ getLinePosition(lineId: number): number;
268
+ /**
269
+ * Get all LineState entries within the given viewport range (1-based, inclusive).
270
+ * Uses SegmentTree.getRange() for O(viewport + log N) collection instead of
271
+ * O(viewport × log N) per-line lookups.
272
+ */
273
+ getVisibleLines(startLine: number, endLine: number): LineState[];
274
+ /**
275
+ * Get all LineState entries in order. Useful for batch processing.
276
+ */
277
+ getAllLines(): LineState[];
278
+ /**
279
+ * Get a LineState by its persistent line ID. O(1).
280
+ */
281
+ getLineById(lineId: number): LineState | undefined;
282
+ /**
283
+ * Get all lines that are marked dirty.
284
+ */
285
+ getDirtyLines(): LineState[];
286
+ /**
287
+ * Whether any line before `position` (1-based, exclusive) is dirty.
288
+ *
289
+ * Used by ThreeTierEvaluator.setViewport() to decide whether cached
290
+ * checkpoint state might be stale and a full evaluate() (from line 1) is
291
+ * needed instead of the cheap viewport-only path.
292
+ *
293
+ * O(d log N) where d = current dirty line count via {@link dirtyLineIds},
294
+ * not O(N log N), a document that's mostly clean (the steady state after
295
+ * initial load) answers this in the cost of resolving a handful of
296
+ * lineIds to positions, not walking every line up to `position`.
297
+ */
298
+ hasAnyDirtyLineBefore(position: number): boolean;
299
+ /**
300
+ * Whether any **variable-definition** line before `position` (1-based,
301
+ * exclusive) is dirty.
302
+ *
303
+ * Narrower than {@link hasAnyDirtyLineBefore}: `VMCheckpointer.snapshot()`
304
+ * only ever records state for lines with `writes.length > 0` (see
305
+ * VMCheckpoints.ts), so a dirty plain-expression line before the viewport
306
+ * cannot have invalidated any checkpoint, there's no checkpoint entry
307
+ * for it to invalidate. Only a dirty variable-def line can mean the VM
308
+ * state a checkpoint would restore is stale.
309
+ *
310
+ * This distinction matters because `PageManager.evictPageBytecode()`
311
+ * marks evicted non-variable-def lines dirty (so they get Tier 1 if
312
+ * scrolled back into view), and Tier 3's compile-only path never clears
313
+ * `dirty` for non-variable-def lines by design. Using the broader
314
+ * `hasAnyDirtyLineBefore` here meant scrolling far into a large,
315
+ * variable-def-free document would trip `setViewport()`'s fallback to
316
+ * `evaluate()` on every single call, evaluate() reprocesses the evicted
317
+ * lines via Tier 3, which recompiles their bytecode without clearing
318
+ * dirty, so the very next `maintainAfterEval()` re-evicts and re-dirties
319
+ * the same lines, forever re-triggering the fallback on an otherwise
320
+ * unchanged viewport.
321
+ */
322
+ hasAnyDirtyVariableDefLineBefore(position: number): boolean;
323
+ /** Number of lines currently marked dirty. For diagnostics/tests. */
324
+ get dirtyCount(): number;
325
+ /**
326
+ * Verify that bytecode compiled by a worker is still valid for this line.
327
+ *
328
+ * When Phase 5.2h sends compilation to a worker, the worker posts back
329
+ * `{lineId, bytecode, reads, writes, compiledAgainstHash}`. Between dispatch
330
+ * and response, the user may have edited the line. This method lets the
331
+ * main thread check whether the bytecode is still applicable.
332
+ *
333
+ * @returns true if the line still exists and its text hash matches.
334
+ */
335
+ isBytecodeValid(lineId: number, compiledAgainstHash: number): boolean;
336
+ /**
337
+ * Mark a line as clean (re-evaluated successfully).
338
+ */
339
+ markClean(lineId: number): void;
340
+ /**
341
+ * Mark a line as dirty (needs re-evaluation) by its 1-based position.
342
+ * Convenience for callers that have line numbers instead of line IDs.
343
+ */
344
+ markDirtyByLineNumber(lineNumber: number): void;
345
+ /**
346
+ * Mark a line as dirty (needs re-evaluation).
347
+ */
348
+ markDirty(lineId: number): void;
349
+ /**
350
+ * Mark all lines as dirty (e.g., after plugin register/unregister).
351
+ */
352
+ invalidateAll(): void;
353
+ /**
354
+ * Update a line's evaluation state after successful execution (Tier 1 / Tier 2).
355
+ *
356
+ * Sets results, bytecodes, reads, writes, and marks the line clean.
357
+ * Supports multi-expression lines (inline solves) via parallel arrays.
358
+ *
359
+ * @param lineId - Persistent line identifier.
360
+ * @param results - Evaluation result groups for each expression (in order). Each element is a Value[].
361
+ * @param bytecodes - Compiled bytecode for each expression (in order).
362
+ * @param expressions - Extracted expression strings (in order).
363
+ * @param reads - Aggregated read variables across all expressions.
364
+ * @param writes - Aggregated write variables across all expressions.
365
+ * @param isVariableDef - True if any expression defines a variable.
366
+ * @param inlineSolveCount - Number of inline solves (0 for full-line).
367
+ */
368
+ updateLineResult(lineId: number, results: Value[][], bytecodes: BytecodeProgram[], expressions: string[], reads: string[], writes: string[], isVariableDef: boolean, inlineSolveCount?: number): void;
369
+ /**
370
+ * Update a line's compile-only state (Tier 3: background compilation).
371
+ *
372
+ * Stores expressions, bytecodes, reads, and writes. Does NOT set results
373
+ * and does NOT mark the line clean, it still needs execution (Tier 1 or
374
+ * Tier 2) to produce results. This distinction allows the three-tier
375
+ * evaluation strategy: compile invisible lines in the background without
376
+ * executing them, then execute from cached bytecode when scrolled into view.
377
+ *
378
+ * @param lineId - Persistent line identifier.
379
+ * @param expressions - Extracted expression strings (in order).
380
+ * @param bytecodes - Compiled bytecode for each expression (in order).
381
+ * @param reads - Aggregated read variables across all expressions.
382
+ * @param writes - Aggregated write variables across all expressions.
383
+ * @param isVariableDef - True if any expression defines a variable.
384
+ * @param inlineSolveCount - Number of inline solves (0 for full-line).
385
+ */
386
+ updateLineCompiled(lineId: number, expressions: string[], bytecodes: BytecodeProgram[], reads: string[], writes: string[], isVariableDef: boolean, inlineSolveCount?: number): void;
387
+ get lineCount(): number;
388
+ get isEmpty(): boolean;
389
+ /**
390
+ * Iterator over LineState in document order.
391
+ */
392
+ [Symbol.iterator](): IterableIterator<LineState>;
393
+ clear(): void;
394
+ /**
395
+ * Serialize the document model to a plain object for debugging.
396
+ */
397
+ toJSON(): object;
398
+ }
399
+
400
+ /** Emitted when one or more lines have updated results after async resolution. */
401
+ interface LinesUpdatedEvent {
402
+ type: "lines-updated";
403
+ /** Line numbers whose results changed (1-based). */
404
+ lineNumbers: number[];
405
+ /** The query keys whose resolution triggered this update. */
406
+ affectedQueryKeys: string[];
407
+ }
408
+ /** Emitted when an async resolution fails with an error. */
409
+ interface AsyncErrorEvent {
410
+ type: "error";
411
+ /** The query key that failed. */
412
+ queryKey: string;
413
+ /** The package whose resolver failed. */
414
+ packageId: string;
415
+ /** The error that caused the failure. */
416
+ error: Error;
417
+ }
418
+ /** Something an async resolution produced: updated lines, or a failure. */
419
+ type AsyncResolutionEvent = LinesUpdatedEvent | AsyncErrorEvent;
420
+ interface BatchEntry {
421
+ /** The query key that just resolved (or errored). */
422
+ queryKey: string;
423
+ /** The package that owns this data. */
424
+ packageId: string;
425
+ /** AbortSignal for staleness detection. */
426
+ signal: AbortSignal;
427
+ /** Whether this was an error resolution. */
428
+ isError: boolean;
429
+ /** Error if isError. */
430
+ error?: Error;
431
+ }
432
+ /**
433
+ * Micro-batches async resolution completions into a single DAG walk + re-evaluation pass.
434
+ *
435
+ * When multiple promises resolve within the same event-loop tick (e.g., 3 currency
436
+ * rates: USD→GBP, USD→EUR, USD→JPY all return within 2ms), this batcher collapses
437
+ * them into ONE DAG walk and ONE re-execution pass instead of 3 separate ones.
438
+ *
439
+ * Lifecycle:
440
+ * - Engine calls `add()` after each `resolveAsync()` completes
441
+ * - `queueMicrotask()` schedules `flush()` for the end of the current tick
442
+ * - `flush()` deduplicates queryKeys, walks DAG once for all resolved keys,
443
+ * topologically sorts affected lines, re-executes, and fires listener events
444
+ *
445
+ * Perf: For N resolutions in a tick, reduces DAG walks from N to 1 and
446
+ * re-executions from N*avgAffected to totalAffected.
447
+ *
448
+ * ## Streaming Architecture
449
+ * * Events are published to a native {@link ReadableStream}. Stream-based
450
+ * consumers read from the stream via `getReader()`, gaining built-in
451
+ * cancellation (`reader.cancel()`), proper resource cleanup
452
+ * (`reader.releaseLock()`), and the ability to
453
+ * `pipeTo()` / `pipeThrough()` / `tee()` the event flow.
454
+ *
455
+ * The stream uses a configurable {@link CountQueuingStrategy} with a default
456
+ * `highWaterMark` of 64 events to limit the internal buffer size.
457
+ */
458
+ declare class AsyncResolutionBatcher {
459
+ private pending;
460
+ private scheduled;
461
+ /** Set to true by clearPending(), flush() checks this to abort stale work. */
462
+ private cleared;
463
+ private dag;
464
+ private lineCache;
465
+ private vm;
466
+ /**
467
+ * Worker pool for offloading VM re-execution when the affected line
468
+ * count exceeds WORKER_OFFLOAD_THRESHOLD (50). Lazily created on
469
+ * first dispatch; cleared on clearAll().
470
+ */
471
+ private executionPool;
472
+ /**
473
+ * Number of flushes actually dispatched to the worker pool (as opposed
474
+ * to falling back to the main thread because Worker is unavailable).
475
+ * Exposed via {@link workerOffloadCount} for the Workers diagnostic tab.
476
+ */
477
+ private workerOffloadDispatchCount;
478
+ /**
479
+ * Default high-water mark for the internal event stream.
480
+ * Limits the internal buffer size before the stream signals to
481
+ * consumers that they need to catch up.
482
+ */
483
+ private static readonly DEFAULT_HIGH_WATER_MARK;
484
+ /**
485
+ * Internal {@link ReadableStream} for async resolution events.
486
+ * All events (lines-updated and error) are enqueued here.
487
+ */
488
+ private _eventStream;
489
+ /**
490
+ * Controller for the internal event stream. Set during stream
491
+ * initialization; cleared on stream cancellation or clearAll().
492
+ */
493
+ private _streamController;
494
+ /**
495
+ * Test-only synchronous capture array. When enabled (non-null), every
496
+ * event is synchronously pushed here in addition to the stream.
497
+ * Tests read from this array to avoid async stream reader timing issues.
498
+ */
499
+ _testCaptures: AsyncResolutionEvent[] | null;
500
+ /**
501
+ * Called for each line whose result is patched after an async resolution,
502
+ * on both the main-thread and worker-pool paths.
503
+ *
504
+ * **A host that displays async results must set this.** It is the only
505
+ * mechanism that moves a resolved value out of the LineCache and into the
506
+ * host's own document state. The engine cannot do it itself: it does not own
507
+ * a document, the host does, and the batcher has no reference to one.
508
+ *
509
+ * Nullable rather than a constructor parameter because it is cleared by
510
+ * `clearAll()` and re-wired on re-subscribe, so it cannot be readonly. That
511
+ * makes it easy to miss, which is why {@link warnIfUnwired} exists: leaving
512
+ * it unset means async values resolve into the cache and are never shown,
513
+ * with nothing to indicate why. A host that genuinely does not want async
514
+ * results should not register async resolvers at all.
515
+ */
516
+ onLineResult: ((lineNumber: number, value: Value) => void) | null;
517
+ /**
518
+ * Whether {@link warnIfUnwired} has already fired.
519
+ *
520
+ * Once per instance, not once per resolution. A document with fifty live
521
+ * data lines would otherwise produce fifty identical warnings on the first
522
+ * fetch, which is how a useful warning becomes noise people filter out.
523
+ */
524
+ private warnedAboutMissingHook;
525
+ /**
526
+ * Warn once if an async result resolved with no {@link onLineResult} wired.
527
+ *
528
+ * The failure this catches is silent by nature: the value arrives, the cache
529
+ * updates, and the line keeps showing pending forever. Without this a host
530
+ * author has no thread to pull on.
531
+ */
532
+ private warnIfUnwired;
533
+ /** High-water mark used when (re)creating the event stream. */
534
+ private readonly highWaterMark;
535
+ constructor(dag: DependencyGraph, lineCache: LineCache, vm: VM, highWaterMark?: number);
536
+ /**
537
+ * Create a fresh internal event stream and wire its controller.
538
+ * Called from the constructor and again from clearAll() so the batcher
539
+ * keeps emitting events after an engine clear, the engine instance
540
+ * (and this batcher) live on across clear() calls.
541
+ */
542
+ private createEventStream;
543
+ /**
544
+ * Add a resolved query key to the pending batch.
545
+ *
546
+ * Called by ExpressionEngine.resolveAsync() after a promise resolves or errors.
547
+ * If this is the first entry in the current tick, schedules a microtask flush.
548
+ */
549
+ add(entry: BatchEntry): void;
550
+ /**
551
+ * Get the native event stream for stream-based consumers.
552
+ *
553
+ * Use this for backpressure, cancellation, or the ability
554
+ * to `pipeTo()` / `pipeThrough()` the event flow.
555
+ *
556
+ * @returns A {@link ReadableStream} that emits {@link AsyncResolutionEvent}
557
+ * items as the batcher processes async resolutions.
558
+ */
559
+ getEventStream(): ReadableStream<AsyncResolutionEvent>;
560
+ /** Number of resolutions currently queued for the next flush. */
561
+ get pendingCount(): number;
562
+ /** Number of pending entries collapsed by (packageId, queryKey) deduplication. */
563
+ get dedupCount(): number;
564
+ /**
565
+ * Whether the internal event stream currently has an active reader.
566
+ * `1` if a consumer has called `getEventStream().getReader()` (or
567
+ * otherwise locked the stream) and not released it, `0` otherwise.
568
+ */
569
+ get listenerCount(): number;
570
+ /** Number of flushes that were actually dispatched to the worker pool. */
571
+ get workerOffloadCount(): number;
572
+ /** Remove all listeners and cancel pending batch. Called on engine clear. */
573
+ clearAll(): void;
574
+ /**
575
+ * Flush all pending resolutions in a single batched pass.
576
+ *
577
+ * Called automatically via queueMicrotask. Never called directly.
578
+ */
579
+ private flush;
580
+ /**
581
+ * Topologically sort affected lines using Kahn's algorithm.
582
+ *
583
+ * Lines that produce variables come before lines that consume them.
584
+ * This ensures correct evaluation order when multiple interdependent
585
+ * lines are affected by async resolution.
586
+ */
587
+ private topologicalSort;
588
+ /**
589
+ * Offload VM re-execution to the worker pool for large batches.
590
+ *
591
+ * Called when ordered.length > WORKER_OFFLOAD_THRESHOLD (50).
592
+ * Clones bytecode ArrayBuffers, dispatches to workers, and asynchronously
593
+ * patches results back into LineCache before notifying listeners.
594
+ *
595
+ * Handles pending results: lines that return { type: 'pending' } from the
596
+ * worker are NOT marked as updated, the engine's resolveAsync will handle
597
+ * them when the async resolver completes.
598
+ *
599
+ * Safety: checks this.cleared before applying results, if the engine was
600
+ * cleared while the worker batch was in-flight, results are discarded.
601
+ */
602
+ private reExecuteViaWorkerPool;
603
+ /**
604
+ * Execute ordered lines on the main thread, update LineCache, and notify
605
+ * listeners. Returns the list of line numbers that actually changed.
606
+ *
607
+ * Used by both flush() (≤50 lines) and reExecuteViaWorkerPool() (fallback
608
+ * when workers are unavailable). Extracted to avoid code duplication.
609
+ *
610
+ * **Per-line containment (fatal-bug fix)**: `executeBytecode()` used to
611
+ * run here with NO try/catch anywhere in this method's call chain, and
612
+ * this whole batch runs inside a bare `queueMicrotask` (see `add()`) with
613
+ * no surrounding try/catch at any caller either, so if any ONE line's
614
+ * cached bytecode threw (a stack/instruction-limit error, an undefined
615
+ * variable, a corrupted-bytecode `TypeError`), the `for` loop aborted
616
+ * immediately: every line scheduled AFTER the failure in this batch was
617
+ * silently never re-executed or notified even though nothing was wrong
618
+ * with them, every line BEFORE it had already had its `entry.result`
619
+ * mutated in-place but `notifyListeners()` was never reached (a silent
620
+ * `LineCache`/host desync), and, because a bare `queueMicrotask`
621
+ * callback has no caller to catch it, the exception was uncatchable:
622
+ * an `uncaughtException` that could crash the host process outright.
623
+ * (`__tests__/async/AsyncResolutionBatcher.spec.ts`'s topological-sort
624
+ * describe block used to have a test skipped specifically because of
625
+ * this. See that file, now un-skipped and rewritten.) Each line's
626
+ * execution is now its own try/catch: a failure is recorded as an
627
+ * `Error` `Value` for THAT line (still counted as "updated" so the host
628
+ * learns about it and stops showing a stale Pending state) and the loop
629
+ * continues, one line's failure can no longer take out its neighbors.
630
+ */
631
+ private reExecuteMainThread;
632
+ /**
633
+ * Notify all consumers of an async resolution event.
634
+ *
635
+ * Enqueues the event into the internal {@link ReadableStream}.
636
+ * If the stream has been closed or errored (consumer cancelled),
637
+ * the enqueue silently fails (caught by try/catch).
638
+ */
639
+ private notifyListeners;
640
+ }
641
+
642
+ /** The pipeline stages allocation is attributed to. */
643
+ type PipelineStage = 'lexer' | 'normalizer' | 'parser' | 'resolver' | 'vm' | 'orchestrator';
644
+ /** What one stage allocated during one measured span. */
645
+ interface StageAllocation {
646
+ /** Pipeline stage name. */
647
+ stage: PipelineStage;
648
+ /** Number of objects allocated (reserved, requires --expose-gc snapshots). */
649
+ allocCount: number;
650
+ /** Heap bytes allocated (delta of process.memoryUsage().heapUsed). */
651
+ allocBytes: number;
652
+ /** Breakdown of object types allocated (reserved, requires heap snapshots). */
653
+ objectCounts: Record<string, number>;
654
+ /** Wall-clock duration in nanoseconds. */
655
+ wallTimeNs: number;
656
+ /** Optional: sub-stage name for finer-grained tracking within a stage. */
657
+ subStage?: string;
658
+ /** Optional: whether this stage hit a cache (zero-parse, zero-lex). */
659
+ cacheHit?: boolean;
660
+ }
661
+ /** Per-stage allocation for a single expression, in pipeline order. */
662
+ interface PipelineTelemetry {
663
+ /** The expression text that was evaluated. */
664
+ expression: string;
665
+ /** Per-stage allocation and timing data, in execution order. */
666
+ stages: StageAllocation[];
667
+ /** Total heap bytes allocated across all stages. */
668
+ totalAllocBytes: number;
669
+ /** Total wall-clock duration in nanoseconds. */
670
+ totalWallTimeNs: number;
671
+ /** Whether the expression took the orchestrator fast path. */
672
+ fastPath: boolean;
673
+ }
674
+
675
+ /**
676
+ * Result shapes returned when a whole document is parsed rather than a single
677
+ * expression.
678
+ *
679
+ * These describe what a host renders: which lines carry results, where in each
680
+ * line those results belong, and what went wrong where it did.
681
+ */
682
+
683
+ /**
684
+ * One inline solve, and where it sits in its line.
685
+ *
686
+ * An inline solve is an expression embedded in prose between backticks. A line
687
+ * can hold several, so positions are what let a host place each result against
688
+ * the right span rather than at the end of the line.
689
+ */
690
+ interface InlineSolvePosition {
691
+ /** Character offset where the expression starts, within its line. */
692
+ start: number;
693
+ /** Character offset just past the end of the expression. */
694
+ end: number;
695
+ /** The expression text, without the surrounding backticks. */
696
+ expression: string;
697
+ /** One-based line number. */
698
+ lineNumber: number;
699
+ /** One-based column, for hosts that report positions to a user. */
700
+ columnNumber: number;
701
+ /** The evaluated value, or null when evaluation failed or was skipped. */
702
+ result?: Value | null;
703
+ /** Message when this span failed, leaving others on the line intact. */
704
+ error?: string | null;
705
+ }
706
+ /**
707
+ * One line of a parsed document.
708
+ *
709
+ * A line carries either a whole-line expression or a set of inline solves,
710
+ * never both. `isEmpty` covers the third case: prose, a heading, or a blank.
711
+ */
712
+ interface ParsedLine {
713
+ /** One-based line number. */
714
+ lineNumber: number;
715
+ /** The raw line text as it appeared. */
716
+ text: string;
717
+ /** Character offset of the line start within the whole document. */
718
+ startPosition: number;
719
+ /** Character offset just past the line end within the whole document. */
720
+ endPosition: number;
721
+ /** Whether the line holds nothing to evaluate. */
722
+ isEmpty: boolean;
723
+ /** Whether the line holds backtick-delimited expressions. */
724
+ hasInlineSolves: boolean;
725
+ /** Each embedded expression and its position. Empty for a whole-line expression. */
726
+ inlineSolves: InlineSolvePosition[];
727
+ /** The whole-line expression, or null when the line has inline solves or nothing. */
728
+ expression: string | null;
729
+ /** The whole-line result, or null when there is none or it failed. */
730
+ result: Value | null;
731
+ /** Message when the whole-line expression failed. */
732
+ error: string | null;
733
+ }
734
+ /**
735
+ * Everything produced by parsing a document.
736
+ *
737
+ * Errors are collected rather than thrown. One bad line must not cost the
738
+ * results of every other line, so failures are reported alongside the lines
739
+ * that did evaluate.
740
+ */
741
+ interface ParsingResult {
742
+ /** Every line in document order, including ones with nothing to evaluate. */
743
+ lines: ParsedLine[];
744
+ /** Line count, so a caller need not measure `lines`. */
745
+ totalLines: number;
746
+ /** Messages from lines that failed. Those lines are still present above. */
747
+ errors: string[];
748
+ /** Stage-by-stage trace, present only when diagnostics were requested. */
749
+ diagnostics?: DiagnosticReportJSON;
750
+ }
751
+ /** How a document should be parsed. */
752
+ interface UnifiedParsingOptions {
753
+ /**
754
+ * How to read the input.
755
+ *
756
+ * `markdown` respects headings, comments and code fences, and looks for
757
+ * inline solves. `raw` treats every line as an expression. `code` is for
758
+ * input already known to be expressions.
759
+ */
760
+ inputType: 'markdown' | 'raw' | 'code';
761
+ /** Locale for keywords and number formatting. Defaults to English. */
762
+ localeCode?: string;
763
+ /** Include line offsets and positions. Costs a little to compute. */
764
+ includeLineInfo?: boolean;
765
+ /** Include syntax-highlighting spans. */
766
+ includeHighlights?: boolean;
767
+ /** Collect a stage-by-stage trace. For tooling only: it is not free. */
768
+ includeDiagnostics?: boolean;
769
+ }
770
+
771
+ /**
772
+ * DiagnosticPipelineResult, structured pipeline data contract.
773
+ *
774
+ * ## Purpose
775
+ * When `diagnosticMode = true`, the {@link ExpressionEngine} populates this
776
+ * structured result alongside the existing event-based diagnostic system.
777
+ * The playground consumes the `stages[]` array directly, no event
778
+ * reconstruction or timeline parsing needed.
779
+ *
780
+ * ## Dual-mode design
781
+ * - **Production** (`diagnosticMode = false`): stages are empty, zero overhead
782
+ * - **Diagnostic** (`diagnosticMode = true`): all 15 pipeline stages are populated
783
+ * with typed outputs, plus the evaluation result (value, tokens, bytecode)
784
+ *
785
+ * ## Stage numbering
786
+ * | Step | Stage | Description |
787
+ * |------|--------------------|---------------------------------------------|
788
+ * | 1 | pipeline_start | Pipeline initialization |
789
+ * | 2 | safety_length | Expression length validation |
790
+ * | 3 | lexer | Token production |
791
+ * | 4 | normalizer | TokenNormalizer phrase fusion + implicit ops|
792
+ * | 5 | safety_complexity | Complexity score validation |
793
+ * | 6 | readwrite | Variable reads/writes extraction |
794
+ * | 7 | cache_check | Bytecode cache lookup |
795
+ * | 8 | parser | Pratt parser (skipped on cache hit) |
796
+ * | 9 | compiler | Bytecode compiler (skipped on cache hit) |
797
+ * | 10 | async_preflight | Async resolver preflight check |
798
+ * | 11 | vm_execute | Stack VM execution |
799
+ * | 12 | dag_registration | Dependency graph registration |
800
+ * | 13 | linecache | Line-cache storage |
801
+ * | 14 | result | Formatted result value |
802
+ * | 15 | pipeline_end | Pipeline completion summary |
803
+ *
804
+ * @module DiagnosticPipelineResult
805
+ */
806
+
807
+ /**
808
+ * A single pipeline stage result displayed in the playground's Pipeline tab.
809
+ *
810
+ * Each stage has display metadata (label, icon, color, step number) and
811
+ * a typed {@link StageOutput | output} payload. Stages are ordered by
812
+ * {@link stepNumber} and rendered as a vertical flow.
813
+ */
814
+ interface PipelineStageResult {
815
+ /**
816
+ * Unique stage identifier: `'lexer'`, `'normalizer'`, `'vm_execute'`, etc.
817
+ * Used by the PipelineTab to select the correct renderer.
818
+ */
819
+ stage: string;
820
+ /** Human-readable label (e.g., "VM Execute", "Safety: Length") */
821
+ label: string;
822
+ /** Emoji icon for the stage header (e.g., "⚡", "🔤") */
823
+ icon: string;
824
+ /**
825
+ * CSS color class token for styling the stage header.
826
+ * Maps to `.flow-stage-header.{colorClass}` in main.css.
827
+ */
828
+ colorClass: string;
829
+ /** Sequential step number in the pipeline (1–15) */
830
+ stepNumber: number;
831
+ /**
832
+ * Elapsed nanoseconds from pipeline start.
833
+ * 0 when timing is disabled or the timeline collector isn't active.
834
+ */
835
+ elapsedNs: number;
836
+ /**
837
+ * Whether this stage was skipped.
838
+ * Examples: parser/compiler skipped on cache hit, normalizer skipped with no rules.
839
+ */
840
+ skipped: boolean;
841
+ /** Stage-specific typed output (see {@link StageOutput}) */
842
+ output: StageOutput;
843
+ }
844
+ /**
845
+ * Discriminated union of all pipeline stage output types.
846
+ *
847
+ * Each stage produces a specific output interface with a `type` discriminator.
848
+ * The playground uses `stage.output.type` to determine which renderer to use.
849
+ */
850
+ type StageOutput = PipelineStartOutput | LineClassificationOutput | SafetyLengthOutput | LexerOutput | NormalizerOutput | SafetyComplexityOutput | ReadWriteOutput | CacheCheckOutput | ParserOutput | CompilerOutput | AsyncPreflightOutput | VmExecuteOutput | DagRegistrationOutput | LineCacheOutput | ResultOutput | PipelineEndOutput;
851
+ /** Stage 1: Pipeline initialization with the expression and input type. */
852
+ interface PipelineStartOutput {
853
+ type: "pipeline_start";
854
+ /** The raw expression being evaluated */
855
+ expression: string;
856
+ /** Input type: `"expression"`, `"markdown"`, etc. */
857
+ inputType: string;
858
+ }
859
+ /**
860
+ * Lightweight inline solve span with token indices for diagnostic rendering.
861
+ *
862
+ * Unlike {@link InlineSolveSpan} (which carries full character offsets),
863
+ * this is a diagnostics-only struct focused on token-level access. The
864
+ * playground uses `startTokenIndex`/`endTokenIndex` to highlight tokens
865
+ * within the inline solve, and `expression` to show what was evaluated.
866
+ */
867
+ interface InlineSolveSpanInfo {
868
+ /** Token index of INLINE_SOLVE_START in the expression's token array */
869
+ startTokenIndex: number;
870
+ /** Token index of closing BACKTICK_OPEN in the expression's token array */
871
+ endTokenIndex: number;
872
+ /** The expression text between the backticks */
873
+ expression: string;
874
+ /** 1-based column of the `s`` marker */
875
+ columnNumber: number;
876
+ }
877
+ /** Stage 2: Markdown line classification (used by scanDocument path). */
878
+ interface LineClassificationOutput {
879
+ type: "line_classification";
880
+ /** The markdown line type (heading, list, expression, etc.) */
881
+ classification: MarkdownLineType;
882
+ /** Whether this line should be skipped entirely */
883
+ skip: boolean;
884
+ /** Whether the line contains inline solve markers (`s`...``) */
885
+ hasInlineSolve: boolean;
886
+ /** Inline solve spans with token indices (empty if none detected). */
887
+ inlineSolveSpans: InlineSolveSpanInfo[];
888
+ }
889
+ /** Stage 3: Expression length validation against the configured limit. */
890
+ interface SafetyLengthOutput {
891
+ type: "safety_length";
892
+ /** Whether the expression passed the length check */
893
+ passed: boolean;
894
+ /** Actual length of the expression in characters */
895
+ expressionLength: number;
896
+ /** Maximum allowed expression length from config */
897
+ maxLength: number;
898
+ /** Error message when the check fails */
899
+ errorMessage?: string;
900
+ }
901
+ /** Stage 4: Raw token production from the ExpressionLexer. */
902
+ interface LexerOutput {
903
+ type: "lexer";
904
+ /** Total number of tokens produced (excluding whitespace) */
905
+ tokenCount: number;
906
+ /** Counts per token type (e.g., { NUMBER: 3, IDENT: 1, PLUS: 1 }) */
907
+ tokenTypes: Record<string, number>;
908
+ /** Whether parentheses are present in the expression */
909
+ hasParens: boolean;
910
+ /** Locale used for keyword resolution (e.g., "en") */
911
+ locale: string;
912
+ /**
913
+ * Raw tokens before normalization.
914
+ * These are the tokens as they come out of the lexer, before any
915
+ * phrase fusion or implicit operator insertion.
916
+ */
917
+ tokens: Token[];
918
+ }
919
+ /** Stage 5: TokenNormalizer pass with fusion tracking for diagnostic display. */
920
+ interface NormalizerOutput {
921
+ type: "normalizer";
922
+ /** Number of tokens before normalization */
923
+ inputTokenCount: number;
924
+ /** Number of tokens after normalization (may be fewer due to fusions) */
925
+ outputTokenCount: number;
926
+ /**
927
+ * Fusion events recorded during normalization.
928
+ * Each entry shows the rule, source tokens, and resulting fused token.
929
+ * Rendered as the fusion detail table in the Pipeline tab.
930
+ */
931
+ fusions: TokenFusion[];
932
+ /** Per-rule application counts (e.g., [{ rule: "phrase:to the power of", count: 1 }]) */
933
+ rulesApplied: {
934
+ rule: string;
935
+ count: number;
936
+ }[];
937
+ /** Post-normalization tokens ready for parsing */
938
+ tokens: Token[];
939
+ /**
940
+ * All registered phrase → tokenType mappings from the PhraseTrie.
941
+ * Populated by the engine at diagnostic stage build time so the
942
+ * playground NormalizerTab can render the complete trie structure
943
+ * rather than only phrases that matched in this evaluation.
944
+ */
945
+ phrases: Record<string, string>;
946
+ }
947
+ /** Stage 6: Complexity scoring against the configured limit. */
948
+ interface SafetyComplexityOutput {
949
+ type: "safety_complexity";
950
+ /** Whether the expression passed the complexity check */
951
+ passed: boolean;
952
+ /** Computed complexity score (tokens + function calls × 5 + max parens × 10) */
953
+ complexityScore: number;
954
+ /** Maximum allowed complexity from config */
955
+ maxComplexity: number;
956
+ /** Breakdown of the complexity score components */
957
+ breakdown: {
958
+ /** Number of tokens in the expression */
959
+ tokenCount: number;
960
+ /** Number of function call sites detected */
961
+ functionCalls: number;
962
+ /** Maximum parenthesis nesting depth */
963
+ nestingDepth: number;
964
+ };
965
+ /** Error message when the check fails */
966
+ errorMessage?: string;
967
+ }
968
+ /** Stage 7: Variable reads and writes extracted from the token stream. */
969
+ interface ReadWriteOutput {
970
+ type: "readwrite";
971
+ /** Variable names read by this expression */
972
+ reads: string[];
973
+ /** Variable names written (assigned) by this expression */
974
+ writes: string[];
975
+ /** Whether this expression is a variable assignment (`:var = ...`) */
976
+ isAssignment: boolean;
977
+ }
978
+ /** Stage 8: Bytecode cache lookup before parsing. */
979
+ interface CacheCheckOutput {
980
+ type: "cache_check";
981
+ /** Whether the expression was found in the bytecode cache */
982
+ hit: boolean;
983
+ /** Current number of entries in the bytecode cache */
984
+ cacheSize: number;
985
+ /** Cache key used for lookup (typically the expression text) */
986
+ cacheKey: string;
987
+ }
988
+ /** Stage 9: Pratt parser producing an AST from normalized tokens. Skipped on cache hit. */
989
+ interface ParserOutput {
990
+ type: "parser";
991
+ /** Matched parselets during parsing */
992
+ parselets: {
993
+ type: string;
994
+ category: string;
995
+ prefix: boolean;
996
+ }[];
997
+ /** Unique parselet type names used in this expression */
998
+ uniqueParseletTypes: string[];
999
+ /** Maximum AST depth reached during parsing */
1000
+ astDepth: number;
1001
+ }
1002
+ /** Stage 10: Bytecode compiler (AST → opcodes). Skipped on cache hit. */
1003
+ interface CompilerOutput {
1004
+ type: "compiler";
1005
+ /** Number of opcodes in the compiled program */
1006
+ opcodeCount: number;
1007
+ /** Number of numeric constants in the constant pool */
1008
+ numberConstants: number;
1009
+ /** Number of string constants in the constant pool */
1010
+ stringConstants: number;
1011
+ /** Whether the program contains async opcodes (CALL_PLUGIN) */
1012
+ hasAsync: boolean;
1013
+ /** Whether the program was served from cache */
1014
+ cached: boolean;
1015
+ }
1016
+ /** Stage 11: Async resolver preflight check before VM execution. */
1017
+ interface AsyncPreflightOutput {
1018
+ type: "async_preflight";
1019
+ /**
1020
+ * Resolution path.
1021
+ * - `"sync"`: No async resolvers needed, proceed to VM
1022
+ * - `"pending"`: Data not ready, return pending value and await resolution
1023
+ */
1024
+ path: "sync" | "pending";
1025
+ /** Query key if the expression is pending async resolution */
1026
+ pendingQueryKey?: string;
1027
+ /** Number of registered async resolvers */
1028
+ resolverCount: number;
1029
+ /** Whether the preflight guard was skipped (no async opcodes, no resolvers) */
1030
+ skippedGuard: boolean;
1031
+ }
1032
+ /** Stage 12: Stack VM execution of the compiled bytecode program. */
1033
+ interface VmExecuteOutput {
1034
+ type: "vm_execute";
1035
+ /** Total opcode instructions executed */
1036
+ totalInstructions: number;
1037
+ /** Maximum stack depth reached during execution */
1038
+ stackDepth: number;
1039
+ /** Result type: `"Number"`, `"Uom"`, `"Pending"`, etc. */
1040
+ resultType: string;
1041
+ /** String representation of the result value */
1042
+ resultValue: string;
1043
+ /** Whether the VM returned a pending async result */
1044
+ isPending: boolean;
1045
+ }
1046
+ /** Stage 13: Dependency graph registration for incremental re-evaluation. */
1047
+ interface DagRegistrationOutput {
1048
+ type: "dag_registration";
1049
+ /** Variable reads registered in the DAG */
1050
+ readsRegistered: string[];
1051
+ /** Variable writes registered in the DAG */
1052
+ writesRegistered: string[];
1053
+ /** Data source dependencies registered in the DAG */
1054
+ dataSourcesRegistered: string[];
1055
+ }
1056
+ /** Stage 14: Line-cache storage of the evaluation result. */
1057
+ interface LineCacheOutput {
1058
+ type: "linecache";
1059
+ /** Line number in the document (1-based) */
1060
+ lineNumber: number;
1061
+ /** The expression text stored in cache */
1062
+ expression: string;
1063
+ /** Whether the result was successfully stored */
1064
+ stored: boolean;
1065
+ }
1066
+ /** Stage 14: Final evaluation result with formatting. */
1067
+ interface ResultOutput {
1068
+ type: "result";
1069
+ /** Raw value as a string */
1070
+ rawValue: string;
1071
+ /** Human-friendly formatted value (with locale formatting, units, etc.) */
1072
+ formattedValue: string;
1073
+ /** Value type: `"Number"`, `"Uom"`, `"Percentage"`, etc. */
1074
+ valueType: string;
1075
+ /** Unit of measurement if the result carries a unit (e.g., "km") */
1076
+ unit?: string;
1077
+ /** Error message if evaluation failed (null on success) */
1078
+ error?: string;
1079
+ }
1080
+ /** Stage 15: Pipeline completion summary with final statistics. */
1081
+ interface PipelineEndOutput {
1082
+ type: "pipeline_end";
1083
+ /** Whether the pipeline completed successfully */
1084
+ success: boolean;
1085
+ /** Total tokens processed (pre-normalization) */
1086
+ totalTokens: number;
1087
+ /** Total opcodes in the compiled program */
1088
+ totalOpcodes: number;
1089
+ /** Whether the result came from the bytecode cache */
1090
+ cacheHit: boolean;
1091
+ }
1092
+ /**
1093
+ * Complete diagnostic pipeline result produced by
1094
+ * {@link ExpressionEngine.evaluateExpressionWithDiagnostic | evaluateExpressionWithDiagnostic()}
1095
+ * when `diagnosticMode = true`.
1096
+ *
1097
+ * Includes all 15 pipeline stages with typed outputs, plus the evaluation
1098
+ * result data (value, tokens, bytecode). The playground renders the `stages`
1099
+ * array as a data-driven pipeline flow diagram without any reconstruction
1100
+ * from diagnostic events.
1101
+ *
1102
+ * When `diagnosticMode = false` (production), the `diagnostic` field is
1103
+ * `undefined` and only the minimal result data is returned.
1104
+ */
1105
+ interface DiagnosticPipelineResult {
1106
+ /**
1107
+ * Ordered pipeline stages (15 stages total).
1108
+ * Each stage has display metadata and a typed output payload.
1109
+ */
1110
+ stages: PipelineStageResult[];
1111
+ /** The final evaluation result value */
1112
+ value: Value;
1113
+ /** Normalized tokens (post-TokenNormalizer, ready for parsing) */
1114
+ tokens: Token[];
1115
+ /** The compiled bytecode program */
1116
+ program: BytecodeProgram;
1117
+ /** Error message if evaluation failed, `null` otherwise */
1118
+ error: string | null;
1119
+ /** DAG dependency graph snapshot (consumers, writes, reads, dataSourceDeps) */
1120
+ dagSnapshot?: DagSnapshot;
1121
+ /** Engine-wide cache snapshot (bytecode, line cache, async cache) */
1122
+ cacheSnapshot?: CacheSnapshot;
1123
+ /** AsyncResolutionBatcher metrics (pending, dedup, worker, listener counts) */
1124
+ batcherMetrics?: BatcherMetrics;
1125
+ /** VM checkpoints from the ThreeTierEvaluator's checkpointer */
1126
+ checkpoints?: CheckpointSnapshot[];
1127
+ }
1128
+ /** Bytecode cache entry for diagnostic rendering. */
1129
+ interface BytecodeCacheEntry {
1130
+ expression: string;
1131
+ opcodesLength: number;
1132
+ numbersLength: number;
1133
+ stringsLength: number;
1134
+ hasAsync: boolean;
1135
+ }
1136
+ /** Line cache entry info for diagnostic rendering. */
1137
+ /** One line's cached bytecode and the variables it reads and writes. */
1138
+ interface LineCacheEntryInfo {
1139
+ key: string;
1140
+ lineNumber: number;
1141
+ resultType: string;
1142
+ resultValue: string;
1143
+ reads: string[];
1144
+ writeVar: string | null;
1145
+ }
1146
+ /** Async cache package info for diagnostic rendering. */
1147
+ interface AsyncCachePackageInfo {
1148
+ packageId: string;
1149
+ resolvedCount: number;
1150
+ inFlightCount: number;
1151
+ errorCount: number;
1152
+ /** Per-package TTL in milliseconds (undefined = no expiry). */
1153
+ ttlMs?: number;
1154
+ entries: Array<{
1155
+ key: string;
1156
+ status: "resolved" | "in_flight" | "error";
1157
+ errorMessage?: string;
1158
+ createdAt?: number;
1159
+ value?: string;
1160
+ }>;
1161
+ }
1162
+ /** Full cache snapshot for diagnostic rendering. */
1163
+ interface CacheSnapshot {
1164
+ bytecode: BytecodeCacheEntry[];
1165
+ lineCache: LineCacheEntryInfo[];
1166
+ asyncCache: AsyncCachePackageInfo[];
1167
+ }
1168
+ /** Batcher metrics for Workers diagnostic tab. */
1169
+ interface BatcherMetrics {
1170
+ pendingCount: number;
1171
+ dedupCount: number;
1172
+ workerOffloadCount: number;
1173
+ listenerCount: number;
1174
+ }
1175
+ /** VM checkpoint snapshot for diagnostic rendering. */
1176
+ interface CheckpointSnapshot {
1177
+ lineNumber: number;
1178
+ variables: string[];
1179
+ variableCount: number;
1180
+ }
1181
+
1182
+ /**
1183
+ * Return type of {@link evaluateLine} and {@link evaluateExpression}.
1184
+ *
1185
+ * A single-element `Value[]`, kept as an array (rather than a bare
1186
+ * `Value`) for API stability.
1187
+ */
1188
+ interface EvalResults extends Array<Value> {
1189
+ }
1190
+ /** Explicit result of {@link ExpressionEngine.evaluateLineDetailed}. */
1191
+ interface LineEvaluation {
1192
+ /** The evaluated value, wrapped in a single-element array. */
1193
+ values: Value[];
1194
+ }
1195
+ /**
1196
+ * Core expression evaluation engine, the top-level orchestrator.
1197
+ *
1198
+ * Owns the full evaluation pipeline: lexing, parsing, bytecode compilation,
1199
+ * VM execution, DAG-based dependency tracking, and async resolution.
1200
+ *
1201
+ * Key responsibilities:
1202
+ * - Pipeline orchestration: lex → parse → compile → execute → cache
1203
+ * - Bytecode caching for repeated expressions
1204
+ * - DAG-based incremental re-evaluation on variable changes
1205
+ * - Async resolution via ResolverRegistry + AsyncResolutionBatcher
1206
+ * - Package registration (built-in + external plugins)
1207
+ * - Safety validation (length, complexity, nesting)
1208
+ * - Diagnostic pipeline integration
1209
+ * - Keystroke-level AbortSignal management
1210
+ *
1211
+ * Each engine instance has its own isolated lexer, registry, parser, and
1212
+ * LineCache. The VM uses this engine's own opcode registry, and each engine
1213
+ * creates its own VM instance with configurable limits.
1214
+ *
1215
+ * ## Lifecycle
1216
+ *
1217
+ * Call {@link ExpressionEngine.clear} when you are finished with an engine that
1218
+ * has parsed a document. Dropping your last reference is not sufficient: the
1219
+ * async batcher is reachable from the module-level data-query service, so a
1220
+ * parsed engine stays retained until `clear()` releases it. Measured per engine
1221
+ * after a forced collection, with a 40-line document:
1222
+ *
1223
+ * | Lifecycle | Retained |
1224
+ * | --- | --- |
1225
+ * | constructed, never parsed | 8.2KB |
1226
+ * | constructed and parsed | 46.9KB |
1227
+ * | constructed, parsed, cleared | 10.0KB |
1228
+ *
1229
+ * This matters for hosts that create one engine per document or per tab, which
1230
+ * is the intended usage. Reusing a single engine across documents is also fine:
1231
+ * `clear()` resets it for the next one rather than consuming it.
1232
+ *
1233
+ * @example
1234
+ * ```typescript
1235
+ * import { ExpressionEngine } from "solve-engine";
1236
+ * const engine = new ExpressionEngine("en");
1237
+ * const [value] = engine.evaluateExpression("2 + 2 * 10");
1238
+ * console.log(value.toNumber()); // 22
1239
+ * ```
1240
+ */
1241
+ declare class ExpressionEngine {
1242
+ private dag;
1243
+ private lineCache;
1244
+ private scopeManager;
1245
+ private lexer;
1246
+ private registry;
1247
+ private parser;
1248
+ private localeCode;
1249
+ private vm;
1250
+ /**
1251
+ * Registries this engine owns, rather than shares with every other
1252
+ * instance in the process. See {@link EngineContext}.
1253
+ */
1254
+ private readonly context;
1255
+ private config;
1256
+ private diagnosticPipeline;
1257
+ /**
1258
+ * Direct reference to the timeline collector registered above (when
1259
+ * diagnosticMode is on), kept alongside the generic `diagnosticPipeline`
1260
+ * so `evaluateExpressionWithDiagnostic()` can cheaply read its current
1261
+ * `parseletMatchCount` as a "before" baseline without paying for a full
1262
+ * `getReport()` build (which copies the whole cumulative parselets
1263
+ * array). See that method's use of it for why a baseline is needed at
1264
+ * all, `TimelineDiagnosticCollector`'s state is deliberately cumulative
1265
+ * across an entire document pass, not reset per line.
1266
+ */
1267
+ private timelineCollector?;
1268
+ /** Registry of async resolvers from registered packages. */
1269
+ private resolverRegistry;
1270
+ /**
1271
+ * Per-package record of contributions made to the SHARED registries
1272
+ * (this engine's variable resolver / resolver namespaces), so
1273
+ * {@link unregisterPackage} can reverse them. Keyed by package name.
1274
+ */
1275
+ private packageContributions;
1276
+ /**
1277
+ * The actual `IEnginePackage` descriptor of every package currently
1278
+ * registered on this engine instance, keyed by name, separate from
1279
+ * {@link packageContributions} (which tracks what was WRITTEN into
1280
+ * shared registries, not the original descriptor). Used by
1281
+ * {@link registerPackage}'s automatic `checkPackageCompatibility()` call
1282
+ *. See `api/PackageCompatibility.ts`'s module doc for why this exists.
1283
+ */
1284
+ private registeredPackages;
1285
+ /**
1286
+ * The `DocumentModel` this engine is currently evaluating, if any
1287
+ * `null` for a bare engine with no document (e.g. anything only ever
1288
+ * calling `evaluateExpression()`). `ExpressionEngine` doesn't own its
1289
+ * `DocumentModel` (`ThreeTierEvaluator` constructs and owns both as
1290
+ * siblings). This is set once, via {@link setDocumentModel}, purely so
1291
+ * {@link makeLineContext} can answer "what's line N's cached result"
1292
+ * for cross-line features (`prev`/`line<N>`/aggregation. See
1293
+ * `packages/lines/`) without the engine needing to own document
1294
+ * lifecycle itself.
1295
+ */
1296
+ private documentModel;
1297
+ /**
1298
+ * Called once by `ThreeTierEvaluator`'s constructor. Not part of the
1299
+ * public evaluate-a-document contract, purely internal wiring so
1300
+ * {@link makeLineContext} has something to read from.
1301
+ */
1302
+ setDocumentModel(doc: DocumentModel | null): void;
1303
+ /**
1304
+ * Build the {@link LineExecutionContext} passed to `executeBytecode()`
1305
+ * for a given line. `lineNumber = -1` (the existing sentinel
1306
+ * `evaluateExpression()`/`evaluateLine(-1, ...)` already use for "no
1307
+ * real document") naturally produces a context with both closures
1308
+ * `undefined`, a cross-line plugin function must check for that itself
1309
+ * and return a clear error, never assume line 0 exists.
1310
+ */
1311
+ private makeLineContext;
1312
+ /**
1313
+ * Package-contributed completion candidates (`IEnginePackage.completionItems`),
1314
+ * keyed by package name, engine-instance-local, not a shared registry
1315
+ * (unlike tokenCategories), so no separate register/unregister module is
1316
+ * needed: registerPackage()/unregisterPackage() just set/delete the
1317
+ * package's own entry. {@link getPackageCompletionItems} flattens it.
1318
+ */
1319
+ private packageCompletionItems;
1320
+ /**
1321
+ * Keystroke-level AbortSignal, set by the UI layer (MarkdownEditorViewPlugin)
1322
+ * before each evaluation. When the user types a new keystroke, the old signal
1323
+ * is aborted, causing all in-flight async work (fetches, preflight checks,
1324
+ * batcher flushes) to be canceled atomically.
1325
+ *
1326
+ * executeAndStore() and executeRaw() link their local AbortControllers to
1327
+ * this signal so that when the keystroke changes, all per-evaluation controllers
1328
+ * are aborted together.
1329
+ */
1330
+ private keystrokeSignal;
1331
+ /**
1332
+ * Micro-batcher that collapses multiple async resolutions into a single
1333
+ * DAG walk + re-evaluation pass. Replaces the old single-callback pattern.
1334
+ */
1335
+ private batcher;
1336
+ /** Post-lexer token normalizer for phrase fusion, implicit multiply, etc. */
1337
+ private normalizer;
1338
+ /** TanStack Query client, injected into resolvers for cache reads/writes. */
1339
+ readonly queryClient: QueryClient;
1340
+ private bytecodeCache;
1341
+ /**
1342
+ * Insert into the bytecode cache, evicting the oldest entry when full.
1343
+ *
1344
+ * Bug fix (found during release hardening): this used to check against
1345
+ * a hardcoded `BYTECODE_CACHE_MAX_ENTRIES = 2000` constant that never
1346
+ * read `config.performance.defaultCacheSize`, despite that field being
1347
+ * documented (see `EngineConfig`'s JSDoc example) as exactly this knob.
1348
+ * A host raising `defaultCacheSize` for large documents had zero effect;
1349
+ * every document beyond ~2000 unique expressions per line silently lost
1350
+ * the bytecode-cache benefit on re-evaluation regardless of config.
1351
+ */
1352
+ private cacheBytecode;
1353
+ private builderPool;
1354
+ private builderPoolIndex;
1355
+ private lastTelemetry;
1356
+ constructor(localeCode?: string, diagnosticMode?: boolean, config?: Partial<typeof DEFAULT_CONFIG>, diagnosticPipeline?: DiagnosticPipeline, packages?: IEnginePackage[]);
1357
+ /**
1358
+ * Get the native event stream from the batcher for stream-based consumers.
1359
+ *
1360
+ * Use this instead of `addAsyncListener()` when you need:
1361
+ * - **Backpressure**: the stream buffers up to `highWaterMark` events;
1362
+ * when full, `enqueue()` blocks until the consumer reads, preventing
1363
+ * unbounded memory growth.
1364
+ * - **Cancellation**: call `reader.cancel()` or pass an `AbortSignal` to
1365
+ * `pipeTo()` to stop receiving events.
1366
+ * - **Piping**: use `stream.pipeTo(writable)` or `stream.pipeThrough(transform)`
1367
+ * to build a reactive pipeline.
1368
+ * - **Teeing**: use `stream.tee()` to serve multiple independent consumers.
1369
+ *
1370
+ * @returns A {@link ReadableStream} that emits {@link AsyncResolutionEvent}
1371
+ * items as the batcher processes async resolutions.
1372
+ */
1373
+ getEventStream(): ReadableStream<AsyncResolutionEvent>;
1374
+ /**
1375
+ * Get the batcher instance (for test infrastructure).
1376
+ *
1377
+ * Tests use this to access `batcher._testCaptures` for synchronous
1378
+ * event observation without async stream reader timing issues.
1379
+ */
1380
+ getBatcher(): AsyncResolutionBatcher;
1381
+ /**
1382
+ * Register a package with the engine's isolated registries.
1383
+ *
1384
+ * Handles all IEnginePackage fields:
1385
+ * - `lexerVocabulary` → engine's isolated lexer (via this.lexer.registerVocabulary)
1386
+ * - `prefixParselets` → engine's isolated ParseletRegistry
1387
+ * - `infixParselets` → engine's isolated ParseletRegistry
1388
+ * - `variableSources` → this engine's own variable resolver
1389
+ *
1390
+ * Built-in packages (ARITHMETIC, FUNCTION, UOM, etc.) are registered
1391
+ * via this method during construction. External user packages can also
1392
+ * use this method for data-driven registration.
1393
+ *
1394
+ * @param pkg - The package to register.
1395
+ */
1396
+ registerPackage(pkg: IEnginePackage): void;
1397
+ /**
1398
+ * Unregister a package previously registered via {@link registerPackage}.
1399
+ *
1400
+ * Reverses the package's contributions to the SHARED registries, plugin
1401
+ * functions (pluginFunctionRegistry), variable sources
1402
+ * (this engine's variable resolver), async resolvers, and now
1403
+ * token highlight categories (TokenCategoryMap), which registerPackage
1404
+ * wrote into process-wide state. Also reverts
1405
+ * the package's lexer plugin (custom keyword/operator token types
1406
+ * revert to generic IDENT/ERROR, matching ExpressionLexer.unregisterVocabulary()'s
1407
+ * own contract). This engine-instance-local registration is reversed
1408
+ * here too, even though it isn't a "shared" registry, so a package's
1409
+ * lexer and highlighting contributions clean up together rather than
1410
+ * only half-reversing on unregister. Per-engine parselets/phrases are
1411
+ * still left in place: they live in this engine's isolated registries
1412
+ * and are discarded with the engine instance.
1413
+ *
1414
+ * Clears the bytecode cache, removing handlers changes what compiled
1415
+ * bytecode is valid.
1416
+ *
1417
+ * @param packageName - The `name` the package was registered under.
1418
+ * @returns true if the package was found and unregistered.
1419
+ */
1420
+ unregisterPackage(packageName: string): boolean;
1421
+ /**
1422
+ * Get the effective engine configuration currently in use.
1423
+ * Includes all defaults merged with any constructor overrides.
1424
+ * Useful for introspection, lets consumers see what values are actually
1425
+ * in effect after merging with DEFAULT_CONFIG.
1426
+ */
1427
+ getConfig(): EngineConfig;
1428
+ /**
1429
+ * Get the underlying diagnostic pipeline for advanced usage.
1430
+ */
1431
+ getDiagnosticPipeline(): DiagnosticPipeline;
1432
+ /**
1433
+ * Store a result in the line cache (with DAG registration).
1434
+ * Extracted common pattern from 8 call sites.
1435
+ */
1436
+ private storeLineResult;
1437
+ /**
1438
+ * Execute bytecode and handle the result.
1439
+ *
1440
+ * Replaces ALL 5 try/catch blocks that previously caught AsyncSuspenseError.
1441
+ * Now that executeBytecode returns an EvalResult discriminated union,
1442
+ * we simply check result.type instead of catching errors.
1443
+ *
1444
+ * Sets up AbortController → VM for stale-data prevention.
1445
+ * Cleans up the VM stack after execution (success or pending).
1446
+ * Fires async resolution via fire-and-forget for pending results.
1447
+ *
1448
+ * `tracePipeline`/`traceExpression`, when given, are passed straight
1449
+ * through to `executeBytecode()`'s own optional VM-step-tracing
1450
+ * parameters, used ONLY by {@link evaluateExpressionWithDiagnostic}
1451
+ * when `vmTraceEnabled` is on. Omitted by every other caller, with zero
1452
+ * behavior change (identical to today's hardcoded `undefined, undefined`).
1453
+ */
1454
+ private executeAndStore;
1455
+ /**
1456
+ * Execute bytecode and return the raw EvalResult without DAG/LineCache updates.
1457
+ * Used by reEvaluateLine, executeCached, and evaluateIncremental which
1458
+ * manage their own cache state differently.
1459
+ *
1460
+ * @param lineNumber - 1-based line this bytecode belongs to, for
1461
+ * cross-line features (`prev`/`line<N>`/aggregation. See
1462
+ * `makeLineContext()`). Defaults to -1 (the existing "no real
1463
+ * document" sentinel) for any caller that doesn't have a real line
1464
+ * number to pass.
1465
+ */
1466
+ private executeRaw;
1467
+ /**
1468
+ * Fire-and-forget async resolution using async/await.
1469
+ *
1470
+ * On resolution or error:
1471
+ * 1. Checks AbortSignal, if aborted, stale data is discarded.
1472
+ * 2. Stores result/error in AsyncResultCache (per-package scoped).
1473
+ * 3. Defers re-evaluation to AsyncResolutionBatcher which collapses
1474
+ * multiple resolutions into a single DAG walk + re-execution pass
1475
+ * and fires typed events to all listeners.
1476
+ */
1477
+ private resolveAsync;
1478
+ /**
1479
+ * Unified parsing method that handles different input types and returns comprehensive results
1480
+ * with precise coordinate mapping for inline solves.
1481
+ */
1482
+ parseDocument(input: string, options?: UnifiedParsingOptions): ParsingResult;
1483
+ /**
1484
+ * Batch-evaluate an array of lines in a single pass.
1485
+ *
1486
+ * Uses scanDocument() to classify and tokenize all lines in a single
1487
+ * character-by-character walk through the re-joined document text.
1488
+ * This eliminates the per-line Lexer.reset() + classifyLine() +
1489
+ * findInlineSolvesInLine() overhead from the old three-pass approach.
1490
+ *
1491
+ * Tokenization results from scanDocument() are passed directly to the
1492
+ * parser via evaluateLineWithPreTokenized(), skipping re-lexing.
1493
+ */
1494
+ /**
1495
+ * Batch-evaluate an array of lines in a single pass.
1496
+ *
1497
+ * Primarily used by tests. For production, prefer parseDocument()
1498
+ * which calls scanDocument() directly on the raw document string,
1499
+ * bypassing the split→join roundtrip that this method performs.
1500
+ */
1501
+ evaluateLines(lines: string[]): ParsedLine[];
1502
+ /**
1503
+ * Process pre-scanned line results into ParsedLine objects.
1504
+ *
1505
+ * Shared by parseDocument() (which scanDocuments the raw input) and
1506
+ * evaluateLines() (which scanDocuments joined line arrays). Handles
1507
+ * inline solve extraction, variable assignment detection, and
1508
+ * expression evaluation for each non-skipped line.
1509
+ */
1510
+ private processScanResults;
1511
+ /**
1512
+ * Evaluate a line using pre-tokenized tokens from scanDocument().
1513
+ *
1514
+ * Skips the lexing step entirely, the tokens are already available
1515
+ * from the document-level scan. Only parsing, compilation, and
1516
+ * execution are performed.
1517
+ *
1518
+ * This is the production fast path for evaluateLines(). Note: this
1519
+ * path intentionally bypasses the diagnostic pipeline. For diagnostic
1520
+ * events, use evaluateExpressionWithDiagnostic() directly.
1521
+ */
1522
+ private evaluateLineWithPreTokenized;
1523
+ /**
1524
+ * Route parse+compile to the active parser (PrecedenceParser or Recursive Descent).
1525
+ *
1526
+ * Sets up the builder on the active parser, loads tokens, and calls
1527
+ * parseExpression(). Abstracts the API difference between the two parsers:
1528
+ * - PrecedenceParser: parser.setBuilder(builder); parser.parseExpression(0)
1529
+ * - RD: parser.builder = builder; parser.parseExpression(0)
1530
+ */
1531
+ private parseExpression;
1532
+ /**
1533
+ * Compiles a standalone token list into a fresh, independent
1534
+ * `BytecodeProgram`, used for a `=>`-triggered expression and for a
1535
+ * bare equation's own right-hand side. Reuses {@link parseExpression}
1536
+ * (the SAME "compile a token list into a builder, then check for
1537
+ * leftover trailing tokens" logic every ordinary line already goes
1538
+ * through), just with a throwaway builder instead of the pooled one
1539
+ * (this grammar is rare, a per-call allocation here is a non-issue).
1540
+ */
1541
+ private compileAdHoc;
1542
+ /**
1543
+ * Executes an already-compiled program in symbolic-tolerant mode (an
1544
+ * undefined variable becomes a `Symbolic` placeholder instead of
1545
+ * throwing `UNDEFINED_VARIABLE`. See `vm/VM.ts`'s `executeBytecode()`
1546
+ * doc comment on its own `symbolicTolerant` parameter). Used by both
1547
+ * the "just simplify this" `=>` mode and bare equation evaluation
1548
+ * (a bare assignment's RHS, and an equation's own RHS at solve time)
1549
+ *, every one of these needs forward-tolerant reads of not-yet-defined
1550
+ * names, which ordinary evaluation deliberately never allows.
1551
+ */
1552
+ private executeSymbolicTolerant;
1553
+ /** Compiles and executes `tokens` in symbolic-tolerant mode, the "just simplify this" `=>` fallback when there's no stored equation to solve. */
1554
+ private simplifySymbolically;
1555
+ /**
1556
+ * Parses a bare (colon-less) equation's left-hand side, `factor1 *
1557
+ * factor2 * ... * variable`, into an ordered list of bare names, or
1558
+ * `null` if `tokens` isn't EXACTLY that shape (an alternating
1559
+ * `IDENT/UNIT`, `STAR`, `IDENT/UNIT`, `STAR`, ... sequence with no
1560
+ * other token types). Returning `null` means "don't intercept this
1561
+ * line at all", it falls through to whatever the ordinary expression
1562
+ * grammar already does with a bare `=` in it (today: a clear parse
1563
+ * error), so this pattern-match can only ever ADD a new capability,
1564
+ * never take one away.
1565
+ *
1566
+ * Requiring `IDENT`/`UNIT` at every name position is what keeps this
1567
+ * safe from the reserved-keyword collision risk `VariableParselet.ts`
1568
+ * guards against for the colon-prefixed form: a genuinely reserved
1569
+ * word (`clamp`, `global`, ...) always lexes as ITS OWN token type,
1570
+ * never `IDENT`/`UNIT`, so it can never satisfy this pattern, the
1571
+ * same protection, for free, without a duplicated keyword list.
1572
+ */
1573
+ private parseFactorChain;
1574
+ /**
1575
+ * Solves a stored equation, `variable = inv(factor1*factor2*...) *
1576
+ * rhs`. Every step is symbolic-aware (`vm/MatrixOps.ts`'s
1577
+ * `matrixMultiply()`/`inverse()`), so a factor whose OWN cells are
1578
+ * still-unassigned free variables (`s = [sx,0,0;...]`) solves
1579
+ * correctly, producing a Matrix whose cells are algebraic formulas
1580
+ * rather than plain numbers. Errors (a missing factor, a non-Matrix
1581
+ * factor/RHS, a singular combined matrix) are returned as `Error`-typed
1582
+ * Values, not thrown, matching this engine's established "matrix
1583
+ * errors propagate as values" convention (DIMENSION_MISMATCH,
1584
+ * SINGULAR_MATRIX, ...).
1585
+ */
1586
+ private solveEquation;
1587
+ /**
1588
+ * Detects and handles this session's two new symbolic-algebra grammar
1589
+ * shapes on `normalizedTokens`, returning the computed result directly
1590
+ * (bypassing ordinary bytecode compilation/caching entirely, since
1591
+ * BOTH shapes have effects, a stored equation, a direct variable
1592
+ * assignment, that a cached bytecode program can't represent) or
1593
+ * `null` if neither shape matches (meaning ordinary processing should
1594
+ * proceed exactly as it did before this feature existed):
1595
+ *
1596
+ * 1. `<bareIdent> =>` or `<expr> =>`, trailing `THEREFORE`. A bare
1597
+ * identifier with a STORED equation solves it (see
1598
+ * {@link solveEquation}); anything else (including a bare
1599
+ * identifier with NO stored equation) runs in symbolic-tolerant
1600
+ * mode and simplifies (see {@link simplifySymbolically}), a
1601
+ * near-free "just simplify this" mode.
1602
+ * 2. `factor1*factor2*...*variable = rhs`, a bare (colon-less), NOT
1603
+ * already colon/global-prefixed, top-level `EQUALS` whose LHS
1604
+ * matches {@link parseFactorChain}'s narrow pattern. A single bare
1605
+ * name (`s = [sx,0,0;...]`) is an ORDINARY (colon-less) assignment
1606
+ *, its RHS is ALWAYS evaluated symbolic-tolerantly (so a matrix
1607
+ * literal with still-unassigned entries assigns successfully,
1608
+ * carrying those free variables as real symbolic cells) and stored
1609
+ * via `vm.setVar()`. Two or more names (`s*t*v = rhs`) stores a
1610
+ * genuine equation keyed by the LAST name (`v`), solved later via
1611
+ * shape 1 above.
1612
+ *
1613
+ * This is a deliberately narrow pattern match, not a general equation
1614
+ * solver. See `OpRegistry.ts`'s `EquationDef` doc comment and this
1615
+ * session's own Phase H.2 scope decision (full symbolic MATRICES, not
1616
+ * a general CAS).
1617
+ */
1618
+ private trySymbolicGrammar;
1619
+ /**
1620
+ * Stores a general scalar equation (`x^2 - 4 = 0`) keyed by its unknown, or
1621
+ * returns `null` to let ordinary processing continue.
1622
+ *
1623
+ * This is the riskiest pattern match in the file, because a bare `=` is
1624
+ * already claimed by three shipped grammars, and swallowing any of them
1625
+ * would break a working feature silently. Each is excluded deliberately:
1626
+ *
1627
+ * - **A user-defined function definition** (`f(x) = 2*x`). Its left side is
1628
+ * a name followed by `(`, which no equation ever is, so a `LPAREN` in
1629
+ * second position declines outright. This is the exclusion that matters
1630
+ * most: `parseFactorChain` already returns `null` for it, so without this
1631
+ * guard the definition grammar would be intercepted before the parser
1632
+ * ever saw it.
1633
+ * - **A bare assignment** (`a = [1,2;3,4]`) and **a product-chain matrix
1634
+ * equation** (`a*x = [60;70]`). Both are handled by `parseFactorChain`
1635
+ * above and never reach here.
1636
+ * - **A colon-prefixed or global assignment**. Excluded earlier still.
1637
+ *
1638
+ * Two further conditions narrow it to genuine equations. There must be
1639
+ * exactly one top-level `=` (an `==` comparison lexes as `EQUALITY`, a
1640
+ * different token, so it cannot collide), and at least one side must carry
1641
+ * an unknown. Without that last check `2+2 = 4` would become an equation
1642
+ * with no variable and report an identity, where today it is a parse error;
1643
+ * changing an unrelated line's behaviour is not this feature's business.
1644
+ *
1645
+ * @param normalizedTokens - The whole line's normalized tokens.
1646
+ * @param eqIdx - Index of the first `EQUALS` token.
1647
+ * @returns A confirmation value when stored, or `null` to decline.
1648
+ */
1649
+ private tryStoreScalarEquation;
1650
+ /**
1651
+ * The names in an equation that have no value yet, which is what makes them
1652
+ * the thing to solve for.
1653
+ *
1654
+ * A name that already holds a value is not an unknown: in `y = 2` followed
1655
+ * by `x + y = 5`, only `x` is being solved for. A name that is a defined
1656
+ * function is likewise excluded, so a call to one never looks like an
1657
+ * unknown.
1658
+ *
1659
+ * @param lhsTokens - Left-hand side tokens.
1660
+ * @param rhsTokens - Right-hand side tokens.
1661
+ * @returns The distinct unassigned names, in first-seen order.
1662
+ */
1663
+ private equationUnknowns;
1664
+ /**
1665
+ * Solves a stored scalar equation via `symbolic/Solve.ts`.
1666
+ *
1667
+ * Both sides are evaluated symbolic-tolerantly first, so the unknown
1668
+ * survives as a `Symbolic` value rather than throwing, and a side that
1669
+ * reduces to a plain number is lifted into the symbolic domain to match.
1670
+ *
1671
+ * @param equation - The stored equation.
1672
+ * @returns The solution, rendered by the same helper `solve()` uses, so the
1673
+ * two surfaces cannot disagree about how an outcome reads.
1674
+ */
1675
+ private solveScalarEquation;
1676
+ /**
1677
+ * Lex an expression via `resetExpression` (skips `classifyLine`
1678
+ * callers already know this is an expression), filtering out COMMENT
1679
+ * tokens (they have no parselet) and tracking whether any paren was
1680
+ * seen. Shared by every lex-then-{@link prepareExpression} call site:
1681
+ * {@link compileExpression}, {@link tryCompileExpression}, and
1682
+ * {@link evaluateExpressionWithDiagnostic}. `onToken`, when given, is
1683
+ * called once per emitted (already-filtered) token, used ONLY by the
1684
+ * diagnostic path to fire its per-token `TokenEmitted` events; every
1685
+ * other caller omits it.
1686
+ */
1687
+ private lexToTokens;
1688
+ /**
1689
+ * Shared pipeline front-half: safety checks → COMMENT filter →
1690
+ * normalize → complexity → read/write extraction → bytecode cache
1691
+ * lookup or parse+compile.
1692
+ *
1693
+ * Used by {@link evaluateWithTokens} (which continues into preflight +
1694
+ * execution) and {@link compileExpression} (compile-only). Previously
1695
+ * both carried their own copy of this sequence, which had already
1696
+ * drifted. Returns a discriminated union instead of throwing so each
1697
+ * caller decides whether/how to re-throw.
1698
+ *
1699
+ * The `'error'` variant carries the actual `EngineError` each safety
1700
+ * check or `parseExpression()` itself already constructed, not just
1701
+ * its flattened `.message` (the previous shape). A parse failure in
1702
+ * particular can be any of dozens of specific codes (UNDEFINED_VARIABLE,
1703
+ * NO_PREFIX_PARSELET, FUNCTION_ARITY_MISMATCH, ...), each with its own
1704
+ * `expected`/`found`/`suggestion` detail, callers used to discard all
1705
+ * of that and reconstruct a generic EVALUATION_ERROR/PARSE_ERROR wrapper
1706
+ * around just the message, which directly worked against this session's
1707
+ * "errors are verbose and easy to understand" goal. Callers should
1708
+ * generally just `throw prep.error` (see {@link evaluateWithTokens},
1709
+ * {@link compileExpression}) rather than wrapping it again.
1710
+ *
1711
+ * `onFusion`, when given, is passed straight through to the normalizer's
1712
+ * own optional per-fusion callback, used ONLY by
1713
+ * {@link evaluateExpressionWithDiagnostic} to observe individual token
1714
+ * fusions for its `normalizer` diagnostic stage. Omitted by every other
1715
+ * caller, with zero behavior change (identical to not passing a 2nd
1716
+ * argument to `normalizer.normalize()` at all).
1717
+ *
1718
+ * The `'error'` variant's `normalizedTokens` is populated whenever
1719
+ * normalization already ran before the failure (`'complexity'`/`'parse'`
1720
+ * stages), `undefined` when it failed before tokens were even considered
1721
+ * (`'length'` stage), lets {@link evaluateExpressionWithDiagnostic}
1722
+ * reconstruct its own diagnostic-stage payloads (which need the
1723
+ * normalized tokens to recompute a display-only complexity score) without
1724
+ * re-deriving them by hand.
1725
+ *
1726
+ * The `'ready'` (uncached) variant's `parserAlloc` is the
1727
+ * `AllocationTracker.track('parser', ...)` result for JUST the actual
1728
+ * parse+build call (`null` whenever `AllocationTracker.isEnabled()` is
1729
+ * false, or on a cache hit, no parsing happened). This has to be
1730
+ * measured HERE, not reconstructed by a caller after the fact: unlike
1731
+ * every other diagnostic field, a heap-delta measurement only means
1732
+ * anything over the EXACT span it's wrapped around, wrapping the
1733
+ * whole `prepareExpression()` call from the outside (as
1734
+ * {@link evaluateExpressionWithDiagnostic} briefly did) widens that span
1735
+ * to also cover normalize/complexity-check/cache-lookup, which measurably
1736
+ * produced negative-allocation readings (a GC sweep landing inside the
1737
+ * wider window) in `AllocationTracker.spec.ts` well over half the time.
1738
+ */
1739
+ private prepareExpression;
1740
+ /**
1741
+ * Shared async-resolver preflight check, run before VM execution.
1742
+ *
1743
+ * O(1) guard: skips the O(n) resolver scan when the bytecode has no
1744
+ * async opcodes AND no resolvers are registered. Either condition alone
1745
+ * is enough to warrant a preflight scan:
1746
+ * - program.hasAsync: bytecode contains CALL_PLUGIN (async VM path)
1747
+ * - resolverRegistry.size > 0: resolvers may intercept any expression
1748
+ * For purely sync expressions (e.g., `2 + 2`) with no resolvers, this is
1749
+ * an O(1) fast-path that bypasses the resolver scan entirely.
1750
+ *
1751
+ * If any registered resolver says "data not ready", registers a DAG
1752
+ * data-source dependency, stores a Pending result, and fires async
1753
+ * resolution (fire-and-forget, resolves later, re-evaluates on
1754
+ * completion), the caller should skip VM execution entirely and return
1755
+ * the pending Value as-is. Otherwise the caller should proceed to
1756
+ * {@link executeAndStore}.
1757
+ *
1758
+ * Used by both {@link evaluateWithTokens} and
1759
+ * {@link evaluateExpressionWithDiagnostic}, previously each carried its
1760
+ * own copy of this exact sequence (differing only in a cosmetic
1761
+ * abortLogger label), which is how the `=>` grammar shipped silently
1762
+ * dead on the diagnostic path earlier this session: a future top-level
1763
+ * grammar addition can no longer be wired into only one of the two.
1764
+ */
1765
+ private preflightAsync;
1766
+ /**
1767
+ * Evaluate an expression using already-lexed tokens.
1768
+ *
1769
+ * The lean, non-diagnostic-instrumented path, used by
1770
+ * {@link evaluateLineWithPreTokenized} (tokens from `scanDocument()`)
1771
+ * and, via {@link compileExpression}/{@link tryCompileExpression},
1772
+ * compile-only callers. NOT used by `evaluateLine()`/
1773
+ * `evaluateExpression()`, those route through
1774
+ * {@link evaluateExpressionWithDiagnostic} instead (evaluateLine ->
1775
+ * evaluateLineDetailed -> evaluateLineWithDebug ->
1776
+ * evaluateExpressionWithDiagnostic), which does its own lexing and its
1777
+ * own diagnostic-instrumented front-half, delegating to the SAME
1778
+ * {@link prepareExpression} this method calls. Delegates the front-half
1779
+ * to {@link prepareExpression}, then runs async preflight (via
1780
+ * {@link preflightAsync}) + VM execution (via {@link executeAndStore}).
1781
+ */
1782
+ private evaluateWithTokens;
1783
+ /**
1784
+ * Evaluate a single expression line with full DAG and LineCache integration.
1785
+ *
1786
+ * @param lineNumber - 1-based line position in the document.
1787
+ * @param lineText - The raw line text.
1788
+ * @returns The evaluated Value, wrapped in a single-element array.
1789
+ * @throws {EngineError} On evaluation failure.
1790
+ */
1791
+ evaluateLine(lineNumber: number, lineText: string): EvalResults;
1792
+ /**
1793
+ * Evaluate a line and return an explicit `{ values }` object.
1794
+ *
1795
+ * @throws {EngineError} On evaluation failure.
1796
+ */
1797
+ evaluateLineDetailed(lineNumber: number, lineText: string): LineEvaluation;
1798
+ /**
1799
+ * Evaluate a line with diagnostic information, supporting both regular expressions and inline solves.
1800
+ *
1801
+ * This is the primary entry point for the playground's debug/DIagnostic mode.
1802
+ * It delegates to {@link evaluateExpressionWithDiagnostic} for all actual evaluation,
1803
+ * but first checks for inline solve syntax (`s`expression``) and wraps the result
1804
+ * with inline solve position metadata when found.
1805
+ *
1806
+ * @param lineNumber - 1-based line number in the document.
1807
+ * @param lineText - Raw line text, which may be a regular expression or an inline solve.
1808
+ * @param inputType - Optional input type hint passed through to the diagnostic pipeline.
1809
+ * @returns An object containing the evaluated `value`, the raw `tokens`, the compiled
1810
+ * `program`, optional `error` message, optional `debug` report JSON, and optional
1811
+ * structured `diagnostic` pipeline result with all 15 pipeline stages.
1812
+ */
1813
+ evaluateLineWithDebug(lineNumber: number, lineText: string, inputType?: string): {
1814
+ value: Value;
1815
+ tokens: Token[];
1816
+ program: BytecodeProgram;
1817
+ error?: string;
1818
+ engineError?: EngineError;
1819
+ inlineSolve?: InlineSolvePosition;
1820
+ debug?: DiagnosticReportJSON;
1821
+ diagnostic?: DiagnosticPipelineResult;
1822
+ };
1823
+ /**
1824
+ * Build a single pipeline stage result for the structured diagnostic output.
1825
+ *
1826
+ * Appends a `PipelineStageResult` to the given `stages` array with the provided
1827
+ * metadata. This runs in parallel with the existing event-based diagnostic system
1828
+ * (via `DiagnosticPipeline.fire*` methods). Both paths are enabled by the same
1829
+ * `hasCollectors` guard so there is no performance impact when `diagnosticMode`
1830
+ * is `false`, the stages array stays empty because this method is never called.
1831
+ *
1832
+ * Each stage captures:
1833
+ * - **Identity**: `stage` name (e.g., `"lexer"`), display `label`, `icon`, `colorClass`
1834
+ * - **Position**: `stepNumber` in the pipeline (0-15)
1835
+ * - **Timing**: `elapsedNs` wall-time (overridden by TimelineDiagnosticCollector)
1836
+ * - **State**: `skipped` flag for stages bypassed by cache hits or guard conditions
1837
+ * - **Payload**: `output`, a discriminated union typed per stage
1838
+ *
1839
+ * @param stages - Mutable array being accumulated for the final DiagnosticPipelineResult.
1840
+ * @param stage - Canonical stage identifier (kebab-case, e.g. `"async_preflight"`).
1841
+ * @param label - Human-readable stage name for the dashboard.
1842
+ * @param icon - Single emoji/character icon for visual identification.
1843
+ * @param colorClass - CSS class name for color-coding the stage in the UI.
1844
+ * @param stepNumber - Ordinal position in the 15-stage pipeline.
1845
+ * @param elapsedNs - Wall-clock time in nanoseconds (0 placeholder; timeline overrides).
1846
+ * @param skipped - Whether this stage was bypassed (e.g., cache hit, guard short-circuit).
1847
+ * @param output - Stage-specific data payload typed via the StageOutput discriminated union.
1848
+ */
1849
+ private addDiagnosticStage;
1850
+ /**
1851
+ * Core expression evaluation logic with diagnostic pipeline integration.
1852
+ *
1853
+ * Executes the full 15-stage evaluation pipeline while simultaneously
1854
+ * populating two diagnostic data structures:
1855
+ *
1856
+ * 1. **Event-based**, fires typed events to registered `DiagnosticCollector`
1857
+ * instances via `DiagnosticPipeline.fire*()` methods. Supports streaming
1858
+ * diagnostics via `TimelineDiagnosticCollector`.
1859
+ * 2. **Structured stages**, accumulates a `PipelineStageResult[]` array
1860
+ * with per-stage typed payloads (see `DiagnosticPipelineResult.ts`).
1861
+ * This is returned as the `diagnostic` field for declarative rendering.
1862
+ *
1863
+ * The 15 stages, in order:
1864
+ * ```
1865
+ * 1 pipeline_start, Pipeline initialization + metadata
1866
+ * 2 safety_length, Expression length validation
1867
+ * 3 lexer, Tokenization via ExpressionLexer
1868
+ * 4 normalizer, Token fusion (phrase, implicit multiply)
1869
+ * 5 safety_complexity, Token-count & nesting-depth check
1870
+ * 6 readwrite, Variable read/write extraction for DAG
1871
+ * 7 cache_check, Bytecode cache hit/miss
1872
+ * 8 parser, AST construction via PrecedenceParser
1873
+ * 9 compiler, Bytecode generation + constant table
1874
+ * 10 async_preflight, Async resolver pre-flight check
1875
+ * 11 vm_execute, Bytecode execution on the VM
1876
+ * 12 dag_registration, DAG node registration for incremental eval
1877
+ * 13 linecache, Result stored in LineCache
1878
+ * 14 result, Final value + formatting
1879
+ * 15 pipeline_end, Completion summary + statistics
1880
+ * ```
1881
+ *
1882
+ * Early-exit paths are taken for safety violations, empty expressions,
1883
+ * parse failures, and async pending results. Each early exit still
1884
+ * fires relevant pipeline events and records partial stages.
1885
+ *
1886
+ * When `AllocationTracker.isEnabled()`, each pipeline stage is wrapped
1887
+ * with `AllocationTracker.track()` to capture wall-time and heap delta.
1888
+ * When disabled (production), `track()` is a zero-overhead passthrough
1889
+ * that returns the result directly.
1890
+ *
1891
+ * The stages above describe OBSERVABLE shape, not a second implementation
1892
+ * of the underlying work: normalize/complexity-check/symbolic-grammar/
1893
+ * readwrite/cache-lookup/parse/compile are delegated to
1894
+ * {@link prepareExpression} (the SAME method the lean
1895
+ * {@link evaluateWithTokens} path calls), async preflight to
1896
+ * {@link preflightAsync}, and VM execution to {@link executeAndStore}
1897
+ * every stage/event below is reconstructed from what those shared
1898
+ * methods return, not fired from inside a second hand-duplicated copy
1899
+ * of their logic. This matters: a new top-level grammar shape wired
1900
+ * into `prepareExpression()` is automatically reachable from BOTH
1901
+ * `evaluateLine()`/`evaluateExpression()` (this method) and the lean
1902
+ * path, previously they were two independent implementations that had
1903
+ * already drifted once (the `=>`/equation-statement grammar shipped
1904
+ * dead on this, the real path, until a dedicated test caught it).
1905
+ *
1906
+ * @param expression - The raw expression string to evaluate.
1907
+ * @param lineNumber - 1-based line number for DAG and LineCache entries.
1908
+ * @param inputType - Input type hint (default `"expression"`), passed to
1909
+ * the diagnostic pipeline for metadata.
1910
+ * @returns An object with `value`, `tokens`, `program`, optional `error`,
1911
+ * optional `debug` report JSON, and optional `diagnostic` containing
1912
+ * the full structured pipeline stages array when collectors are active.
1913
+ */
1914
+ private evaluateExpressionWithDiagnostic;
1915
+ /**
1916
+ * Build a complete DiagnosticPipelineResult with engine-wide snapshot data.
1917
+ *
1918
+ * Populates dagSnapshot, cacheSnapshot, batcherMetrics, and checkpoints
1919
+ * alongside the per-line pipeline stages, value, tokens, and program.
1920
+ * Previously the playground made separate engine method calls for each.
1921
+ */
1922
+ private buildDiagnosticResult;
1923
+ /**
1924
+ * Re-evaluate a cached line without reparsing.
1925
+ *
1926
+ * Used when a variable referenced by this line has changed. Skips
1927
+ * lexing, parsing, and compilation, performs only a pre-flight async
1928
+ * check and VM execution against the cached bytecode.
1929
+ *
1930
+ * Returns `undefined` if the line is not in cache.
1931
+ *
1932
+ * @param lineNumber - The line to re-evaluate.
1933
+ * @param expression - The original expression string (used for cache lookup).
1934
+ * @returns The updated `Value`, or `undefined` if uncached.
1935
+ */
1936
+ reEvaluateLine(lineNumber: number, expression: string): Value | undefined;
1937
+ getDag(): DependencyGraph;
1938
+ getLineCache(): LineCache;
1939
+ getBytecodeCache(): Map<string, BytecodeProgram>;
1940
+ /**
1941
+ * Get the shared VM instance.
1942
+ * Used by VMCheckpointer to create/restore checkpoints.
1943
+ */
1944
+ getVM(): VM;
1945
+ /**
1946
+ * This engine's registries.
1947
+ *
1948
+ * For introspection: checking what a package registered, or asserting that
1949
+ * unregistering removed it. These used to be module-level singletons a
1950
+ * caller could import directly, which is exactly the coupling
1951
+ * {@link EngineContext} removes, so reaching them now goes through the
1952
+ * engine that owns them.
1953
+ *
1954
+ * @returns The context created for this engine. Mutating what it holds
1955
+ * affects this engine's behaviour, so treat it as read-only unless you are
1956
+ * deliberately registering something.
1957
+ */
1958
+ getContext(): EngineContext;
1959
+ getScopeManager(): ScopeManager;
1960
+ getLexer(): Lexer;
1961
+ /**
1962
+ * Every completion candidate contributed by currently-registered
1963
+ * packages (`IEnginePackage.completionItems`), flattened across all of
1964
+ * them. Used by `LanguageService.getCompletions()`.
1965
+ */
1966
+ getPackageCompletionItems(): CompletionItem[];
1967
+ getParseletRegistry(): {
1968
+ prefix: Array<{
1969
+ tokenType: string;
1970
+ bindingPower: number;
1971
+ category?: string;
1972
+ }>;
1973
+ infix: Array<{
1974
+ tokenType: string;
1975
+ leftBindingPower: number;
1976
+ rightBindingPower: number;
1977
+ category?: string;
1978
+ }>;
1979
+ };
1980
+ /**
1981
+ * Lex + normalize (phrase fusion, implicit multiply, domain token
1982
+ * merging) `text` WITHOUT parsing or executing it, a cheap way for a
1983
+ * host to inspect what token stream a line would actually produce,
1984
+ * without paying for a full parse/compile/VM pass.
1985
+ *
1986
+ * Built for line-classification heuristics like "does this look like a
1987
+ * real expression, or is it prose I shouldn't bother evaluating", a
1988
+ * host that only checks for digits/operators/symbols before deciding
1989
+ * whether to evaluate a line will incorrectly skip genuine all-word
1990
+ * expressions (`weather in Tokyo`, `time in Paris`, `average of X, Y,
1991
+ * Z`), since none of those contain a digit or symbol. Checking whether
1992
+ * `tokenizeForClassification(text)[0]?.type` is anything OTHER than the
1993
+ * generic `IDENT` fallback is a reliable signal that the lexer/normalizer
1994
+ * actually recognized a specific keyword or fused multi-word phrase
1995
+ * i.e., this is real, registered vocabulary, not an arbitrary word that
1996
+ * merely happens to be lexable (every word lexes as IDENT if nothing
1997
+ * more specific claims it, so IDENT alone proves nothing about intent).
1998
+ *
1999
+ * Assumes the caller has already ruled out markdown-structural lines
2000
+ * (headings, code fences, etc.) via `getLexer().classifyLine()`. This
2001
+ * always tokenizes as a plain expression line, mirroring
2002
+ * `Lexer.resetExpression()`'s own "caller already knows this is
2003
+ * evaluable" contract.
2004
+ */
2005
+ tokenizeForClassification(text: string): Token[];
2006
+ getParser(): PrecedenceParser;
2007
+ isDiagnosticMode(): boolean;
2008
+ /**
2009
+ * Set the keystroke-level AbortSignal for the current evaluation cycle.
2010
+ *
2011
+ * Called by the UI layer (via ThreeTierEvaluator) before evaluate() or
2012
+ * evaluateAll(). All per-evaluation AbortControllers created during this
2013
+ * cycle link to this signal so that when the user types a new keystroke,
2014
+ * all in-flight async work is canceled atomically.
2015
+ *
2016
+ * @param signal The keystroke's AbortSignal, or null to clear.
2017
+ */
2018
+ setKeystrokeSignal(signal: AbortSignal | null): void;
2019
+ /**
2020
+ * Get a serializable cache snapshot for diagnostic rendering.
2021
+ *
2022
+ * Returns bytecode cache entries, line cache entries, and async cache
2023
+ * packages, all as plain objects with no internal references. Previously
2024
+ * the playground accessed this via `(engine as any).getCacheSnapshot?.()`.
2025
+ */
2026
+ getCacheSnapshot(): CacheSnapshot;
2027
+ /**
2028
+ * Get serializable batcher metrics for the Workers diagnostic tab.
2029
+ *
2030
+ * Reads the typed read-only accessors on {@link AsyncResolutionBatcher}
2031
+ * (`pendingCount`/`dedupCount`/`workerOffloadCount`/`listenerCount`)
2032
+ * instead of reaching into its private fields via `(this.batcher as any)`.
2033
+ */
2034
+ getBatcherMetrics(): BatcherMetrics;
2035
+ /**
2036
+ * Get a serializable snapshot of VM checkpoints for diagnostics.
2037
+ *
2038
+ * The checkpointer lives on the ThreeTierEvaluator (not the engine),
2039
+ * so this returns an empty array when no checkpointer is available.
2040
+ * Previously accessed via `(vm as any).checkpointer.getAllCheckpoints?.()`.
2041
+ */
2042
+ getCheckpoints(): CheckpointSnapshot[];
2043
+ /**
2044
+ * Get the most recent pipeline telemetry from AllocationTracker.
2045
+ *
2046
+ * Returns null when AllocationTracker.isEnabled() is false (production
2047
+ * zero overhead), or when no expression has been evaluated via
2048
+ * evaluateExpressionWithDiagnostic() since the last clear().
2049
+ *
2050
+ * Use this in test/benchmark suites to inspect per-stage wall-time and
2051
+ * heap allocation data without enabling the full diagnostic pipeline.
2052
+ */
2053
+ getLastTelemetry(): PipelineTelemetry | null;
2054
+ /**
2055
+ * Evaluate a raw expression string without line-number context.
2056
+ * Returns the Value result. Throws on error.
2057
+ */
2058
+ evaluateExpression(expression: string): EvalResults;
2059
+ /**
2060
+ * Compile-only path: lex → parse → bytecode, without execution.
2061
+ *
2062
+ * Used by Tier 3 (background) evaluation to discover reads/writes
2063
+ * for the dependency graph without running display-only expressions.
2064
+ *
2065
+ * Uses the bytecode cache, repeated compilations of the same expression
2066
+ * return the cached program with zero allocation.
2067
+ *
2068
+ * @param expression - The raw expression string to compile.
2069
+ * @returns Object with compiled `program`, lexed `tokens`, and extracted `reads`/`writes`.
2070
+ * @throws ErrorFactory on parse failure or safety check failure.
2071
+ */
2072
+ compileExpression(expression: string): {
2073
+ program: BytecodeProgram;
2074
+ tokens: Token[];
2075
+ reads: string[];
2076
+ writes: string[];
2077
+ };
2078
+ /**
2079
+ * Non-throwing "does this compile" check. Same lex → prepare pipeline as
2080
+ * {@link compileExpression}, but returns a boolean instead of throwing on
2081
+ * failure.
2082
+ *
2083
+ * compileExpression()'s failure path constructs a EngineError via
2084
+ * ErrorFactory, which calls Error.captureStackTrace(), one of V8's more
2085
+ * expensive operations. That's fine for genuine execution/compile errors
2086
+ * (rare, and the caller needs the message), but LanguageService's
2087
+ * syntax-highlighting gate calls compileExpression() purely to ask "does
2088
+ * this parse", on every visible line, every keystroke, and the common
2089
+ * case for a real markdown document is prose lines that DON'T parse, not
2090
+ * the rare case. Benchmarked: constructing-and-throwing that exception on
2091
+ * every non-matching line was responsible for highlighting an
2092
+ * unrecognized-prose line costing roughly an order of magnitude more than
2093
+ * a recognized expression. This skips that construction entirely, still
2094
+ * reuses the bytecode cache and prepareExpression()'s normal work, just
2095
+ * never builds an Error object for the "no" answer.
2096
+ *
2097
+ * Note: this only avoids the outer exception compileExpression() itself
2098
+ * would construct. A genuinely deep parse failure (an unmatched token
2099
+ * mid-expression, not just "stopped early with leftover tokens") still
2100
+ * goes through the parser's own throw/catch inside prepareExpression()
2101
+ * unavoidable without restructuring the parser's failure signaling, which
2102
+ * is out of scope here.
2103
+ */
2104
+ tryCompileExpression(expression: string): boolean;
2105
+ /**
2106
+ * Execute pre-compiled bytecode against the engine's shared VM.
2107
+ * Used by Tier 2 (scroll into view) to re-execute cached bytecode
2108
+ * without re-lexing, re-parsing, or re-compiling.
2109
+ *
2110
+ * Preserves the VM stack, pops any leftover items after execution.
2111
+ * Does NOT update DAG or LineCache (caller is responsible for state
2112
+ * management via DocumentModel).
2113
+ *
2114
+ * @returns The execution result, or undefined if bytecode is empty.
2115
+ * @param lineNumber - 1-based line this bytecode belongs to, for
2116
+ * cross-line features (see `makeLineContext()`). Defaults to -1.
2117
+ */
2118
+ executeCached(program: BytecodeProgram, lineNumber?: number): Value;
2119
+ /**
2120
+ * Fast path: evaluate an expression and return a number directly.
2121
+ *
2122
+ * Skips Value object allocation when only a numeric result is needed.
2123
+ * Returns NaN on error or for bare undefined variable references.
2124
+ *
2125
+ * Performs a pre-check for bare identifiers (single-token variable
2126
+ * references). If the identifier is not a known variable, returns NaN
2127
+ * immediately without attempting evaluation, avoids the ambiguity of
2128
+ * "result === 0" when a variable might legitimately store the value 0.
2129
+ *
2130
+ * @param expression - The raw expression string to evaluate.
2131
+ * @returns The numeric result, or NaN on error/undefined variable.
2132
+ */
2133
+ evaluateNumber(expression: string): number;
2134
+ clear(): void;
2135
+ /**
2136
+ * Incrementally re-evaluate lines affected by a variable change.
2137
+ * Walks the DAG from the changed variable to find exactly which lines
2138
+ * need re-execution, no dirty-set indirection, no sorting guesswork.
2139
+ * Uses Kahn's algorithm for topological ordering: producers always
2140
+ * execute before consumers, regardless of document line order.
2141
+ */
2142
+ evaluateIncremental(variable: string, newValue: number): Map<number, Value>;
2143
+ }
2144
+
2145
+ /** A single classified span within a line, the entire output contract of the language service. */
2146
+ interface SemanticToken {
2147
+ from: number;
2148
+ to: number;
2149
+ category: TokenCategory;
2150
+ }
2151
+ /** A single completion candidate, the entire output contract of `getCompletions()`. */
2152
+ interface CompletionItem {
2153
+ label: string;
2154
+ /** Reuses the highlighting taxonomy, one adapter can serve both features. */
2155
+ category: TokenCategory;
2156
+ /** e.g. a unit's measure ("length"), or the category name for keywords/functions. */
2157
+ detail?: string;
2158
+ }
2159
+ /** Options for the editor-facing language service: completions and highlighting. */
2160
+ interface LanguageServiceOptions {
2161
+ /**
2162
+ * Overrides how the service discovers "variable names known in this
2163
+ * document", used to legitimize a lone bare identifier line (see
2164
+ * `getSemanticTokens`'s single-token gate) and variable-name
2165
+ * completions (`getCompletions`). Defaults to reading
2166
+ * `engine.getDag().getSnapshot()`, which works for any consumer
2167
+ * sharing one `ExpressionEngine` between evaluation and the language
2168
+ * service (the real Obsidian editor).
2169
+ *
2170
+ * Required for consumers whose language service is backed by a
2171
+ * *different*, non-evaluating engine than the one that actually runs
2172
+ * the document (the playground's dedicated lexing-only engine, whose
2173
+ * own DAG is always empty), pass a function reading the real
2174
+ * evaluation engine's DAG snapshot instead.
2175
+ */
2176
+ variableNameSource?: () => Iterable<string>;
2177
+ }
2178
+ /**
2179
+ * Editor-agnostic "language server" for solve expressions: turns a line of
2180
+ * text into semantic token ranges, using the exact same lexer real
2181
+ * evaluation uses (so it only ever classifies what the engine's grammar
2182
+ * actually recognizes, never a separate/duplicated tokenizer). No
2183
+ * knowledge of CSS, CodeMirror, VS Code, or any other rendering concept
2184
+ * lives here. See `language/adapters/` for that.
2185
+ *
2186
+ * Classification happens at the LEXER stage, before the normalizer runs
2187
+ * (normalization, phrase fusion, implicit multiply, and package-specific
2188
+ * rules, happens later, only on the real evaluation path). A package's
2189
+ * lexer-level custom token types (e.g. a custom keyword) are recognized
2190
+ * here exactly as evaluation would see them. A package's *normalizer*-fused
2191
+ * synthetic tokens (e.g. OSRS's GAME_ITEM, built by fusing several
2192
+ * consecutive IDENT tokens against an item-name trie) are NOT. This
2193
+ * service still shows the pre-fusion IDENT tokens individually for those.
2194
+ * `IEnginePackage.tokenCategories` entries for normalizer-only token types
2195
+ * are still valid, correct registrations (queryable via getTokenCategory)
2196
+ *, they just won't currently be reachable through this lexer-only
2197
+ * classification path. Folding normalization in would require running it
2198
+ * per keystroke on the highlighting path too, which needs its own careful
2199
+ * design (span recomputation for fused multi-token ranges, in particular)
2200
+ * rather than a quick addition here.
2201
+ *
2202
+ * Lexing alone is NOT sufficient to decide "recognized", though: a run of
2203
+ * plain-English words ("My name is ron") lexes into a sequence of
2204
+ * individually-valid IDENT tokens with no grammar tying them together
2205
+ * every word "recognized" at the token level, but the line as a whole is
2206
+ * not something the engine would ever accept as an expression. Surfacing
2207
+ * per-token colors for that case looks like the editor mistook prose for
2208
+ * code. So a line's tokens are only surfaced once the line as a whole
2209
+ * parses successfully (via `ExpressionEngine.compileExpression`, the same
2210
+ * parse pipeline, and the same bytecode cache, real evaluation uses; no
2211
+ * separate/duplicated grammar check). A single bare word ("hello", a valid
2212
+ * variable reference) or a keyword-only line ("pi") still parses and still
2213
+ * highlights, only genuinely ungrammatical text is suppressed, unless it's
2214
+ * a known variable elsewhere in the document (see `variableNameSource`).
2215
+ *
2216
+ * `getCompletions()` is the other half of this "language server": unlike
2217
+ * `getSemanticTokens()`, it's explicitly for *incomplete*, mid-typing text
2218
+ *, it deliberately does NOT gate on parse validity (a half-typed
2219
+ * expression almost never parses), using simple prefix matching instead.
2220
+ *
2221
+ * Must be constructed with an already-configured `ExpressionEngine` (one
2222
+ * with all currently-relevant packages registered) rather than a bare
2223
+ * lexer, reusing an existing engine is both the fast path (no throwaway
2224
+ * lexer construction) and the *correct* one: a highlighting-only lexer
2225
+ * built independently of the evaluation engine would silently fail to
2226
+ * recognize plugin-contributed tokens (e.g. a package's custom keywords)
2227
+ * unless it happened to have the identical packages registered.
2228
+ */
2229
+ declare class LanguageService {
2230
+ private engine;
2231
+ private variableNameSource;
2232
+ private cache;
2233
+ private staticCompletionCandidates;
2234
+ private staticCompletionIndex;
2235
+ constructor(engine?: ExpressionEngine | null, options?: LanguageServiceOptions);
2236
+ private defaultVariableNames;
2237
+ /**
2238
+ * Classify every recognized token on one line.
2239
+ *
2240
+ * @param lineText - The raw line text (may be a markdown-structural line
2241
+ * the engine's classifier skips, that's handled by the underlying
2242
+ * lexer, which returns no tokens for those).
2243
+ * @param lineNumber - 1-based line number, used purely as a cache key.
2244
+ */
2245
+ getSemanticTokens(lineText: string, lineNumber: number): SemanticToken[];
2246
+ /**
2247
+ * Completion candidates for the identifier prefix immediately before
2248
+ * `cursorOffset` on `lineText`. Deliberately simple prefix matching, not
2249
+ * parser-driven "what's grammatically valid here" prediction, a
2250
+ * half-typed expression almost never parses, so gating on parse
2251
+ * validity (the way `getSemanticTokens` does) would suppress
2252
+ * completions almost always. This is the safest, fastest option that
2253
+ * still delivers real value.
2254
+ *
2255
+ * Candidates come from three sources: keywords (which already include
2256
+ * function names. See `ExpressionLexer.getKeywords()`'s doc comment)
2257
+ * and units, both static per engine configuration and cached lazily;
2258
+ * package-contributed items (`IEnginePackage.completionItems`), same
2259
+ * cache; and variable names, read fresh from `variableNameSource()` on
2260
+ * every call since those change on every edit.
2261
+ */
2262
+ getCompletions(lineText: string, cursorOffset: number): CompletionItem[];
2263
+ /** Lazily builds and caches the keyword/unit/package-item candidate list. See `staticCompletionCandidates`. */
2264
+ private getStaticCompletionCandidates;
2265
+ /**
2266
+ * The static candidates bucketed by lowercased first character, built once
2267
+ * from {@link getStaticCompletionCandidates} and invalidated alongside it.
2268
+ */
2269
+ private getStaticCompletionIndex;
2270
+ private isKnownVariable;
2271
+ /**
2272
+ * Whether the engine's parser actually accepts a piece of text as a
2273
+ * well-formed expression, not merely whether it lexes into individually
2274
+ * recognized token types. See the class doc comment's prose example.
2275
+ * `tryCompileExpression` is compile-only (lex → normalize → parse →
2276
+ * cache bytecode, no VM execution, no network/async side effects) and
2277
+ * reuses the engine's existing bytecode cache, so text that's already
2278
+ * been evaluated (or previously highlight-checked) is a cache hit here
2279
+ * too.
2280
+ *
2281
+ * Deliberately calls the non-throwing `tryCompileExpression` rather than
2282
+ * try/catching `compileExpression`. This runs on every visible line on
2283
+ * every keystroke, and the common case for a real markdown document is
2284
+ * lines that DON'T parse (prose), not lines that do. Throwing there would
2285
+ * mean constructing a EngineError (with V8 stack-trace capture) for the
2286
+ * common case instead of the rare one.
2287
+ */
2288
+ private parsesAsExpression;
2289
+ private putCache;
2290
+ /**
2291
+ * Evict specific lines (e.g. the lines actually touched by a CodeMirror
2292
+ * change set) instead of the whole cache, the surgical counterpart to
2293
+ * {@link invalidateCache}, letting a single-line edit stay cheap even in
2294
+ * a large document: every other cached line is untouched and still hits
2295
+ * on the next call.
2296
+ */
2297
+ invalidateLines(lineNumbers: Iterable<number>): void;
2298
+ /**
2299
+ * Full cache clear. Reserved for cases with no meaningful "which lines
2300
+ * changed" (e.g. the document was swapped wholesale, or a package was
2301
+ * registered/unregistered mid-session, changing what categories exist).
2302
+ * Prefer {@link invalidateLines} for ordinary edits. Also rebuilds the
2303
+ * lazily-cached keyword/unit/package-item completion candidates on next
2304
+ * use, the only thing that can change that list mid-session.
2305
+ */
2306
+ invalidateCache(): void;
2307
+ }
2308
+
2309
+ /**
2310
+ * Public API for registering plugins with the solve-js engine.
2311
+ *
2312
+ * All registration goes through this interface, parselets, variable
2313
+ * sources, and full packages. The default implementation is
2314
+ * {@link PackageRegistry} (singleton via {@link packageRegistry}).
2315
+ *
2316
+ * @example
2317
+ * ```typescript
2318
+ * import { packageRegistry } from "solve-engine";
2319
+ * packageRegistry.registerPackage(myCustomPackage);
2320
+ * ```
2321
+ */
2322
+ interface IPackageRegistry {
2323
+ /** Register a prefix parselet (e.g., `GE`, `NOW`, `floor`). */
2324
+ registerPrefixParselet(tokenType: string, parselet: PrefixParselet): void;
2325
+ /** Register an infix parselet (e.g., `+`, `in`, `to`). */
2326
+ registerInfixParselet(tokenType: string, parselet: InfixParselet): void;
2327
+ /** @deprecated Has no effect. See {@link IEnginePackage.variableSources}. */
2328
+ registerVariableSource(source: IVariableSource): void;
2329
+ /** Register a complete package (parselets + variable sources). */
2330
+ registerPackage(pkg: IEnginePackage): void;
2331
+ /** Convenience reference to the Value class for creating typed values. */
2332
+ Value: typeof Value;
2333
+ }
2334
+ /**
2335
+ * Package descriptor for registering a complete provider with the engine.
2336
+ *
2337
+ * A package bundles all the pieces needed for a domain-specific provider:
2338
+ * lexer plugins for custom token recognition, parselets for Pratt parsing,
2339
+ * plugin functions dispatched via CALL_PLUGIN bytecode, variable sources,
2340
+ * and optional async resolvers for data that loads asynchronously (e.g.,
2341
+ * exchange rates, game prices).
2342
+ *
2343
+ * @example
2344
+ * ```typescript
2345
+ * const myPackage: IEnginePackage = {
2346
+ * name: "MyProvider",
2347
+ * lexerVocabulary: myLexerVocabulary,
2348
+ * prefixParselets: [{ tokenType: "MY_FUNC", parselet: new MyParselet() }],
2349
+ * pluginFunctions: [{ index: MY_FN_IDX, handler: myHandler }],
2350
+ * asyncResolvers: [myAsyncResolver],
2351
+ * };
2352
+ * packageRegistry.registerPackage(myPackage);
2353
+ * ```
2354
+ */
2355
+ interface IEnginePackage {
2356
+ /** Human-readable name for debugging and error attribution. */
2357
+ name: string;
2358
+ /**
2359
+ * Semver range of `solve-engine` versions this package is compatible with
2360
+ * (e.g. `"^0.1.0"`, `">=0.1.0 <0.3.0"`), checked against the engine's own
2361
+ * running version ({@link ENGINE_VERSION}, `@solve-js/constants/version`)
2362
+ * via `checkEngineVersionCompatibility()`/`assertEngineVersionCompatible()`
2363
+ * (`@solve-js/api/EngineVersionCompatibility`) at registration time.
2364
+ *
2365
+ * Optional, omitted means "no declared constraint," so every package
2366
+ * that predates this field (all built-ins, `examples/osrs`) keeps
2367
+ * registering exactly as before.
2368
+ *
2369
+ * Unlike every other compatibility signal in this codebase (e.g.
2370
+ * `checkPackageCompatibility()`'s sibling-package collision warnings,
2371
+ * which always log and proceed. See `api/PackageCompatibility.ts`), a
2372
+ * declared `engineVersion` range the running engine does NOT satisfy is
2373
+ * a deliberate, hard REJECTION: `registerPackage()` throws rather than
2374
+ * warning. See `ARCHITECTURE.md` §5.3.
2375
+ */
2376
+ engineVersion?: string;
2377
+ /** Optional lexer vocabulary (keywords/operators/units) for recognizing custom tokens (e.g., `GE`, `£`). */
2378
+ lexerVocabulary?: LexerVocabulary;
2379
+ /** Prefix parselets for this package's custom functions/operators. */
2380
+ prefixParselets?: Array<{
2381
+ tokenType: string;
2382
+ parselet: PrefixParselet;
2383
+ }>;
2384
+ /** Infix parselets for this package's custom binary operators. */
2385
+ infixParselets?: Array<{
2386
+ tokenType: string;
2387
+ parselet: InfixParselet;
2388
+ }>;
2389
+ /**
2390
+ * Functions dispatched via CALL_PLUGIN bytecode (emitted by this package's
2391
+ * parselets with `builder.emitIndex(index)`).
2392
+ *
2393
+ * Each entry's `index` MUST come from {@link allocatePluginFunctionIndex}
2394
+ * (`@solve-js/vm/VMBuiltins`), never hardcode a number. Two packages
2395
+ * independently picking the same index would silently overwrite each
2396
+ * other's handler in the shared registry.
2397
+ *
2398
+ * The handler's optional second parameter, `context`, carries the
2399
+ * current line's {@link LineExecutionContext} (line number, and, only
2400
+ * inside a real document, never `evaluateExpression()`'s single-shot
2401
+ * path, closures for reading another line's cached result). Every
2402
+ * handler that doesn't need cross-line data can ignore it entirely.
2403
+ *
2404
+ * @example
2405
+ * ```ts
2406
+ * const MY_FN_IDX = allocatePluginFunctionIndex();
2407
+ * // In a parselet's parse(): builder.emitOpcode(OpCode.CALL_PLUGIN); builder.emitIndex(MY_FN_IDX); builder.emitIndex(argCount);
2408
+ * pluginFunctions: [{ index: MY_FN_IDX, handler: myHandler }]
2409
+ * ```
2410
+ */
2411
+ pluginFunctions?: Array<{
2412
+ index: number;
2413
+ handler: (args: Value[], context?: LineExecutionContext) => Value | Promise<Value>;
2414
+ }>;
2415
+ /**
2416
+ * Named-variable sources.
2417
+ *
2418
+ * @deprecated Currently has no effect. Sources declared here are registered
2419
+ * into the engine's {@link EngineContext} and unregistered again on package
2420
+ * removal, but no evaluation path ever calls `VariableResolver.resolve()`, so
2421
+ * a variable a source provides is never found. Verified by searching every
2422
+ * use of `IVariableSource` outside its own declaration: they are all
2423
+ * registration bookkeeping.
2424
+ *
2425
+ * Declared here rather than deleted because removing a public field is a
2426
+ * breaking change and the intended behaviour is worth keeping. Documented as
2427
+ * dead so a package author does not spend an afternoon working out why their
2428
+ * variables resolve to nothing. To expose a value today, contribute a plugin
2429
+ * function through {@link IEnginePackage.pluginFunctions}.
2430
+ */
2431
+ variableSources?: IVariableSource[];
2432
+ /**
2433
+ * Async resolvers for this package's domain.
2434
+ * When set, the ExpressionEngine runs preflight() before VM execution
2435
+ * for each resolver. If async data is needed, a Pending result is
2436
+ * returned immediately and the line re-evaluates when the data resolves.
2437
+ *
2438
+ * Each resolver must have a unique `namespace`, the ResolverRegistry
2439
+ * is keyed by namespace. Multiple resolvers let a package handle
2440
+ * distinct async operations (e.g., `fetch`, `wait`, `poll`) in
2441
+ * separate, focused classes rather than one monolithic preflight().
2442
+ */
2443
+ asyncResolvers?: IAsyncResolver[];
2444
+ /**
2445
+ * Multi-word phrases to fuse into single compound tokens.
2446
+ * Each key is a space-separated phrase (e.g., "to the power of"),
2447
+ * each value is the target token type after fusion (e.g., "CARET").
2448
+ *
2449
+ * Registered into the engine's {@link PhraseTrie} for single-pass
2450
+ * O(depth) matching, no separate rule scanning per phrase.
2451
+ *
2452
+ * @example
2453
+ * ```ts
2454
+ * phrases: {
2455
+ * "to the power of": "CARET",
2456
+ * "abyssal whip": "ITEM",
2457
+ * }
2458
+ * ```
2459
+ */
2460
+ phrases?: Record<string, string>;
2461
+ /**
2462
+ * Normalizer rules for post-lexer token transformation.
2463
+ * Applied by the TokenNormalizer between lexing and parsing.
2464
+ * For phrase fusion, prefer the declarative {@link phrases} field
2465
+ * which uses the faster PhraseTrie. Use this for non-phrase rules
2466
+ * like implicit operator insertion.
2467
+ */
2468
+ normalizerRules?: NormalizerRule[];
2469
+ /**
2470
+ * Semantic highlight categories for this package's custom token types
2471
+ * (introduced via {@link lexerVocabulary} or {@link normalizerRules}), the
2472
+ * plugin-facing half of solve-js's editor-agnostic language service (see
2473
+ * `language/TokenCategoryMap.ts`). Without an entry here, a package's
2474
+ * custom tokens (e.g. a game-item name fused from several identifiers)
2475
+ * are still lexed and parsed correctly, but render with no highlight
2476
+ * category in any editor integration.
2477
+ *
2478
+ * @example
2479
+ * ```ts
2480
+ * tokenCategories: { MY_KEYWORD: "keyword", MY_ITEM: "my-plugin-item" }
2481
+ * ```
2482
+ */
2483
+ tokenCategories?: Record<string, TokenCategory>;
2484
+ /**
2485
+ * Completion candidates for this package, the plugin-facing half of
2486
+ * solve-js's editor-agnostic completions API
2487
+ * (`LanguageService.getCompletions()`). A package's single-word
2488
+ * keywords (via {@link lexerVocabulary}) already flow into completions
2489
+ * automatically; this field is for candidates that AREN'T lexer
2490
+ * keywords, such as a vocabulary of item/entity names. A plain,
2491
+ * pre-built list, not a callback, completion candidate lists are
2492
+ * meant to be cheap and static within one engine configuration.
2493
+ *
2494
+ * @example
2495
+ * ```ts
2496
+ * completionItems: [{ label: "Abyssal whip", category: "my-plugin-item", detail: "Item" }]
2497
+ * ```
2498
+ */
2499
+ completionItems?: CompletionItem[];
2500
+ /**
2501
+ * Custom `as <name>` converters, the extension point for the
2502
+ * Converters package's general `<expr> as <type>` grammar (e.g.
2503
+ * `50% as decimal`, `255 as hex`). The built-in converter names
2504
+ * (`percent`, `decimal`, `hex`, `fraction`, `multiplier`, `sci`,
2505
+ * `binary`, `octal`, ...) dispatch to dedicated fast opcodes; anything
2506
+ * else, including any name a third-party package registers here
2507
+ * resolves through `OpCode.CALL_AS_CONVERTER` against
2508
+ * `vm/VMBuiltins.ts`'s `asConverterRegistry` at runtime. No lexer
2509
+ * keyword registration is needed for a custom name: the AS parselet
2510
+ * accepts any bare-word token after "as" and reads its raw text.
2511
+ *
2512
+ * Each handler is a pure, synchronous `(value: Value) => Value`, for
2513
+ * async conversions (e.g. a live currency-style lookup), use
2514
+ * {@link asyncResolvers} instead.
2515
+ *
2516
+ * @example
2517
+ * ```ts
2518
+ * asConverters: { roman: (v) => stringValue(toRomanNumeral(v.toNumber())) }
2519
+ * ```
2520
+ */
2521
+ asConverters?: Record<string, (value: Value) => Value>;
2522
+ }
2523
+ /**
2524
+ * Default implementation of {@link IPackageRegistry}, the plugin registration API.
2525
+ *
2526
+ * All registrations delegate to shared singletons (parselet registry,
2527
+ * variable resolver, lexer). This ensures that packages registered through
2528
+ * any PackageRegistry instance are visible engine-wide.
2529
+ *
2530
+ * @example
2531
+ * ```typescript
2532
+ * import { packageRegistry } from "solve-engine";
2533
+ *
2534
+ * // Register a complete provider package
2535
+ * packageRegistry.registerPackage({
2536
+ * name: "MyProvider",
2537
+ * prefixParselets: [{ tokenType: "MY_FUNC", parselet: new MyParselet() }],
2538
+ * });
2539
+ * ```
2540
+ *
2541
+ * @deprecated Register on an engine instead:
2542
+ * `engine.registerPackage(pkg)`.
2543
+ *
2544
+ * This class writes into process-wide singletons, which is incompatible with an
2545
+ * engine owning its own registries. Since the introduction of
2546
+ * {@link EngineContext}, an engine reads plugin functions, opcode handlers and
2547
+ * variable sources from its own context, so a package registered here is not
2548
+ * visible to any engine. Parselets and lexer vocabulary registered here reach
2549
+ * the shared registries, which an engine also does not read: it builds its own
2550
+ * `ParseletRegistry` and its own `Lexer`.
2551
+ *
2552
+ * In other words this path registers into state nothing evaluates against. It
2553
+ * remains exported because removing it is a breaking change, and it is where
2554
+ * the singletons that survive are still written from, but it should not be used
2555
+ * in new code and will be removed before 1.0 proper.
2556
+ */
2557
+ declare class PackageRegistry implements IPackageRegistry {
2558
+ Value: typeof Value;
2559
+ registerPrefixParselet(tokenType: string, parselet: PrefixParselet): void;
2560
+ registerInfixParselet(tokenType: string, parselet: InfixParselet): void;
2561
+ registerVariableSource(source: IVariableSource): void;
2562
+ registerPackage(pkg: IEnginePackage): void;
2563
+ }
2564
+ /**
2565
+ * Singleton PackageRegistry instance, the default plugin registration API.
2566
+ *
2567
+ * All packages should register through this instance. The underlying registries
2568
+ * are shared singletons, so multiple PackageRegistry instances would be redundant.
2569
+ */
2570
+ declare const packageRegistry: PackageRegistry;
2571
+
2572
+ export { type ApplyChangesResult as A, type BatcherMetrics as B, type CacheSnapshot as C, DocumentModel as D, type EvalResults as E, type IEnginePackage as I, type LineEvaluation as L, PackageRegistry as P, type SemanticToken as S, type ViewportRange as V, ExpressionEngine as a, type IPackageRegistry as b, type LineChange as c, type InlineSolvePosition as d, type AsyncCachePackageInfo as e, type AsyncErrorEvent as f, AsyncResolutionBatcher as g, type AsyncResolutionEvent as h, type BytecodeCacheEntry as i, type CheckpointSnapshot as j, type LineCacheEntryInfo as k, type LineState as l, type LinesUpdatedEvent as m, type CompletionItem as n, LanguageService as o, packageRegistry as p, type LanguageServiceOptions as q };