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.
- package/dist/BytecodeBuilder-3egMyuDi.d.cts +274 -0
- package/dist/BytecodeBuilder-3egMyuDi.d.ts +274 -0
- package/dist/Configuration-fVpd-HC-.d.cts +228 -0
- package/dist/Configuration-fVpd-HC-.d.ts +228 -0
- package/dist/EngineError-DvPSnhZi.d.cts +304 -0
- package/dist/EngineError-DvPSnhZi.d.ts +304 -0
- package/dist/Lexer-BJdhlnej.d.cts +531 -0
- package/dist/Lexer-D9l4Yrk2.d.ts +531 -0
- package/dist/NormalizerRule-BrVoVjmP.d.cts +163 -0
- package/dist/NormalizerRule-CEjf1FyD.d.ts +163 -0
- package/dist/PackageRegistry-D-Tv_7ca.d.cts +2572 -0
- package/dist/PackageRegistry-DaH4aIKP.d.ts +2572 -0
- package/dist/Parselet-CK8cQu2a.d.cts +299 -0
- package/dist/Parselet-ConOIdRO.d.ts +299 -0
- package/dist/ScopeManager-B5k0nk05.d.ts +559 -0
- package/dist/ScopeManager-CKfQtl7l.d.cts +559 -0
- package/dist/Token-0jpvUdPY.d.cts +245 -0
- package/dist/Token-0jpvUdPY.d.ts +245 -0
- package/dist/VMCheckpoints-BmDwIXgK.d.ts +171 -0
- package/dist/VMCheckpoints-CqFuLwrT.d.cts +171 -0
- package/dist/Value-KtAQNHur.d.cts +295 -0
- package/dist/Value-KtAQNHur.d.ts +295 -0
- package/dist/chunk-2RWAXT6O.js +3 -0
- package/dist/chunk-2RWAXT6O.js.map +1 -0
- package/dist/chunk-34RRD7PC.js +1492 -0
- package/dist/chunk-34RRD7PC.js.map +1 -0
- package/dist/chunk-3PPFLFH4.js +5097 -0
- package/dist/chunk-3PPFLFH4.js.map +1 -0
- package/dist/chunk-3VGDNEE3.js +84 -0
- package/dist/chunk-3VGDNEE3.js.map +1 -0
- package/dist/chunk-3YNVWKR2.cjs +370 -0
- package/dist/chunk-3YNVWKR2.cjs.map +1 -0
- package/dist/chunk-47LRVGOT.cjs +316 -0
- package/dist/chunk-47LRVGOT.cjs.map +1 -0
- package/dist/chunk-4MG4XKO2.js +1644 -0
- package/dist/chunk-4MG4XKO2.js.map +1 -0
- package/dist/chunk-4QADQTWS.js +2392 -0
- package/dist/chunk-4QADQTWS.js.map +1 -0
- package/dist/chunk-53B6KDDJ.cjs +1651 -0
- package/dist/chunk-53B6KDDJ.cjs.map +1 -0
- package/dist/chunk-5X2PTP6F.cjs +104 -0
- package/dist/chunk-5X2PTP6F.cjs.map +1 -0
- package/dist/chunk-5YEMOYSE.js +264 -0
- package/dist/chunk-5YEMOYSE.js.map +1 -0
- package/dist/chunk-64W6GLLZ.js +481 -0
- package/dist/chunk-64W6GLLZ.js.map +1 -0
- package/dist/chunk-6BKTCEUP.cjs +233 -0
- package/dist/chunk-6BKTCEUP.cjs.map +1 -0
- package/dist/chunk-6NTVRDQV.cjs +1642 -0
- package/dist/chunk-6NTVRDQV.cjs.map +1 -0
- package/dist/chunk-6RDSQURE.js +290 -0
- package/dist/chunk-6RDSQURE.js.map +1 -0
- package/dist/chunk-75JP4BZX.js +93 -0
- package/dist/chunk-75JP4BZX.js.map +1 -0
- package/dist/chunk-B7LXJ6LC.cjs +36 -0
- package/dist/chunk-B7LXJ6LC.cjs.map +1 -0
- package/dist/chunk-BVAR5J2F.js +257 -0
- package/dist/chunk-BVAR5J2F.js.map +1 -0
- package/dist/chunk-C4XZV6E7.cjs +269 -0
- package/dist/chunk-C4XZV6E7.cjs.map +1 -0
- package/dist/chunk-CCJPSHGI.cjs +40 -0
- package/dist/chunk-CCJPSHGI.cjs.map +1 -0
- package/dist/chunk-CLL7RUQV.cjs +316 -0
- package/dist/chunk-CLL7RUQV.cjs.map +1 -0
- package/dist/chunk-DM3LMRBC.js +329 -0
- package/dist/chunk-DM3LMRBC.js.map +1 -0
- package/dist/chunk-DUEZFTK2.js +38 -0
- package/dist/chunk-DUEZFTK2.js.map +1 -0
- package/dist/chunk-EBSPLUW4.cjs +489 -0
- package/dist/chunk-EBSPLUW4.cjs.map +1 -0
- package/dist/chunk-EHAHVROS.cjs +261 -0
- package/dist/chunk-EHAHVROS.cjs.map +1 -0
- package/dist/chunk-EIGTWK5N.js +167 -0
- package/dist/chunk-EIGTWK5N.js.map +1 -0
- package/dist/chunk-EPOXXJBK.js +96 -0
- package/dist/chunk-EPOXXJBK.js.map +1 -0
- package/dist/chunk-FEAIOR7N.cjs +91 -0
- package/dist/chunk-FEAIOR7N.cjs.map +1 -0
- package/dist/chunk-G535KJEG.js +312 -0
- package/dist/chunk-G535KJEG.js.map +1 -0
- package/dist/chunk-GCM5JHMX.js +88 -0
- package/dist/chunk-GCM5JHMX.js.map +1 -0
- package/dist/chunk-GOLDJNMZ.js +368 -0
- package/dist/chunk-GOLDJNMZ.js.map +1 -0
- package/dist/chunk-GW32KPCU.cjs +360 -0
- package/dist/chunk-GW32KPCU.cjs.map +1 -0
- package/dist/chunk-HTXVVJRA.cjs +1549 -0
- package/dist/chunk-HTXVVJRA.cjs.map +1 -0
- package/dist/chunk-HWSZJQCI.js +311 -0
- package/dist/chunk-HWSZJQCI.js.map +1 -0
- package/dist/chunk-IF532O7C.js +101 -0
- package/dist/chunk-IF532O7C.js.map +1 -0
- package/dist/chunk-J73SJHR3.cjs +2415 -0
- package/dist/chunk-J73SJHR3.cjs.map +1 -0
- package/dist/chunk-JBSYC7BB.cjs +7473 -0
- package/dist/chunk-JBSYC7BB.cjs.map +1 -0
- package/dist/chunk-KVILKGMS.js +228 -0
- package/dist/chunk-KVILKGMS.js.map +1 -0
- package/dist/chunk-LIPPNDBE.js +258 -0
- package/dist/chunk-LIPPNDBE.js.map +1 -0
- package/dist/chunk-LR7YASZF.cjs +172 -0
- package/dist/chunk-LR7YASZF.cjs.map +1 -0
- package/dist/chunk-M4N5GA5Z.cjs +309 -0
- package/dist/chunk-M4N5GA5Z.cjs.map +1 -0
- package/dist/chunk-M5LX5AOO.js +7459 -0
- package/dist/chunk-M5LX5AOO.js.map +1 -0
- package/dist/chunk-MZOVP3WI.cjs +96 -0
- package/dist/chunk-MZOVP3WI.cjs.map +1 -0
- package/dist/chunk-N5YFR4UP.cjs +10 -0
- package/dist/chunk-N5YFR4UP.cjs.map +1 -0
- package/dist/chunk-NGBBNL4K.cjs +4 -0
- package/dist/chunk-NGBBNL4K.cjs.map +1 -0
- package/dist/chunk-NH2O2AUR.js +146 -0
- package/dist/chunk-NH2O2AUR.js.map +1 -0
- package/dist/chunk-NLNDMWZC.js +31 -0
- package/dist/chunk-NLNDMWZC.js.map +1 -0
- package/dist/chunk-NMCRQP3Z.cjs +778 -0
- package/dist/chunk-NMCRQP3Z.cjs.map +1 -0
- package/dist/chunk-NMD5VRN4.cjs +1327 -0
- package/dist/chunk-NMD5VRN4.cjs.map +1 -0
- package/dist/chunk-OC6LTXYS.cjs +286 -0
- package/dist/chunk-OC6LTXYS.cjs.map +1 -0
- package/dist/chunk-OT6OJY7C.cjs +114 -0
- package/dist/chunk-OT6OJY7C.cjs.map +1 -0
- package/dist/chunk-PFSDGGYL.js +8 -0
- package/dist/chunk-PFSDGGYL.js.map +1 -0
- package/dist/chunk-QNJ4ACRT.cjs +155 -0
- package/dist/chunk-QNJ4ACRT.cjs.map +1 -0
- package/dist/chunk-QWY3VEZN.js +1637 -0
- package/dist/chunk-QWY3VEZN.js.map +1 -0
- package/dist/chunk-RFYD5TJE.js +111 -0
- package/dist/chunk-RFYD5TJE.js.map +1 -0
- package/dist/chunk-RIN643A3.js +354 -0
- package/dist/chunk-RIN643A3.js.map +1 -0
- package/dist/chunk-RZCWSXTA.cjs +5122 -0
- package/dist/chunk-RZCWSXTA.cjs.map +1 -0
- package/dist/chunk-SDGRK7EP.js +774 -0
- package/dist/chunk-SDGRK7EP.js.map +1 -0
- package/dist/chunk-VEKIWQ3F.cjs +39 -0
- package/dist/chunk-VEKIWQ3F.cjs.map +1 -0
- package/dist/chunk-WQTTOGXC.cjs +99 -0
- package/dist/chunk-WQTTOGXC.cjs.map +1 -0
- package/dist/chunk-X6K3HUOY.cjs +86 -0
- package/dist/chunk-X6K3HUOY.cjs.map +1 -0
- package/dist/chunk-XJCMXB2E.cjs +333 -0
- package/dist/chunk-XJCMXB2E.cjs.map +1 -0
- package/dist/chunk-XVWCOTR6.js +1324 -0
- package/dist/chunk-XVWCOTR6.js.map +1 -0
- package/dist/chunk-YLJ4XMA6.js +33 -0
- package/dist/chunk-YLJ4XMA6.js.map +1 -0
- package/dist/chunk-Z2XP767T.cjs +35 -0
- package/dist/chunk-Z2XP767T.cjs.map +1 -0
- package/dist/chunk-Z4X2RWOF.js +37 -0
- package/dist/chunk-Z4X2RWOF.js.map +1 -0
- package/dist/constants.cjs +22 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +12 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -0
- package/dist/engine.cjs +79 -0
- package/dist/engine.cjs.map +1 -0
- package/dist/engine.d.cts +587 -0
- package/dist/engine.d.ts +587 -0
- package/dist/engine.js +34 -0
- package/dist/engine.js.map +1 -0
- package/dist/errors.cjs +81 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.cts +95 -0
- package/dist/errors.d.ts +95 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/format.cjs +171 -0
- package/dist/format.cjs.map +1 -0
- package/dist/format.d.cts +51 -0
- package/dist/format.d.ts +51 -0
- package/dist/format.js +168 -0
- package/dist/format.js.map +1 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +152 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/language.cjs +308 -0
- package/dist/language.cjs.map +1 -0
- package/dist/language.d.cts +110 -0
- package/dist/language.d.ts +110 -0
- package/dist/language.js +287 -0
- package/dist/language.js.map +1 -0
- package/dist/lexer.cjs +64 -0
- package/dist/lexer.cjs.map +1 -0
- package/dist/lexer.d.cts +56 -0
- package/dist/lexer.d.ts +56 -0
- package/dist/lexer.js +11 -0
- package/dist/lexer.js.map +1 -0
- package/dist/normalizer.cjs +40 -0
- package/dist/normalizer.cjs.map +1 -0
- package/dist/normalizer.d.cts +364 -0
- package/dist/normalizer.d.ts +364 -0
- package/dist/normalizer.js +11 -0
- package/dist/normalizer.js.map +1 -0
- package/dist/packages.cjs +121 -0
- package/dist/packages.cjs.map +1 -0
- package/dist/packages.d.cts +691 -0
- package/dist/packages.d.ts +691 -0
- package/dist/packages.js +24 -0
- package/dist/packages.js.map +1 -0
- package/dist/parser.cjs +55 -0
- package/dist/parser.cjs.map +1 -0
- package/dist/parser.d.cts +46 -0
- package/dist/parser.d.ts +46 -0
- package/dist/parser.js +10 -0
- package/dist/parser.js.map +1 -0
- package/dist/pipeline-CrsRPmsv.d.ts +285 -0
- package/dist/pipeline-DPUQt7yL.d.cts +285 -0
- package/dist/resolvers.cjs +13 -0
- package/dist/resolvers.cjs.map +1 -0
- package/dist/resolvers.d.cts +111 -0
- package/dist/resolvers.d.ts +111 -0
- package/dist/resolvers.js +4 -0
- package/dist/resolvers.js.map +1 -0
- package/dist/services.cjs +21 -0
- package/dist/services.cjs.map +1 -0
- package/dist/services.d.cts +30 -0
- package/dist/services.d.ts +30 -0
- package/dist/services.js +4 -0
- package/dist/services.js.map +1 -0
- package/dist/uom.cjs +56 -0
- package/dist/uom.cjs.map +1 -0
- package/dist/uom.d.cts +224 -0
- package/dist/uom.d.ts +224 -0
- package/dist/uom.js +11 -0
- package/dist/uom.js.map +1 -0
- package/dist/utilities.cjs +32 -0
- package/dist/utilities.cjs.map +1 -0
- package/dist/utilities.d.cts +113 -0
- package/dist/utilities.d.ts +113 -0
- package/dist/utilities.js +7 -0
- package/dist/utilities.js.map +1 -0
- package/dist/variables.cjs +17 -0
- package/dist/variables.cjs.map +1 -0
- package/dist/variables.d.cts +87 -0
- package/dist/variables.d.ts +87 -0
- package/dist/variables.js +4 -0
- package/dist/variables.js.map +1 -0
- package/dist/vm.cjs +284 -0
- package/dist/vm.cjs.map +1 -0
- package/dist/vm.d.cts +68 -0
- package/dist/vm.d.ts +68 -0
- package/dist/vm.js +194 -0
- package/dist/vm.js.map +1 -0
- 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 };
|