solve-engine 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BytecodeBuilder-B0xskcv5.d.cts → BytecodeBuilder-Bp9xeTmX.d.cts} +4 -0
- package/dist/{BytecodeBuilder-B0xskcv5.d.ts → BytecodeBuilder-Bp9xeTmX.d.ts} +4 -0
- package/dist/{Configuration-B-G5gTRn.d.cts → Configuration-C9W8tJv_.d.cts} +53 -1
- package/dist/{Configuration-B-G5gTRn.d.ts → Configuration-C9W8tJv_.d.ts} +53 -1
- package/dist/{EngineError-LU7W7AgI.d.cts → EngineError-B61GS1jp.d.cts} +14 -0
- package/dist/{EngineError-LU7W7AgI.d.ts → EngineError-B61GS1jp.d.ts} +14 -0
- package/dist/FormattingSettings-CJHyxcYu.d.cts +27 -0
- package/dist/FormattingSettings-CJHyxcYu.d.ts +27 -0
- package/dist/{Lexer-DpjdaQ98.d.cts → Lexer-BOs7euZe.d.cts} +28 -1
- package/dist/{Lexer-CNmWxabg.d.ts → Lexer-CSI_lwbW.d.ts} +28 -1
- package/dist/PackageCompatibility-B-7rK1TD.d.cts +76 -0
- package/dist/PackageCompatibility-Dh59eF-X.d.ts +76 -0
- package/dist/{PackageRegistry-zuzqt51V.d.cts → PackageRegistry-BHWJP83F.d.cts} +449 -11
- package/dist/{PackageRegistry-ClIFXxAe.d.ts → PackageRegistry-_8rDlvxI.d.ts} +449 -11
- package/dist/{Parselet-BaySkMV3.d.ts → Parselet-BBT8riYh.d.ts} +12 -3
- package/dist/{Parselet-DuI1Pjiq.d.cts → Parselet-BHcgK9S7.d.cts} +12 -3
- package/dist/{ScopeManager-B6GzdhVG.d.cts → ScopeManager-8vf02dwj.d.cts} +56 -5
- package/dist/{ScopeManager-udv4Twwq.d.ts → ScopeManager-CxA24W5n.d.ts} +56 -5
- package/dist/{Token-BzG5G4ja.d.cts → Token-B1hdkedD.d.cts} +9 -0
- package/dist/{Token-BzG5G4ja.d.ts → Token-B1hdkedD.d.ts} +9 -0
- package/dist/{TokenNormalizer-t_GotBxr.d.ts → TokenNormalizer-C6VzZgHa.d.ts} +1 -1
- package/dist/{TokenNormalizer-DGVa24Q-.d.cts → TokenNormalizer-DRc1Js1V.d.cts} +1 -1
- package/dist/{VMCheckpoints-DwLjivM7.d.cts → VMCheckpoints-ELYqITdF.d.cts} +3 -3
- package/dist/{VMCheckpoints-BiaIlOOY.d.ts → VMCheckpoints-MK--EBH2.d.ts} +3 -3
- package/dist/{Value-CXJqDH9J.d.cts → Value-BUi1RA3S.d.cts} +124 -5
- package/dist/{Value-CXJqDH9J.d.ts → Value-BUi1RA3S.d.ts} +124 -5
- package/dist/WorkerError-_-RkoQ5P.d.ts +75 -0
- package/dist/WorkerError-gzmaopj2.d.cts +75 -0
- package/dist/{chunk-GQCOSXMG.js → chunk-267JPOTF.js} +43 -5
- package/dist/chunk-267JPOTF.js.map +1 -0
- package/dist/{chunk-JMXUNXQS.cjs → chunk-2TZKENDH.cjs} +51 -8
- package/dist/chunk-2TZKENDH.cjs.map +1 -0
- package/dist/chunk-3OWCDIPN.js +215 -0
- package/dist/chunk-3OWCDIPN.js.map +1 -0
- package/dist/{chunk-VB37OC6I.js → chunk-4D6NIHE2.js} +40 -3
- package/dist/chunk-4D6NIHE2.js.map +1 -0
- package/dist/{chunk-TY3TLZAW.cjs → chunk-536WPM2V.cjs} +18 -2
- package/dist/chunk-536WPM2V.cjs.map +1 -0
- package/dist/chunk-5HRB36DK.js +405 -0
- package/dist/chunk-5HRB36DK.js.map +1 -0
- package/dist/chunk-5ON7PUAZ.js +12 -0
- package/dist/chunk-5ON7PUAZ.js.map +1 -0
- package/dist/{chunk-KV7UW6T6.js → chunk-7KYSUDQO.js} +12 -5
- package/dist/chunk-7KYSUDQO.js.map +1 -0
- package/dist/{chunk-6WFMPTGB.cjs → chunk-7ZX6B7SY.cjs} +774 -528
- package/dist/chunk-7ZX6B7SY.cjs.map +1 -0
- package/dist/chunk-ALYRJ72W.cjs +434 -0
- package/dist/chunk-ALYRJ72W.cjs.map +1 -0
- package/dist/{chunk-5KMIY374.cjs → chunk-B4HBEFTB.cjs} +17 -2
- package/dist/chunk-B4HBEFTB.cjs.map +1 -0
- package/dist/chunk-B7NLZMQ3.cjs +74 -0
- package/dist/chunk-B7NLZMQ3.cjs.map +1 -0
- package/dist/{chunk-UQ3UIZJC.js → chunk-BLI4NIQY.js} +6 -2
- package/dist/chunk-BLI4NIQY.js.map +1 -0
- package/dist/{chunk-T556MJDZ.cjs → chunk-C75AFGVD.cjs} +11 -11
- package/dist/{chunk-T556MJDZ.cjs.map → chunk-C75AFGVD.cjs.map} +1 -1
- package/dist/{chunk-G535KJEG.js → chunk-CCBZZQAE.js} +2 -2
- package/dist/{chunk-G535KJEG.js.map → chunk-CCBZZQAE.js.map} +1 -1
- package/dist/chunk-CKQMXMHR.cjs +219 -0
- package/dist/chunk-CKQMXMHR.cjs.map +1 -0
- package/dist/{chunk-PFUESQTW.cjs → chunk-EJ3ILXX6.cjs} +48 -2
- package/dist/chunk-EJ3ILXX6.cjs.map +1 -0
- package/dist/{chunk-526PMQOA.js → chunk-ENKKJYD3.js} +10 -4
- package/dist/chunk-ENKKJYD3.js.map +1 -0
- package/dist/{chunk-5X2PTP6F.cjs → chunk-ENRIK36Q.cjs} +2 -12
- package/dist/chunk-ENRIK36Q.cjs.map +1 -0
- package/dist/{chunk-3D7V24DG.js → chunk-ERCOHGXD.js} +17 -2
- package/dist/chunk-ERCOHGXD.js.map +1 -0
- package/dist/{chunk-IF532O7C.js → chunk-FD5ZZHEU.js} +3 -12
- package/dist/chunk-FD5ZZHEU.js.map +1 -0
- package/dist/{chunk-7FSDNNNC.js → chunk-G7Z4HJQA.js} +11 -5
- package/dist/chunk-G7Z4HJQA.js.map +1 -0
- package/dist/{chunk-FQGX2PA2.js → chunk-GQMUHVE3.js} +115 -4
- package/dist/chunk-GQMUHVE3.js.map +1 -0
- package/dist/chunk-GUG7SNSV.js +1764 -0
- package/dist/chunk-GUG7SNSV.js.map +1 -0
- package/dist/{chunk-UM6BVY2S.cjs → chunk-HIQ5HSZL.cjs} +175 -43
- package/dist/chunk-HIQ5HSZL.cjs.map +1 -0
- package/dist/{chunk-HVQFNJKE.cjs → chunk-HVRVSI2Z.cjs} +104 -86
- package/dist/chunk-HVRVSI2Z.cjs.map +1 -0
- package/dist/{chunk-3LAEG75D.js → chunk-I4GAWIPW.js} +24 -6
- package/dist/chunk-I4GAWIPW.js.map +1 -0
- package/dist/{chunk-PA4VC73I.cjs → chunk-J45BCEZ4.cjs} +33 -27
- package/dist/chunk-J45BCEZ4.cjs.map +1 -0
- package/dist/{chunk-NG2JHZHE.js → chunk-JIPATHVY.js} +1853 -2640
- package/dist/chunk-JIPATHVY.js.map +1 -0
- package/dist/{chunk-IP7ASJEW.js → chunk-JJIXHXFQ.js} +5 -5
- package/dist/{chunk-IP7ASJEW.js.map → chunk-JJIXHXFQ.js.map} +1 -1
- package/dist/chunk-L2TE7PMO.cjs +14 -0
- package/dist/chunk-L2TE7PMO.cjs.map +1 -0
- package/dist/{chunk-HBVFFBRR.cjs → chunk-MG6Q3DUO.cjs} +1962 -2748
- package/dist/chunk-MG6Q3DUO.cjs.map +1 -0
- package/dist/{chunk-R3PY4G7J.js → chunk-MVTOCRV2.js} +48 -3
- package/dist/chunk-MVTOCRV2.js.map +1 -0
- package/dist/{chunk-Y7FT4IQT.js → chunk-NKW7LKYU.js} +266 -28
- package/dist/chunk-NKW7LKYU.js.map +1 -0
- package/dist/{chunk-O3ANBHSA.js → chunk-NNQ2TYDF.js} +123 -5
- package/dist/chunk-NNQ2TYDF.js.map +1 -0
- package/dist/{chunk-AA3KTWTX.js → chunk-OADDUPT3.js} +4 -4
- package/dist/{chunk-AA3KTWTX.js.map → chunk-OADDUPT3.js.map} +1 -1
- package/dist/{chunk-SESSWASV.cjs → chunk-OTN6SZOD.cjs} +1868 -393
- package/dist/chunk-OTN6SZOD.cjs.map +1 -0
- package/dist/{chunk-5LI5EPGJ.cjs → chunk-QFTDTX6K.cjs} +40 -3
- package/dist/chunk-QFTDTX6K.cjs.map +1 -0
- package/dist/{chunk-YXCTLWOH.js → chunk-RRLHC37V.js} +1530 -57
- package/dist/chunk-RRLHC37V.js.map +1 -0
- package/dist/chunk-SNHVVOJK.cjs +1768 -0
- package/dist/chunk-SNHVVOJK.cjs.map +1 -0
- package/dist/{chunk-HDP7VK3C.cjs → chunk-SRGQ72IR.cjs} +649 -349
- package/dist/chunk-SRGQ72IR.cjs.map +1 -0
- package/dist/{chunk-4B2CNWQU.cjs → chunk-SSV46KFA.cjs} +14 -7
- package/dist/chunk-SSV46KFA.cjs.map +1 -0
- package/dist/{chunk-AJA6LUI7.js → chunk-UO6BUV6K.js} +18 -2
- package/dist/chunk-UO6BUV6K.js.map +1 -0
- package/dist/chunk-WWQEY7BV.js +67 -0
- package/dist/chunk-WWQEY7BV.js.map +1 -0
- package/dist/{chunk-5WVP4YHP.js → chunk-WXEHD6TT.js} +333 -33
- package/dist/chunk-WXEHD6TT.js.map +1 -0
- package/dist/{chunk-47LRVGOT.cjs → chunk-XA4CKRML.cjs} +2 -2
- package/dist/{chunk-47LRVGOT.cjs.map → chunk-XA4CKRML.cjs.map} +1 -1
- package/dist/{chunk-6KFYJ6TD.cjs → chunk-XIYYHA65.cjs} +6 -2
- package/dist/chunk-XIYYHA65.cjs.map +1 -0
- package/dist/{chunk-FE23VSSA.cjs → chunk-XJGJCF2R.cjs} +6 -6
- package/dist/{chunk-FE23VSSA.cjs.map → chunk-XJGJCF2R.cjs.map} +1 -1
- package/dist/{chunk-R7YVBHCV.cjs → chunk-YTIYVHL7.cjs} +184 -73
- package/dist/chunk-YTIYVHL7.cjs.map +1 -0
- package/dist/{chunk-TMA4RCEN.js → chunk-Z7XGGLI2.js} +3 -3
- package/dist/{chunk-TMA4RCEN.js.map → chunk-Z7XGGLI2.js.map} +1 -1
- package/dist/{chunk-A2N2GFCG.cjs → chunk-ZVTWQLK4.cjs} +10 -10
- package/dist/{chunk-A2N2GFCG.cjs.map → chunk-ZVTWQLK4.cjs.map} +1 -1
- package/dist/{chunk-524F3ATQ.cjs → chunk-ZXCESUJS.cjs} +15 -9
- package/dist/chunk-ZXCESUJS.cjs.map +1 -0
- package/dist/constants.cjs +5 -5
- package/dist/constants.d.cts +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +2 -2
- package/dist/engine.cjs +52 -35
- package/dist/engine.d.cts +13 -13
- package/dist/engine.d.ts +13 -13
- package/dist/engine.js +26 -21
- package/dist/errors.cjs +44 -19
- package/dist/errors.d.cts +3 -2
- package/dist/errors.d.ts +3 -2
- package/dist/errors.js +2 -1
- package/dist/format.cjs +19 -169
- package/dist/format.cjs.map +1 -1
- package/dist/format.d.cts +18 -26
- package/dist/format.d.ts +18 -26
- package/dist/format.js +7 -171
- package/dist/format.js.map +1 -1
- package/dist/index.cjs +250 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -74
- package/dist/index.d.ts +110 -74
- package/dist/index.js +228 -23
- package/dist/index.js.map +1 -1
- package/dist/language.cjs +9 -9
- package/dist/language.d.cts +12 -12
- package/dist/language.d.ts +12 -12
- package/dist/language.js +3 -3
- package/dist/lexer.cjs +16 -16
- package/dist/lexer.d.cts +3 -3
- package/dist/lexer.d.ts +3 -3
- package/dist/lexer.js +5 -5
- package/dist/normalizer.cjs +10 -10
- package/dist/normalizer.d.cts +3 -3
- package/dist/normalizer.d.ts +3 -3
- package/dist/normalizer.js +4 -4
- package/dist/packages.cjs +45 -36
- package/dist/packages.d.cts +135 -31
- package/dist/packages.d.ts +135 -31
- package/dist/packages.js +14 -13
- package/dist/parser.cjs +16 -16
- package/dist/parser.d.cts +6 -5
- package/dist/parser.d.ts +6 -5
- package/dist/parser.js +6 -6
- package/dist/{pipeline-BEb3hujr.d.cts → pipeline-CtfJtPQc.d.cts} +3 -3
- package/dist/{pipeline-B6k5lCB7.d.ts → pipeline-DCd5M6Gk.d.ts} +3 -3
- package/dist/resolvers.d.cts +3 -3
- package/dist/resolvers.d.ts +3 -3
- package/dist/testing.cjs +478 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +271 -0
- package/dist/testing.d.ts +271 -0
- package/dist/testing.js +470 -0
- package/dist/testing.js.map +1 -0
- package/dist/uom.cjs +16 -16
- package/dist/uom.d.cts +3 -3
- package/dist/uom.d.ts +3 -3
- package/dist/uom.js +6 -6
- package/dist/utilities.cjs +6 -5
- package/dist/utilities.js +2 -1
- package/dist/vm.cjs +34 -34
- package/dist/vm.d.cts +8 -8
- package/dist/vm.d.ts +8 -8
- package/dist/vm.js +9 -9
- package/dist/worker.cjs +493 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.d.cts +509 -0
- package/dist/worker.d.ts +509 -0
- package/dist/worker.js +484 -0
- package/dist/worker.js.map +1 -0
- package/package.json +21 -1
- package/dist/chunk-3D7V24DG.js.map +0 -1
- package/dist/chunk-3LAEG75D.js.map +0 -1
- package/dist/chunk-4B2CNWQU.cjs.map +0 -1
- package/dist/chunk-524F3ATQ.cjs.map +0 -1
- package/dist/chunk-526PMQOA.js.map +0 -1
- package/dist/chunk-5KMIY374.cjs.map +0 -1
- package/dist/chunk-5LI5EPGJ.cjs.map +0 -1
- package/dist/chunk-5WVP4YHP.js.map +0 -1
- package/dist/chunk-5X2PTP6F.cjs.map +0 -1
- package/dist/chunk-6KFYJ6TD.cjs.map +0 -1
- package/dist/chunk-6WFMPTGB.cjs.map +0 -1
- package/dist/chunk-7FSDNNNC.js.map +0 -1
- package/dist/chunk-AJA6LUI7.js.map +0 -1
- package/dist/chunk-FQGX2PA2.js.map +0 -1
- package/dist/chunk-GQCOSXMG.js.map +0 -1
- package/dist/chunk-HBVFFBRR.cjs.map +0 -1
- package/dist/chunk-HDP7VK3C.cjs.map +0 -1
- package/dist/chunk-HVQFNJKE.cjs.map +0 -1
- package/dist/chunk-IF532O7C.js.map +0 -1
- package/dist/chunk-JMXUNXQS.cjs.map +0 -1
- package/dist/chunk-KV7UW6T6.js.map +0 -1
- package/dist/chunk-NG2JHZHE.js.map +0 -1
- package/dist/chunk-O3ANBHSA.js.map +0 -1
- package/dist/chunk-PA4VC73I.cjs.map +0 -1
- package/dist/chunk-PFUESQTW.cjs.map +0 -1
- package/dist/chunk-R3PY4G7J.js.map +0 -1
- package/dist/chunk-R7YVBHCV.cjs.map +0 -1
- package/dist/chunk-SESSWASV.cjs.map +0 -1
- package/dist/chunk-TY3TLZAW.cjs.map +0 -1
- package/dist/chunk-UM6BVY2S.cjs.map +0 -1
- package/dist/chunk-UQ3UIZJC.js.map +0 -1
- package/dist/chunk-VB37OC6I.js.map +0 -1
- package/dist/chunk-Y7FT4IQT.js.map +0 -1
- package/dist/chunk-YXCTLWOH.js.map +0 -1
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
import { ParseletRegistry, PrecedenceParser } from './chunk-
|
|
1
|
+
import { ParseletRegistry, PrecedenceParser } from './chunk-I4GAWIPW.js';
|
|
2
2
|
import { ResolverRegistry } from './chunk-3VGDNEE3.js';
|
|
3
|
-
import { abortLogger
|
|
4
|
-
import { executeBytecode, createVM, DependencyGraph, ScopeManager } from './chunk-
|
|
5
|
-
import { DiagnosticPipeline, TimelineDiagnosticCollector, DiagnosticEventType } from './chunk-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
3
|
+
import { abortLogger } from './chunk-FD5ZZHEU.js';
|
|
4
|
+
import { executeBytecode, createVM, DependencyGraph, ScopeManager } from './chunk-WXEHD6TT.js';
|
|
5
|
+
import { DiagnosticPipeline, TimelineDiagnosticCollector, DiagnosticEventType } from './chunk-CCBZZQAE.js';
|
|
6
|
+
import { formatValue, DEFAULT_FORMATTING_SETTINGS } from './chunk-3OWCDIPN.js';
|
|
7
|
+
import { BUILTIN_PACKAGES, containsSymbolicCall } from './chunk-RRLHC37V.js';
|
|
8
|
+
import { BytecodeBuilder } from './chunk-ENKKJYD3.js';
|
|
9
|
+
import { TokenNormalizer, BUILTIN_PHRASES, implicitMultiplyRule } from './chunk-JJIXHXFQ.js';
|
|
9
10
|
import { countLines } from './chunk-3BOK2LDK.js';
|
|
10
11
|
import { createQueryClient, setActiveQueryClient, getActiveQueryClient } from './chunk-NLNDMWZC.js';
|
|
11
|
-
import { sharedOpRegistry, createEngineContext, registerAsConverter, unregisterAsConverter, solveEquationValues, globalDagKey
|
|
12
|
-
import { matrixMultiply, inverse } from './chunk-
|
|
13
|
-
import {
|
|
14
|
-
import { mergeEngineConfig, DEFAULT_CONFIG, ENGINE_VERSION } from './chunk-
|
|
15
|
-
import { Lexer, buildTokenLookup, sharedLexer } from './chunk-
|
|
16
|
-
import { registerTokenCategory, unregisterTokenCategory } from './chunk-
|
|
17
|
-
import {
|
|
12
|
+
import { sharedOpRegistry, createEngineContext, registerAsConverter, unregisterAsConverter, solveEquationValues, globalDagKey } from './chunk-NKW7LKYU.js';
|
|
13
|
+
import { matrixMultiply, inverse } from './chunk-NNQ2TYDF.js';
|
|
14
|
+
import { errorValue, pendingValue, stringValue, numberValue, freezeIfDev, Value, boolValue, uomValue, percentageValue, datetimeValue, bigIntValue, hexValue, ValueType } from './chunk-267JPOTF.js';
|
|
15
|
+
import { mergeEngineConfig, DEFAULT_CONFIG, ENGINE_VERSION } from './chunk-G7Z4HJQA.js';
|
|
16
|
+
import { Lexer, buildTokenLookup, sharedLexer } from './chunk-OADDUPT3.js';
|
|
17
|
+
import { registerTokenCategory, unregisterTokenCategory } from './chunk-UO6BUV6K.js';
|
|
18
|
+
import { LexerToken } from './chunk-GQMUHVE3.js';
|
|
19
|
+
import { TokenTypes, tokenTypeId } from './chunk-4D6NIHE2.js';
|
|
20
|
+
import { getLocale } from './chunk-XBTEO4OB.js';
|
|
21
|
+
import { normalizeUnknownError, ErrorFactory, EngineError } from './chunk-ERCOHGXD.js';
|
|
18
22
|
import { __commonJS, __toESM } from './chunk-YLJ4XMA6.js';
|
|
19
23
|
|
|
20
24
|
// node_modules/semver/internal/constants.js
|
|
@@ -1965,106 +1969,41 @@ var require_semver2 = __commonJS({
|
|
|
1965
1969
|
}
|
|
1966
1970
|
});
|
|
1967
1971
|
|
|
1968
|
-
// src/
|
|
1969
|
-
var
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
this.writeVariable = writeVariable;
|
|
1975
|
-
}
|
|
1976
|
-
};
|
|
1977
|
-
var LineCache = class _LineCache {
|
|
1978
|
-
constructor() {
|
|
1979
|
-
/** line number → (expression, or "" for expressionless entries) → entry */
|
|
1980
|
-
this.byLine = /* @__PURE__ */ new Map();
|
|
1981
|
-
this.count = 0;
|
|
1982
|
-
}
|
|
1983
|
-
static exprKey(expression) {
|
|
1984
|
-
return expression ?? "";
|
|
1985
|
-
}
|
|
1986
|
-
static displayKey(line, exprKey) {
|
|
1987
|
-
return exprKey === "" ? `${line}` : `${line}:${exprKey}`;
|
|
1988
|
-
}
|
|
1989
|
-
get(line, expression) {
|
|
1990
|
-
return this.byLine.get(line)?.get(_LineCache.exprKey(expression));
|
|
1991
|
-
}
|
|
1992
|
-
/** Find any cache entry for the given line number, regardless of expression suffix */
|
|
1993
|
-
getEntryForLine(line) {
|
|
1994
|
-
const entries = this.byLine.get(line);
|
|
1995
|
-
if (!entries) return void 0;
|
|
1996
|
-
for (const entry of entries.values()) {
|
|
1997
|
-
return entry;
|
|
1998
|
-
}
|
|
1999
|
-
return void 0;
|
|
2000
|
-
}
|
|
2001
|
-
set(line, entry, expression) {
|
|
2002
|
-
let entries = this.byLine.get(line);
|
|
2003
|
-
if (!entries) {
|
|
2004
|
-
entries = /* @__PURE__ */ new Map();
|
|
2005
|
-
this.byLine.set(line, entries);
|
|
2006
|
-
}
|
|
2007
|
-
const key = _LineCache.exprKey(expression);
|
|
2008
|
-
if (entries.size > 0 && !entries.has(key)) {
|
|
2009
|
-
this.count -= entries.size;
|
|
2010
|
-
entries.clear();
|
|
2011
|
-
}
|
|
2012
|
-
if (!entries.has(key)) this.count++;
|
|
2013
|
-
entries.set(key, entry);
|
|
2014
|
-
}
|
|
2015
|
-
has(line, expression) {
|
|
2016
|
-
return this.byLine.get(line)?.has(_LineCache.exprKey(expression)) ?? false;
|
|
2017
|
-
}
|
|
2018
|
-
remove(line, expression) {
|
|
2019
|
-
const entries = this.byLine.get(line);
|
|
2020
|
-
if (!entries) return;
|
|
2021
|
-
if (entries.delete(_LineCache.exprKey(expression))) {
|
|
2022
|
-
this.count--;
|
|
2023
|
-
if (entries.size === 0) this.byLine.delete(line);
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
removeAllForLine(line) {
|
|
2027
|
-
const entries = this.byLine.get(line);
|
|
2028
|
-
if (!entries) return;
|
|
2029
|
-
this.count -= entries.size;
|
|
2030
|
-
this.byLine.delete(line);
|
|
2031
|
-
}
|
|
2032
|
-
clearLine(line) {
|
|
2033
|
-
this.removeAllForLine(line);
|
|
2034
|
-
}
|
|
2035
|
-
/**
|
|
2036
|
-
* Number of entries in the cache. Useful for diagnostics.
|
|
2037
|
-
*/
|
|
2038
|
-
get size() {
|
|
2039
|
-
return this.count;
|
|
2040
|
-
}
|
|
2041
|
-
clear() {
|
|
2042
|
-
this.byLine.clear();
|
|
2043
|
-
this.count = 0;
|
|
1972
|
+
// src/api/EngineVersionCompatibility.ts
|
|
1973
|
+
var import_semver = __toESM(require_semver2(), 1);
|
|
1974
|
+
function checkEngineVersionCompatibility(pkg, engineVersion = ENGINE_VERSION) {
|
|
1975
|
+
if (!pkg.engineVersion) return { compatible: true };
|
|
1976
|
+
if ((0, import_semver.validRange)(pkg.engineVersion) === null) {
|
|
1977
|
+
return { compatible: false, reason: "invalid-range", declaredRange: pkg.engineVersion, engineVersion };
|
|
2044
1978
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
forEach(callback) {
|
|
2049
|
-
for (const [line, entries] of this.byLine) {
|
|
2050
|
-
for (const [exprKey, entry] of entries) {
|
|
2051
|
-
callback(_LineCache.displayKey(line, exprKey), entry);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
1979
|
+
const comparableVersion = (0, import_semver.coerce)(engineVersion)?.version ?? engineVersion;
|
|
1980
|
+
if (!(0, import_semver.satisfies)(comparableVersion, pkg.engineVersion)) {
|
|
1981
|
+
return { compatible: false, reason: "range-not-satisfied", declaredRange: pkg.engineVersion, engineVersion };
|
|
2054
1982
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
|
|
1983
|
+
return { compatible: true };
|
|
1984
|
+
}
|
|
1985
|
+
function assertEngineVersionCompatible(pkg, engineVersion = ENGINE_VERSION) {
|
|
1986
|
+
const result = checkEngineVersionCompatibility(pkg, engineVersion);
|
|
1987
|
+
if (result.compatible) return;
|
|
1988
|
+
if (result.reason === "invalid-range") {
|
|
1989
|
+
throw ErrorFactory.config({
|
|
1990
|
+
code: "PACKAGE_ENGINE_VERSION_INVALID_RANGE",
|
|
1991
|
+
message: `Package "${pkg.name}" declares an invalid engineVersion range: "${result.declaredRange}" is not a valid semver range.`,
|
|
1992
|
+
expected: `a valid semver range string (e.g. "^0.1.0", ">=0.2.0 <1.0.0")`,
|
|
1993
|
+
found: `"${result.declaredRange}"`,
|
|
1994
|
+
suggestion: `This is a range-syntax typo in "${pkg.name}"'s own descriptor, not a version mismatch with the running engine \u2014 check for a stray character.`,
|
|
1995
|
+
context: { packageName: pkg.name, declaredRange: result.declaredRange }
|
|
1996
|
+
});
|
|
2066
1997
|
}
|
|
2067
|
-
|
|
1998
|
+
throw ErrorFactory.config({
|
|
1999
|
+
code: "PACKAGE_ENGINE_VERSION_MISMATCH",
|
|
2000
|
+
message: `Package "${pkg.name}" declares engineVersion "${result.declaredRange}", which is not satisfied by the running engine version "${result.engineVersion}".`,
|
|
2001
|
+
expected: `an engine version satisfying "${result.declaredRange}"`,
|
|
2002
|
+
found: `engine version "${result.engineVersion}"`,
|
|
2003
|
+
suggestion: `Update "${pkg.name}" to a version built against solve-engine ${result.engineVersion}, or pin solve-engine to a version satisfying "${result.declaredRange}".`,
|
|
2004
|
+
context: { packageName: pkg.name, declaredRange: result.declaredRange, engineVersion: result.engineVersion }
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2068
2007
|
|
|
2069
2008
|
// src/api/PackageCompatibility.ts
|
|
2070
2009
|
function collectParseletConflicts(kind, fieldName, existingPkg, candidate, out) {
|
|
@@ -2200,858 +2139,364 @@ function checkPackageCompatibility(candidate, existing) {
|
|
|
2200
2139
|
};
|
|
2201
2140
|
}
|
|
2202
2141
|
|
|
2203
|
-
// src/
|
|
2204
|
-
var
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
if (
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
const comparableVersion = (0, import_semver.coerce)(engineVersion)?.version ?? engineVersion;
|
|
2211
|
-
if (!(0, import_semver.satisfies)(comparableVersion, pkg.engineVersion)) {
|
|
2212
|
-
return { compatible: false, reason: "range-not-satisfied", declaredRange: pkg.engineVersion, engineVersion };
|
|
2213
|
-
}
|
|
2214
|
-
return { compatible: true };
|
|
2142
|
+
// src/engine/EngineSnapshot.ts
|
|
2143
|
+
var SNAPSHOT_FORMAT = "solve-engine/snapshot";
|
|
2144
|
+
var SNAPSHOT_VERSION = 1;
|
|
2145
|
+
function encodeNumber(n) {
|
|
2146
|
+
if (Number.isFinite(n)) return n;
|
|
2147
|
+
if (Number.isNaN(n)) return "NaN";
|
|
2148
|
+
return n > 0 ? "Infinity" : "-Infinity";
|
|
2215
2149
|
}
|
|
2216
|
-
function
|
|
2217
|
-
|
|
2218
|
-
if (
|
|
2219
|
-
if (
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
context: { packageName: pkg.name, declaredRange: result.declaredRange }
|
|
2227
|
-
});
|
|
2228
|
-
}
|
|
2229
|
-
throw ErrorFactory.config({
|
|
2230
|
-
code: "PACKAGE_ENGINE_VERSION_MISMATCH",
|
|
2231
|
-
message: `Package "${pkg.name}" declares engineVersion "${result.declaredRange}", which is not satisfied by the running engine version "${result.engineVersion}".`,
|
|
2232
|
-
expected: `an engine version satisfying "${result.declaredRange}"`,
|
|
2233
|
-
found: `engine version "${result.engineVersion}"`,
|
|
2234
|
-
suggestion: `Update "${pkg.name}" to a version built against solve-engine ${result.engineVersion}, or pin solve-engine to a version satisfying "${result.declaredRange}".`,
|
|
2235
|
-
context: { packageName: pkg.name, declaredRange: result.declaredRange, engineVersion: result.engineVersion }
|
|
2150
|
+
function decodeNumber(n) {
|
|
2151
|
+
if (typeof n === "number") return n;
|
|
2152
|
+
if (n === "NaN") return NaN;
|
|
2153
|
+
if (n === "Infinity") return Infinity;
|
|
2154
|
+
if (n === "-Infinity") return -Infinity;
|
|
2155
|
+
throw ErrorFactory.validation({
|
|
2156
|
+
code: SnapshotErrorCodes.SNAPSHOT_MALFORMED,
|
|
2157
|
+
message: `Snapshot holds an unrecognised number sentinel: ${JSON.stringify(n)}`,
|
|
2158
|
+
expected: `a JSON number, or one of "NaN", "Infinity", "-Infinity"`,
|
|
2159
|
+
found: JSON.stringify(n)
|
|
2236
2160
|
});
|
|
2237
2161
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2162
|
+
var SnapshotErrorCodes = {
|
|
2163
|
+
/** The object handed to `fromJSON` is not a snapshot at all, or its version does not match this engine's reader. */
|
|
2164
|
+
SNAPSHOT_VERSION_MISMATCH: "SNAPSHOT_VERSION_MISMATCH",
|
|
2165
|
+
/** A snapshot with the right envelope but internally inconsistent contents (a bad number sentinel, a missing field). */
|
|
2166
|
+
SNAPSHOT_MALFORMED: "SNAPSHOT_MALFORMED",
|
|
2167
|
+
/** A value the snapshot format cannot yet represent (a symbolic expression, a symbolic matrix cell). Deferred, see the module doc. */
|
|
2168
|
+
SNAPSHOT_UNSUPPORTED_VALUE: "SNAPSHOT_UNSUPPORTED_VALUE"
|
|
2169
|
+
};
|
|
2170
|
+
function serializeDecimal(d) {
|
|
2171
|
+
return { coef: d.coef.toString(), scale: d.scale };
|
|
2172
|
+
}
|
|
2173
|
+
function deserializeDecimal(d) {
|
|
2174
|
+
return { coef: BigInt(d.coef), scale: d.scale };
|
|
2175
|
+
}
|
|
2176
|
+
function serializeRational(r) {
|
|
2177
|
+
return { n: r.n.toString(), d: r.d.toString() };
|
|
2178
|
+
}
|
|
2179
|
+
function deserializeRational(r) {
|
|
2180
|
+
return { n: BigInt(r.n), d: BigInt(r.d) };
|
|
2181
|
+
}
|
|
2182
|
+
function valueTypeName(type) {
|
|
2183
|
+
return ValueType[type] ?? String(type);
|
|
2184
|
+
}
|
|
2185
|
+
function unsupportedValue(type, where) {
|
|
2186
|
+
throw ErrorFactory.validation({
|
|
2187
|
+
code: SnapshotErrorCodes.SNAPSHOT_UNSUPPORTED_VALUE,
|
|
2188
|
+
message: `A ${valueTypeName(type)} value (${where}) cannot be included in a snapshot yet.`,
|
|
2189
|
+
expected: "a concrete number, string, boolean, unit, range, matrix, bigint, or money value",
|
|
2190
|
+
found: `${valueTypeName(type)} value`,
|
|
2191
|
+
suggestion: "Symbolic (algebra) values are deferred to a follow-up. Evaluate the document without the symbolic line, or re-derive it after restoring.",
|
|
2192
|
+
context: { valueType: type, location: where }
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
function serializeValue(value, where) {
|
|
2196
|
+
switch (value.type) {
|
|
2197
|
+
case 0 /* Number */: {
|
|
2198
|
+
const out = { t: 0 /* Number */, v: encodeNumber(value.value) };
|
|
2199
|
+
if (value.exact !== void 0) out.exact = serializeDecimal(value.exact);
|
|
2200
|
+
if (value.rational !== void 0) out.rational = serializeRational(value.rational);
|
|
2201
|
+
return out;
|
|
2202
|
+
}
|
|
2203
|
+
case 1 /* Hex */: {
|
|
2204
|
+
const raw = value.value;
|
|
2205
|
+
if (typeof raw === "bigint") {
|
|
2206
|
+
return { t: 1 /* Hex */, v: raw.toString(), big: true, base: value.unit };
|
|
2207
|
+
}
|
|
2208
|
+
return { t: 1 /* Hex */, v: encodeNumber(raw), base: value.unit };
|
|
2209
|
+
}
|
|
2210
|
+
case 2 /* BigInt */:
|
|
2211
|
+
return { t: 2 /* BigInt */, v: value.value.toString() };
|
|
2212
|
+
case 3 /* String */:
|
|
2213
|
+
return { t: 3 /* String */, v: value.value };
|
|
2214
|
+
case 4 /* Datetime */:
|
|
2215
|
+
return { t: 4 /* Datetime */, v: encodeNumber(value.value) };
|
|
2216
|
+
case 5 /* Percentage */:
|
|
2217
|
+
return { t: 5 /* Percentage */, v: encodeNumber(value.value) };
|
|
2218
|
+
case 6 /* Uom */: {
|
|
2219
|
+
const out = { t: 6 /* Uom */, v: encodeNumber(value.value), unit: value.unit ?? "" };
|
|
2220
|
+
if (value.exact !== void 0) out.exact = serializeDecimal(value.exact);
|
|
2221
|
+
return out;
|
|
2222
|
+
}
|
|
2223
|
+
case 7 /* Matrix */: {
|
|
2224
|
+
const m = value.value;
|
|
2225
|
+
if (m.hasSymbolic) unsupportedValue(9 /* Symbolic */, `${where} (symbolic matrix cell)`);
|
|
2226
|
+
const data = m.data.map((cell) => serializeMatrixCell(cell, where));
|
|
2227
|
+
return { t: 7 /* Matrix */, rows: m.rows, cols: m.cols, data };
|
|
2228
|
+
}
|
|
2229
|
+
case 8 /* Range */: {
|
|
2230
|
+
const r = value.value;
|
|
2231
|
+
return { t: 8 /* Range */, min: encodeNumber(r.min), max: encodeNumber(r.max) };
|
|
2232
|
+
}
|
|
2233
|
+
case 10 /* Boolean */:
|
|
2234
|
+
return { t: 10 /* Boolean */, v: value.value };
|
|
2235
|
+
case 13 /* Error */:
|
|
2236
|
+
return { t: 13 /* Error */, code: value.value, message: value.unit ?? "" };
|
|
2237
|
+
default:
|
|
2238
|
+
return unsupportedValue(value.type, where);
|
|
2247
2239
|
}
|
|
2248
|
-
return compileEngine;
|
|
2249
2240
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
if (
|
|
2253
|
-
|
|
2241
|
+
function serializeMatrixCell(cell, where) {
|
|
2242
|
+
if (typeof cell === "boolean") return cell;
|
|
2243
|
+
if (typeof cell === "number") return encodeNumber(cell);
|
|
2244
|
+
return unsupportedValue(9 /* Symbolic */, `${where} (symbolic matrix cell)`);
|
|
2245
|
+
}
|
|
2246
|
+
function deserializeValue(sv) {
|
|
2247
|
+
switch (sv.t) {
|
|
2248
|
+
case 0 /* Number */: {
|
|
2249
|
+
const v = new Value(0 /* Number */, decodeNumber(sv.v));
|
|
2250
|
+
if (sv.exact !== void 0) v.exact = deserializeDecimal(sv.exact);
|
|
2251
|
+
if (sv.rational !== void 0) v.rational = deserializeRational(sv.rational);
|
|
2252
|
+
return v;
|
|
2253
|
+
}
|
|
2254
|
+
case 1 /* Hex */: {
|
|
2255
|
+
const value = sv.big ? BigInt(sv.v) : decodeNumber(sv.v);
|
|
2256
|
+
return new Value(1 /* Hex */, value, sv.base);
|
|
2257
|
+
}
|
|
2258
|
+
case 2 /* BigInt */:
|
|
2259
|
+
return new Value(2 /* BigInt */, BigInt(sv.v));
|
|
2260
|
+
case 3 /* String */:
|
|
2261
|
+
return new Value(3 /* String */, sv.v);
|
|
2262
|
+
case 4 /* Datetime */:
|
|
2263
|
+
return new Value(4 /* Datetime */, decodeNumber(sv.v));
|
|
2264
|
+
case 5 /* Percentage */:
|
|
2265
|
+
return new Value(5 /* Percentage */, decodeNumber(sv.v));
|
|
2266
|
+
case 6 /* Uom */: {
|
|
2267
|
+
const v = new Value(6 /* Uom */, decodeNumber(sv.v), sv.unit);
|
|
2268
|
+
if (sv.exact !== void 0) v.exact = deserializeDecimal(sv.exact);
|
|
2269
|
+
return v;
|
|
2270
|
+
}
|
|
2271
|
+
case 7 /* Matrix */: {
|
|
2272
|
+
const data = sv.data.map((cell) => typeof cell === "boolean" ? cell : decodeNumber(cell));
|
|
2273
|
+
const m = { rows: sv.rows, cols: sv.cols, data, hasSymbolic: false };
|
|
2274
|
+
return new Value(7 /* Matrix */, m);
|
|
2275
|
+
}
|
|
2276
|
+
case 8 /* Range */:
|
|
2277
|
+
return new Value(8 /* Range */, { min: decodeNumber(sv.min), max: decodeNumber(sv.max) });
|
|
2278
|
+
case 10 /* Boolean */:
|
|
2279
|
+
return new Value(10 /* Boolean */, sv.v);
|
|
2280
|
+
case 13 /* Error */:
|
|
2281
|
+
return new Value(13 /* Error */, sv.code, sv.message);
|
|
2282
|
+
default:
|
|
2283
|
+
throw ErrorFactory.validation({
|
|
2284
|
+
code: SnapshotErrorCodes.SNAPSHOT_MALFORMED,
|
|
2285
|
+
message: `Snapshot holds a value with an unknown type tag: ${JSON.stringify(sv.t)}`,
|
|
2286
|
+
expected: "a known SerializedValue type tag",
|
|
2287
|
+
found: JSON.stringify(sv.t)
|
|
2288
|
+
});
|
|
2254
2289
|
}
|
|
2255
|
-
return executeVm;
|
|
2256
2290
|
}
|
|
2257
|
-
function
|
|
2258
|
-
const
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
reads: [],
|
|
2264
|
-
writes: [],
|
|
2265
|
-
isVariableDef: false,
|
|
2266
|
-
error: null
|
|
2291
|
+
function serializeBytecode(program) {
|
|
2292
|
+
const out = {
|
|
2293
|
+
opcodes: Array.from(program.opcodes),
|
|
2294
|
+
numbers: Array.from(program.numbers, encodeNumber),
|
|
2295
|
+
strings: program.strings.slice(),
|
|
2296
|
+
hasAsync: program.hasAsync
|
|
2267
2297
|
};
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
const opcodes = program.opcodes;
|
|
2273
|
-
const numbers = program.numbers;
|
|
2274
|
-
const opcodesBuffer = opcodes.buffer.slice(
|
|
2275
|
-
opcodes.byteOffset,
|
|
2276
|
-
opcodes.byteOffset + opcodes.byteLength
|
|
2277
|
-
);
|
|
2278
|
-
const numbersBuffer = numbers.buffer.slice(
|
|
2279
|
-
numbers.byteOffset,
|
|
2280
|
-
numbers.byteOffset + numbers.byteLength
|
|
2281
|
-
);
|
|
2282
|
-
return {
|
|
2283
|
-
...base,
|
|
2284
|
-
opcodesBuffer,
|
|
2285
|
-
numbersBuffer,
|
|
2286
|
-
opcodesLength: opcodes.length,
|
|
2287
|
-
numbersLength: numbers.length,
|
|
2288
|
-
strings: [...program.strings],
|
|
2289
|
-
reads,
|
|
2290
|
-
writes,
|
|
2291
|
-
isVariableDef
|
|
2292
|
-
};
|
|
2293
|
-
} catch (e) {
|
|
2294
|
-
return {
|
|
2295
|
-
...base,
|
|
2296
|
-
opcodesBuffer: new ArrayBuffer(0),
|
|
2297
|
-
numbersBuffer: new ArrayBuffer(0),
|
|
2298
|
-
opcodesLength: 0,
|
|
2299
|
-
numbersLength: 0,
|
|
2300
|
-
error: e instanceof Error ? e.message : String(e)
|
|
2301
|
-
};
|
|
2298
|
+
if (program.constants) out.constants = Array.from(program.constants.entries());
|
|
2299
|
+
if (program.userFunctionBodies) out.userFunctionBodies = program.userFunctionBodies.map(serializeUserFunction);
|
|
2300
|
+
if (program.anonymousBodies) {
|
|
2301
|
+
out.anonymousBodies = program.anonymousBodies.map((b) => ({ params: b.params.slice(), program: serializeBytecode(b.program) }));
|
|
2302
2302
|
}
|
|
2303
|
+
return out;
|
|
2303
2304
|
}
|
|
2304
|
-
function
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2305
|
+
function deserializeBytecode(sb) {
|
|
2306
|
+
const program = {
|
|
2307
|
+
opcodes: Uint8Array.from(sb.opcodes),
|
|
2308
|
+
numbers: Float64Array.from(sb.numbers, decodeNumber),
|
|
2309
|
+
strings: sb.strings.slice(),
|
|
2310
|
+
hasAsync: sb.hasAsync
|
|
2311
|
+
};
|
|
2312
|
+
if (sb.constants) program.constants = new Map(sb.constants);
|
|
2313
|
+
if (sb.userFunctionBodies) program.userFunctionBodies = sb.userFunctionBodies.map(deserializeUserFunction);
|
|
2314
|
+
if (sb.anonymousBodies) {
|
|
2315
|
+
program.anonymousBodies = sb.anonymousBodies.map((b) => ({ params: b.params.slice(), program: deserializeBytecode(b.program) }));
|
|
2312
2316
|
}
|
|
2313
|
-
|
|
2314
|
-
{ id: msg.id, type: "COMPILE_RESULT", results },
|
|
2315
|
-
transferList
|
|
2316
|
-
);
|
|
2317
|
+
return program;
|
|
2317
2318
|
}
|
|
2318
|
-
function
|
|
2319
|
-
|
|
2320
|
-
vmm.reset();
|
|
2321
|
-
const opcodes = new Uint8Array(item.opcodesBuffer, 0, item.opcodesLength);
|
|
2322
|
-
const numbers = new Float64Array(item.numbersBuffer, 0, item.numbersLength);
|
|
2323
|
-
const bytecode = { opcodes, numbers, strings: item.strings };
|
|
2324
|
-
try {
|
|
2325
|
-
const result = executeBytecode(bytecode, vmm);
|
|
2326
|
-
if (result.type === "pending") {
|
|
2327
|
-
return {
|
|
2328
|
-
lineNumber: item.lineNumber,
|
|
2329
|
-
valueType: 12 /* Pending */,
|
|
2330
|
-
value: 0,
|
|
2331
|
-
isPending: true,
|
|
2332
|
-
queryKey: result.queryKey
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
if (result.type === "error") {
|
|
2336
|
-
return {
|
|
2337
|
-
lineNumber: item.lineNumber,
|
|
2338
|
-
valueType: 13 /* Error */,
|
|
2339
|
-
value: 0,
|
|
2340
|
-
isPending: false,
|
|
2341
|
-
unit: result.error.message
|
|
2342
|
-
};
|
|
2343
|
-
}
|
|
2344
|
-
const val = result.value;
|
|
2345
|
-
return {
|
|
2346
|
-
lineNumber: item.lineNumber,
|
|
2347
|
-
valueType: val.type,
|
|
2348
|
-
value: typeof val.value === "number" ? val.value : typeof val.value === "bigint" ? Number(val.value) : 0,
|
|
2349
|
-
unit: val.unit,
|
|
2350
|
-
isPending: false
|
|
2351
|
-
};
|
|
2352
|
-
} catch {
|
|
2353
|
-
return {
|
|
2354
|
-
lineNumber: item.lineNumber,
|
|
2355
|
-
valueType: 13 /* Error */,
|
|
2356
|
-
value: 0,
|
|
2357
|
-
isPending: false,
|
|
2358
|
-
unit: "Worker execution failed"
|
|
2359
|
-
};
|
|
2360
|
-
}
|
|
2319
|
+
function serializeUserFunction(fn) {
|
|
2320
|
+
return { name: fn.name, params: fn.params.slice(), program: serializeBytecode(fn.program) };
|
|
2361
2321
|
}
|
|
2362
|
-
function
|
|
2363
|
-
|
|
2364
|
-
for (const item of msg.items) {
|
|
2365
|
-
results.push(executeOne(item));
|
|
2366
|
-
}
|
|
2367
|
-
self.postMessage({
|
|
2368
|
-
id: msg.id,
|
|
2369
|
-
type: "EXECUTE_RESULT",
|
|
2370
|
-
results
|
|
2371
|
-
});
|
|
2322
|
+
function deserializeUserFunction(fn) {
|
|
2323
|
+
return { name: fn.name, params: fn.params.slice(), program: deserializeBytecode(fn.program) };
|
|
2372
2324
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
case "EXECUTE_BATCH":
|
|
2382
|
-
handleExecuteBatch(msg);
|
|
2383
|
-
break;
|
|
2384
|
-
case "TERMINATE": {
|
|
2385
|
-
if (compileEngine) {
|
|
2386
|
-
compileEngine.clear();
|
|
2387
|
-
compileEngine = null;
|
|
2388
|
-
}
|
|
2389
|
-
if (executeVm) {
|
|
2390
|
-
executeVm.reset();
|
|
2391
|
-
executeVm = null;
|
|
2392
|
-
}
|
|
2393
|
-
self.postMessage({
|
|
2394
|
-
id: msg.id,
|
|
2395
|
-
type: "COMPILE_RESULT",
|
|
2396
|
-
results: []
|
|
2397
|
-
});
|
|
2398
|
-
break;
|
|
2399
|
-
}
|
|
2400
|
-
default: {
|
|
2401
|
-
self.postMessage({
|
|
2402
|
-
id: -1,
|
|
2403
|
-
type: "COMPILE_RESULT",
|
|
2404
|
-
results: [],
|
|
2405
|
-
error: `Unknown message type: ${msg.type}`
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2325
|
+
function assertRestorable(snapshot) {
|
|
2326
|
+
if (typeof snapshot !== "object" || snapshot === null) {
|
|
2327
|
+
throw ErrorFactory.validation({
|
|
2328
|
+
code: SnapshotErrorCodes.SNAPSHOT_VERSION_MISMATCH,
|
|
2329
|
+
message: "fromJSON was given something that is not a snapshot object.",
|
|
2330
|
+
expected: `an object produced by ExpressionEngine.toJSON() with format "${SNAPSHOT_FORMAT}"`,
|
|
2331
|
+
found: snapshot === null ? "null" : typeof snapshot
|
|
2332
|
+
});
|
|
2408
2333
|
}
|
|
2409
|
-
|
|
2410
|
-
if (
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
var ExecutionPool = class {
|
|
2418
|
-
constructor(poolSize) {
|
|
2419
|
-
this.workers = [];
|
|
2420
|
-
this.nextWorker = 0;
|
|
2421
|
-
this.nextId = 1;
|
|
2422
|
-
this.pendingBatches = /* @__PURE__ */ new Map();
|
|
2423
|
-
this.terminated = false;
|
|
2424
|
-
// ── Public API ────────────────────────────────────────────────────────
|
|
2425
|
-
/** Cached availability flag. Checked once; createExecutionWorker never called again. */
|
|
2426
|
-
this._available = null;
|
|
2427
|
-
this.poolSize = Math.min(poolSize ?? this.defaultPoolSize(), 4);
|
|
2334
|
+
const candidate = snapshot;
|
|
2335
|
+
if (candidate.format !== SNAPSHOT_FORMAT) {
|
|
2336
|
+
throw ErrorFactory.validation({
|
|
2337
|
+
code: SnapshotErrorCodes.SNAPSHOT_VERSION_MISMATCH,
|
|
2338
|
+
message: `This object is not a solve-engine snapshot (its "format" is ${JSON.stringify(candidate.format)}).`,
|
|
2339
|
+
expected: `format "${SNAPSHOT_FORMAT}"`,
|
|
2340
|
+
found: JSON.stringify(candidate.format)
|
|
2341
|
+
});
|
|
2428
2342
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2343
|
+
if (candidate.version !== SNAPSHOT_VERSION) {
|
|
2344
|
+
throw ErrorFactory.validation({
|
|
2345
|
+
code: SnapshotErrorCodes.SNAPSHOT_VERSION_MISMATCH,
|
|
2346
|
+
message: `This snapshot was written for format version ${candidate.version}, but this engine restores version ${SNAPSHOT_VERSION}.`,
|
|
2347
|
+
expected: `snapshot version ${SNAPSHOT_VERSION}`,
|
|
2348
|
+
found: `version ${candidate.version}`,
|
|
2349
|
+
suggestion: "Regenerate the snapshot with a matching engine version, or re-evaluate the document from source.",
|
|
2350
|
+
context: { snapshotVersion: candidate.version, readerVersion: SNAPSHOT_VERSION, engineVersion: candidate.engineVersion }
|
|
2351
|
+
});
|
|
2434
2352
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
this._available = false;
|
|
2445
|
-
return false;
|
|
2446
|
-
}
|
|
2447
|
-
try {
|
|
2448
|
-
const w = engine_worker_default();
|
|
2449
|
-
w.terminate();
|
|
2450
|
-
this._available = true;
|
|
2451
|
-
return true;
|
|
2452
|
-
} catch {
|
|
2453
|
-
this._available = false;
|
|
2454
|
-
return false;
|
|
2455
|
-
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// src/cache/LineCache.ts
|
|
2356
|
+
var LineCacheEntry = class {
|
|
2357
|
+
constructor(result, bytecode, readVariables, writeVariable) {
|
|
2358
|
+
this.result = result;
|
|
2359
|
+
this.bytecode = bytecode;
|
|
2360
|
+
this.readVariables = readVariables;
|
|
2361
|
+
this.writeVariable = writeVariable;
|
|
2456
2362
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
*
|
|
2464
|
-
* Falls back to undefined when workers are unavailable, caller should
|
|
2465
|
-
* use the main-thread path.
|
|
2466
|
-
*
|
|
2467
|
-
* @returns ExecuteResult[] on success, undefined if workers unavailable.
|
|
2468
|
-
*/
|
|
2469
|
-
executeBatch(orderedLineNumbers, entries) {
|
|
2470
|
-
if (!this.isAvailable()) return void 0;
|
|
2471
|
-
if (orderedLineNumbers.length === 0) return Promise.resolve([]);
|
|
2472
|
-
this.ensureWorkers();
|
|
2473
|
-
const items = [];
|
|
2474
|
-
for (const lineNumber of orderedLineNumbers) {
|
|
2475
|
-
const entry = entries.get(lineNumber);
|
|
2476
|
-
if (!entry || entry.bytecode.opcodes.length === 0) continue;
|
|
2477
|
-
const opcodes = entry.bytecode.opcodes;
|
|
2478
|
-
const numbers = entry.bytecode.numbers;
|
|
2479
|
-
const opcodesClone = opcodes.buffer.slice(
|
|
2480
|
-
opcodes.byteOffset,
|
|
2481
|
-
opcodes.byteOffset + opcodes.byteLength
|
|
2482
|
-
);
|
|
2483
|
-
const numbersClone = numbers.buffer.slice(
|
|
2484
|
-
numbers.byteOffset,
|
|
2485
|
-
numbers.byteOffset + numbers.byteLength
|
|
2486
|
-
);
|
|
2487
|
-
items.push({
|
|
2488
|
-
lineNumber,
|
|
2489
|
-
opcodesBuffer: opcodesClone,
|
|
2490
|
-
numbersBuffer: numbersClone,
|
|
2491
|
-
opcodesLength: opcodes.length,
|
|
2492
|
-
numbersLength: numbers.length,
|
|
2493
|
-
strings: [...entry.bytecode.strings]
|
|
2494
|
-
});
|
|
2495
|
-
}
|
|
2496
|
-
if (items.length === 0) return Promise.resolve([]);
|
|
2497
|
-
const transferList = [];
|
|
2498
|
-
for (const item of items) {
|
|
2499
|
-
if (item.opcodesBuffer.byteLength > 0) transferList.push(item.opcodesBuffer);
|
|
2500
|
-
if (item.numbersBuffer.byteLength > 0) transferList.push(item.numbersBuffer);
|
|
2501
|
-
}
|
|
2502
|
-
const batchId = this.nextId++;
|
|
2503
|
-
const worker = this.getNextWorker();
|
|
2504
|
-
return new Promise((resolve) => {
|
|
2505
|
-
const timeoutId = setTimeout(() => {
|
|
2506
|
-
this.pendingBatches.delete(batchId);
|
|
2507
|
-
resolve([]);
|
|
2508
|
-
}, WORKER_BATCH_TIMEOUT_MS);
|
|
2509
|
-
this.pendingBatches.set(batchId, {
|
|
2510
|
-
id: batchId,
|
|
2511
|
-
lineNumbers: orderedLineNumbers,
|
|
2512
|
-
resolve: (results) => {
|
|
2513
|
-
clearTimeout(timeoutId);
|
|
2514
|
-
resolve(results);
|
|
2515
|
-
}
|
|
2516
|
-
});
|
|
2517
|
-
worker.postMessage(
|
|
2518
|
-
{ type: "EXECUTE_BATCH", id: batchId, items },
|
|
2519
|
-
transferList
|
|
2520
|
-
);
|
|
2521
|
-
});
|
|
2363
|
+
};
|
|
2364
|
+
var LineCache = class _LineCache {
|
|
2365
|
+
constructor() {
|
|
2366
|
+
/** line number → (expression, or "" for expressionless entries) → entry */
|
|
2367
|
+
this.byLine = /* @__PURE__ */ new Map();
|
|
2368
|
+
this.count = 0;
|
|
2522
2369
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
* Workers are recreated lazily on next dispatch.
|
|
2526
|
-
*/
|
|
2527
|
-
clear() {
|
|
2528
|
-
for (const [, batch] of this.pendingBatches) {
|
|
2529
|
-
batch.resolve([]);
|
|
2530
|
-
}
|
|
2531
|
-
this.pendingBatches.clear();
|
|
2532
|
-
for (const w of this.workers) {
|
|
2533
|
-
w.terminate();
|
|
2534
|
-
}
|
|
2535
|
-
this.workers = [];
|
|
2536
|
-
this.nextWorker = 0;
|
|
2370
|
+
static exprKey(expression) {
|
|
2371
|
+
return expression ?? "";
|
|
2537
2372
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
*/
|
|
2541
|
-
destroy() {
|
|
2542
|
-
this.terminated = true;
|
|
2543
|
-
this.clear();
|
|
2373
|
+
static displayKey(line, exprKey) {
|
|
2374
|
+
return exprKey === "" ? `${line}` : `${line}:${exprKey}`;
|
|
2544
2375
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
console.error(`[ExecutionPool] Worker ${i} error:`, err.message);
|
|
2555
|
-
for (const [id, batch] of this.pendingBatches) {
|
|
2556
|
-
batch.resolve([]);
|
|
2557
|
-
this.pendingBatches.delete(id);
|
|
2558
|
-
}
|
|
2559
|
-
};
|
|
2560
|
-
this.workers.push(worker);
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
getNextWorker() {
|
|
2564
|
-
const w = this.workers[this.nextWorker];
|
|
2565
|
-
this.nextWorker = (this.nextWorker + 1) % this.workers.length;
|
|
2566
|
-
return w;
|
|
2567
|
-
}
|
|
2568
|
-
handleWorkerMessage(data) {
|
|
2569
|
-
if (data.type !== "EXECUTE_RESULT") return;
|
|
2570
|
-
const batch = this.pendingBatches.get(data.id);
|
|
2571
|
-
if (!batch) return;
|
|
2572
|
-
this.pendingBatches.delete(data.id);
|
|
2573
|
-
batch.resolve(data.results);
|
|
2574
|
-
}
|
|
2575
|
-
};
|
|
2576
|
-
function reconstructValue(result) {
|
|
2577
|
-
switch (result.valueType) {
|
|
2578
|
-
case 0 /* Number */:
|
|
2579
|
-
return numberValue(result.value);
|
|
2580
|
-
case 1 /* Hex */:
|
|
2581
|
-
return hexValue(result.value);
|
|
2582
|
-
case 2 /* BigInt */:
|
|
2583
|
-
return bigIntValue(BigInt(result.value));
|
|
2584
|
-
case 3 /* String */:
|
|
2585
|
-
return stringValue(String(result.value));
|
|
2586
|
-
case 4 /* Datetime */:
|
|
2587
|
-
return datetimeValue(result.value);
|
|
2588
|
-
case 5 /* Percentage */:
|
|
2589
|
-
return percentageValue(result.value);
|
|
2590
|
-
case 6 /* Uom */:
|
|
2591
|
-
return uomValue(result.value, result.unit ?? "");
|
|
2592
|
-
case 7 /* Matrix */:
|
|
2593
|
-
return numberValue(result.value);
|
|
2594
|
-
case 10 /* Boolean */:
|
|
2595
|
-
return boolValue(result.value !== 0);
|
|
2596
|
-
case 12 /* Pending */:
|
|
2597
|
-
return pendingValue(result.queryKey ?? "");
|
|
2598
|
-
case 13 /* Error */:
|
|
2599
|
-
return errorValue("WORKER_EXECUTION_ERROR", result.unit ?? "Unknown worker error");
|
|
2600
|
-
default:
|
|
2601
|
-
return numberValue(result.value);
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
// src/engine/AsyncResolutionBatcher.ts
|
|
2606
|
-
var _AsyncResolutionBatcher = class _AsyncResolutionBatcher {
|
|
2607
|
-
constructor(dag, lineCache, vm, highWaterMark = _AsyncResolutionBatcher.DEFAULT_HIGH_WATER_MARK) {
|
|
2608
|
-
this.pending = [];
|
|
2609
|
-
this.scheduled = false;
|
|
2610
|
-
/** Set to true by clearPending(), flush() checks this to abort stale work. */
|
|
2611
|
-
this.cleared = false;
|
|
2612
|
-
/**
|
|
2613
|
-
* Worker pool for offloading VM re-execution when the affected line
|
|
2614
|
-
* count exceeds WORKER_OFFLOAD_THRESHOLD (50). Lazily created on
|
|
2615
|
-
* first dispatch; cleared on clearAll().
|
|
2616
|
-
*/
|
|
2617
|
-
this.executionPool = null;
|
|
2618
|
-
/**
|
|
2619
|
-
* Number of flushes actually dispatched to the worker pool (as opposed
|
|
2620
|
-
* to falling back to the main thread because Worker is unavailable).
|
|
2621
|
-
* Exposed via {@link workerOffloadCount} for the Workers diagnostic tab.
|
|
2622
|
-
*/
|
|
2623
|
-
this.workerOffloadDispatchCount = 0;
|
|
2624
|
-
/**
|
|
2625
|
-
* Controller for the internal event stream. Set during stream
|
|
2626
|
-
* initialization; cleared on stream cancellation or clearAll().
|
|
2627
|
-
*/
|
|
2628
|
-
this._streamController = null;
|
|
2629
|
-
/**
|
|
2630
|
-
* Test-only synchronous capture array. When enabled (non-null), every
|
|
2631
|
-
* event is synchronously pushed here in addition to the stream.
|
|
2632
|
-
* Tests read from this array to avoid async stream reader timing issues.
|
|
2633
|
-
*/
|
|
2634
|
-
this._testCaptures = null;
|
|
2635
|
-
/**
|
|
2636
|
-
* Called for each line whose result is patched after an async resolution,
|
|
2637
|
-
* on both the main-thread and worker-pool paths.
|
|
2638
|
-
*
|
|
2639
|
-
* **A host that displays async results must set this.** It is the only
|
|
2640
|
-
* mechanism that moves a resolved value out of the LineCache and into the
|
|
2641
|
-
* host's own document state. The engine cannot do it itself: it does not own
|
|
2642
|
-
* a document, the host does, and the batcher has no reference to one.
|
|
2643
|
-
*
|
|
2644
|
-
* Nullable rather than a constructor parameter because it is cleared by
|
|
2645
|
-
* `clearAll()` and re-wired on re-subscribe, so it cannot be readonly. That
|
|
2646
|
-
* makes it easy to miss, which is why {@link warnIfUnwired} exists: leaving
|
|
2647
|
-
* it unset means async values resolve into the cache and are never shown,
|
|
2648
|
-
* with nothing to indicate why. A host that genuinely does not want async
|
|
2649
|
-
* results should not register async resolvers at all.
|
|
2650
|
-
*/
|
|
2651
|
-
this.onLineResult = null;
|
|
2652
|
-
/**
|
|
2653
|
-
* Whether {@link warnIfUnwired} has already fired.
|
|
2654
|
-
*
|
|
2655
|
-
* Once per instance, not once per resolution. A document with fifty live
|
|
2656
|
-
* data lines would otherwise produce fifty identical warnings on the first
|
|
2657
|
-
* fetch, which is how a useful warning becomes noise people filter out.
|
|
2658
|
-
*/
|
|
2659
|
-
this.warnedAboutMissingHook = false;
|
|
2660
|
-
this.dag = dag;
|
|
2661
|
-
this.lineCache = lineCache;
|
|
2662
|
-
this.vm = vm;
|
|
2663
|
-
this.highWaterMark = highWaterMark;
|
|
2664
|
-
this._eventStream = this.createEventStream();
|
|
2665
|
-
}
|
|
2666
|
-
/**
|
|
2667
|
-
* Warn once if an async result resolved with no {@link onLineResult} wired.
|
|
2668
|
-
*
|
|
2669
|
-
* The failure this catches is silent by nature: the value arrives, the cache
|
|
2670
|
-
* updates, and the line keeps showing pending forever. Without this a host
|
|
2671
|
-
* author has no thread to pull on.
|
|
2672
|
-
*/
|
|
2673
|
-
warnIfUnwired() {
|
|
2674
|
-
if (this.onLineResult || this.warnedAboutMissingHook) return;
|
|
2675
|
-
this.warnedAboutMissingHook = true;
|
|
2676
|
-
console.warn(
|
|
2677
|
-
"[solve-engine] An async result resolved but AsyncResolutionBatcher.onLineResult is not set, so the value cannot reach your document and the line will keep showing as pending. Set it to mirror resolved values into your own state. This warning appears once per batcher."
|
|
2678
|
-
);
|
|
2679
|
-
}
|
|
2680
|
-
/**
|
|
2681
|
-
* Create a fresh internal event stream and wire its controller.
|
|
2682
|
-
* Called from the constructor and again from clearAll() so the batcher
|
|
2683
|
-
* keeps emitting events after an engine clear, the engine instance
|
|
2684
|
-
* (and this batcher) live on across clear() calls.
|
|
2685
|
-
*/
|
|
2686
|
-
createEventStream() {
|
|
2687
|
-
return new ReadableStream({
|
|
2688
|
-
start: (controller) => {
|
|
2689
|
-
this._streamController = controller;
|
|
2690
|
-
},
|
|
2691
|
-
cancel: () => {
|
|
2692
|
-
this._streamController = null;
|
|
2693
|
-
}
|
|
2694
|
-
}, new CountQueuingStrategy({ highWaterMark: this.highWaterMark }));
|
|
2695
|
-
}
|
|
2696
|
-
// ── Public API ────────────────────────────────────────────────────
|
|
2697
|
-
/**
|
|
2698
|
-
* Add a resolved query key to the pending batch.
|
|
2699
|
-
*
|
|
2700
|
-
* Called by ExpressionEngine.resolveAsync() after a promise resolves or errors.
|
|
2701
|
-
* If this is the first entry in the current tick, schedules a microtask flush.
|
|
2702
|
-
*/
|
|
2703
|
-
add(entry) {
|
|
2704
|
-
this.cleared = false;
|
|
2705
|
-
for (const existing of this.pending) {
|
|
2706
|
-
if (existing.packageId === entry.packageId && existing.queryKey === entry.queryKey) {
|
|
2707
|
-
return;
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
this.pending.push(entry);
|
|
2711
|
-
if (!this.scheduled) {
|
|
2712
|
-
this.scheduled = true;
|
|
2713
|
-
queueMicrotask(() => {
|
|
2714
|
-
try {
|
|
2715
|
-
this.flush();
|
|
2716
|
-
} catch (e) {
|
|
2717
|
-
const engineError = normalizeUnknownError(e);
|
|
2718
|
-
console.error(`[AsyncResolutionBatcher] flush() failed unexpectedly \u2014 this should never happen; please report: ${engineError.format()}`);
|
|
2719
|
-
}
|
|
2720
|
-
});
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
/**
|
|
2724
|
-
* Get the native event stream for stream-based consumers.
|
|
2725
|
-
*
|
|
2726
|
-
* Use this for backpressure, cancellation, or the ability
|
|
2727
|
-
* to `pipeTo()` / `pipeThrough()` the event flow.
|
|
2728
|
-
*
|
|
2729
|
-
* @returns A {@link ReadableStream} that emits {@link AsyncResolutionEvent}
|
|
2730
|
-
* items as the batcher processes async resolutions.
|
|
2731
|
-
*/
|
|
2732
|
-
getEventStream() {
|
|
2733
|
-
return this._eventStream;
|
|
2734
|
-
}
|
|
2735
|
-
/** Number of resolutions currently queued for the next flush. */
|
|
2736
|
-
get pendingCount() {
|
|
2737
|
-
return this.pending.length;
|
|
2738
|
-
}
|
|
2739
|
-
/** Number of pending entries collapsed by (packageId, queryKey) deduplication. */
|
|
2740
|
-
get dedupCount() {
|
|
2741
|
-
const dedup = /* @__PURE__ */ new Set();
|
|
2742
|
-
for (const entry of this.pending) {
|
|
2743
|
-
dedup.add(`${entry.packageId}:${entry.queryKey}`);
|
|
2744
|
-
}
|
|
2745
|
-
return Math.max(0, this.pending.length - dedup.size);
|
|
2746
|
-
}
|
|
2747
|
-
/**
|
|
2748
|
-
* Whether the internal event stream currently has an active reader.
|
|
2749
|
-
* `1` if a consumer has called `getEventStream().getReader()` (or
|
|
2750
|
-
* otherwise locked the stream) and not released it, `0` otherwise.
|
|
2751
|
-
*/
|
|
2752
|
-
get listenerCount() {
|
|
2753
|
-
return this._eventStream.locked ? 1 : 0;
|
|
2754
|
-
}
|
|
2755
|
-
/** Number of flushes that were actually dispatched to the worker pool. */
|
|
2756
|
-
get workerOffloadCount() {
|
|
2757
|
-
return this.workerOffloadDispatchCount;
|
|
2758
|
-
}
|
|
2759
|
-
/** Remove all listeners and cancel pending batch. Called on engine clear. */
|
|
2760
|
-
clearAll() {
|
|
2761
|
-
this.pending = [];
|
|
2762
|
-
this.scheduled = false;
|
|
2763
|
-
this.cleared = true;
|
|
2764
|
-
this._testCaptures = null;
|
|
2765
|
-
this.onLineResult = null;
|
|
2766
|
-
try {
|
|
2767
|
-
this._streamController?.close();
|
|
2768
|
-
} catch {
|
|
2769
|
-
}
|
|
2770
|
-
this._streamController = null;
|
|
2771
|
-
this._eventStream = this.createEventStream();
|
|
2772
|
-
if (this.executionPool) {
|
|
2773
|
-
this.executionPool.clear();
|
|
2774
|
-
this.executionPool = null;
|
|
2775
|
-
}
|
|
2776
|
-
this.workerOffloadDispatchCount = 0;
|
|
2777
|
-
}
|
|
2778
|
-
// ── Private: flush ────────────────────────────────────────────────
|
|
2779
|
-
/**
|
|
2780
|
-
* Flush all pending resolutions in a single batched pass.
|
|
2781
|
-
*
|
|
2782
|
-
* Called automatically via queueMicrotask. Never called directly.
|
|
2783
|
-
*/
|
|
2784
|
-
flush() {
|
|
2785
|
-
this.scheduled = false;
|
|
2786
|
-
if (this.cleared) return;
|
|
2787
|
-
if (this.pending.length === 0) return;
|
|
2788
|
-
const batch = this.pending;
|
|
2789
|
-
this.pending = [];
|
|
2790
|
-
const deduped = /* @__PURE__ */ new Map();
|
|
2791
|
-
for (const entry of batch) {
|
|
2792
|
-
const compositeKey = `${entry.packageId}:${entry.queryKey}`;
|
|
2793
|
-
deduped.set(compositeKey, entry);
|
|
2794
|
-
}
|
|
2795
|
-
const errorEntries = [];
|
|
2796
|
-
const okEntries = [];
|
|
2797
|
-
for (const entry of deduped.values()) {
|
|
2798
|
-
if (entry.isError) {
|
|
2799
|
-
errorEntries.push(entry);
|
|
2800
|
-
} else {
|
|
2801
|
-
okEntries.push(entry);
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
for (const entry of errorEntries) {
|
|
2805
|
-
if (entry.signal.aborted) continue;
|
|
2806
|
-
this.notifyListeners({
|
|
2807
|
-
type: "error",
|
|
2808
|
-
queryKey: entry.queryKey,
|
|
2809
|
-
packageId: entry.packageId,
|
|
2810
|
-
error: entry.error ?? new Error("Unknown async resolution error")
|
|
2811
|
-
});
|
|
2812
|
-
}
|
|
2813
|
-
const allAffected = /* @__PURE__ */ new Set();
|
|
2814
|
-
const allQueryKeys = [];
|
|
2815
|
-
const allEntries = [...okEntries, ...errorEntries];
|
|
2816
|
-
for (const entry of allEntries) {
|
|
2817
|
-
if (entry.signal.aborted) continue;
|
|
2818
|
-
allQueryKeys.push(entry.queryKey);
|
|
2819
|
-
const affected = this.dag.getAffectedLinesByDataSource(
|
|
2820
|
-
entry.packageId,
|
|
2821
|
-
[entry.queryKey]
|
|
2822
|
-
);
|
|
2823
|
-
for (const line of affected) {
|
|
2824
|
-
allAffected.add(line);
|
|
2825
|
-
}
|
|
2376
|
+
get(line, expression) {
|
|
2377
|
+
return this.byLine.get(line)?.get(_LineCache.exprKey(expression));
|
|
2378
|
+
}
|
|
2379
|
+
/** Find any cache entry for the given line number, regardless of expression suffix */
|
|
2380
|
+
getEntryForLine(line) {
|
|
2381
|
+
const entries = this.byLine.get(line);
|
|
2382
|
+
if (!entries) return void 0;
|
|
2383
|
+
for (const entry of entries.values()) {
|
|
2384
|
+
return entry;
|
|
2826
2385
|
}
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2386
|
+
return void 0;
|
|
2387
|
+
}
|
|
2388
|
+
set(line, entry, expression) {
|
|
2389
|
+
let entries = this.byLine.get(line);
|
|
2390
|
+
if (!entries) {
|
|
2391
|
+
entries = /* @__PURE__ */ new Map();
|
|
2392
|
+
this.byLine.set(line, entries);
|
|
2834
2393
|
}
|
|
2835
|
-
const
|
|
2836
|
-
if (
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
entryMap.set(lineNumber, this.lineCache.getEntryForLine(lineNumber));
|
|
2840
|
-
}
|
|
2841
|
-
void this.reExecuteViaWorkerPool(ordered, entryMap, allQueryKeys).catch((e) => {
|
|
2842
|
-
const engineError = normalizeUnknownError(e);
|
|
2843
|
-
console.error(`[AsyncResolutionBatcher] reExecuteViaWorkerPool() failed unexpectedly: ${engineError.format()}`);
|
|
2844
|
-
});
|
|
2845
|
-
return;
|
|
2394
|
+
const key = _LineCache.exprKey(expression);
|
|
2395
|
+
if (entries.size > 0 && !entries.has(key)) {
|
|
2396
|
+
this.count -= entries.size;
|
|
2397
|
+
entries.clear();
|
|
2846
2398
|
}
|
|
2847
|
-
|
|
2399
|
+
if (!entries.has(key)) this.count++;
|
|
2400
|
+
entries.set(key, entry);
|
|
2401
|
+
}
|
|
2402
|
+
has(line, expression) {
|
|
2403
|
+
return this.byLine.get(line)?.has(_LineCache.exprKey(expression)) ?? false;
|
|
2404
|
+
}
|
|
2405
|
+
remove(line, expression) {
|
|
2406
|
+
const entries = this.byLine.get(line);
|
|
2407
|
+
if (!entries) return;
|
|
2408
|
+
if (entries.delete(_LineCache.exprKey(expression))) {
|
|
2409
|
+
this.count--;
|
|
2410
|
+
if (entries.size === 0) this.byLine.delete(line);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
removeAllForLine(line) {
|
|
2414
|
+
const entries = this.byLine.get(line);
|
|
2415
|
+
if (!entries) return;
|
|
2416
|
+
this.count -= entries.size;
|
|
2417
|
+
this.byLine.delete(line);
|
|
2418
|
+
}
|
|
2419
|
+
clearLine(line) {
|
|
2420
|
+
this.removeAllForLine(line);
|
|
2848
2421
|
}
|
|
2849
|
-
// ── Private: topological sort ─────────────────────────────────────
|
|
2850
2422
|
/**
|
|
2851
|
-
*
|
|
2852
|
-
*
|
|
2853
|
-
* Lines that produce variables come before lines that consume them.
|
|
2854
|
-
* This ensures correct evaluation order when multiple interdependent
|
|
2855
|
-
* lines are affected by async resolution.
|
|
2423
|
+
* Number of entries in the cache. Useful for diagnostics.
|
|
2856
2424
|
*/
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
inDegree.set(line, 0);
|
|
2864
|
-
adjacency.set(line, []);
|
|
2865
|
-
}
|
|
2866
|
-
for (const line of lines) {
|
|
2867
|
-
const writes = this.dag.getWrites(line);
|
|
2868
|
-
for (const w of writes) {
|
|
2869
|
-
producerOf.set(w, line);
|
|
2870
|
-
}
|
|
2871
|
-
}
|
|
2872
|
-
for (const line of lines) {
|
|
2873
|
-
const reads = this.dag.getDependencies(line);
|
|
2874
|
-
for (const readVar of reads) {
|
|
2875
|
-
const producer = producerOf.get(readVar);
|
|
2876
|
-
if (producer !== void 0 && producer !== line) {
|
|
2877
|
-
adjacency.get(producer).push(line);
|
|
2878
|
-
inDegree.set(line, (inDegree.get(line) ?? 0) + 1);
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
const queue = [];
|
|
2883
|
-
for (const [line, degree] of inDegree) {
|
|
2884
|
-
if (degree === 0) queue.push(line);
|
|
2885
|
-
}
|
|
2886
|
-
if (queue.length === 0 && lines.length > 0) {
|
|
2887
|
-
return [...lines].sort((a, b) => a - b);
|
|
2888
|
-
}
|
|
2889
|
-
const ordered = [];
|
|
2890
|
-
while (queue.length > 0) {
|
|
2891
|
-
const current = queue.shift();
|
|
2892
|
-
ordered.push(current);
|
|
2893
|
-
for (const downstream of adjacency.get(current) ?? []) {
|
|
2894
|
-
const newDegree = (inDegree.get(downstream) ?? 1) - 1;
|
|
2895
|
-
inDegree.set(downstream, newDegree);
|
|
2896
|
-
if (newDegree === 0) queue.push(downstream);
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
if (ordered.length < lines.length) {
|
|
2900
|
-
const remaining = lines.filter((l) => !ordered.includes(l)).sort((a, b) => a - b);
|
|
2901
|
-
ordered.push(...remaining);
|
|
2902
|
-
}
|
|
2903
|
-
return ordered;
|
|
2425
|
+
get size() {
|
|
2426
|
+
return this.count;
|
|
2427
|
+
}
|
|
2428
|
+
clear() {
|
|
2429
|
+
this.byLine.clear();
|
|
2430
|
+
this.count = 0;
|
|
2904
2431
|
}
|
|
2905
|
-
// ── Private: worker-pool re-execution ─────────────────────────────
|
|
2906
2432
|
/**
|
|
2907
|
-
*
|
|
2908
|
-
*
|
|
2909
|
-
* Called when ordered.length > WORKER_OFFLOAD_THRESHOLD (50).
|
|
2910
|
-
* Clones bytecode ArrayBuffers, dispatches to workers, and asynchronously
|
|
2911
|
-
* patches results back into LineCache before notifying listeners.
|
|
2912
|
-
*
|
|
2913
|
-
* Handles pending results: lines that return { type: 'pending' } from the
|
|
2914
|
-
* worker are NOT marked as updated, the engine's resolveAsync will handle
|
|
2915
|
-
* them when the async resolver completes.
|
|
2916
|
-
*
|
|
2917
|
-
* Safety: checks this.cleared before applying results, if the engine was
|
|
2918
|
-
* cleared while the worker batch was in-flight, results are discarded.
|
|
2433
|
+
* Iterate all cache entries for diagnostics/debugging.
|
|
2919
2434
|
*/
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
const results = this.executionPool.executeBatch(ordered, entryMap);
|
|
2925
|
-
if (!results) {
|
|
2926
|
-
this.reExecuteMainThread(ordered, allQueryKeys, entryMap);
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
2929
|
-
this.workerOffloadDispatchCount++;
|
|
2930
|
-
const workerResults = await results;
|
|
2931
|
-
if (this.cleared) return;
|
|
2932
|
-
const updatedLineNumbers = [];
|
|
2933
|
-
for (const wr of workerResults) {
|
|
2934
|
-
const entry = entryMap.get(wr.lineNumber);
|
|
2935
|
-
if (!entry) continue;
|
|
2936
|
-
if (wr.isPending) {
|
|
2937
|
-
continue;
|
|
2435
|
+
forEach(callback) {
|
|
2436
|
+
for (const [line, entries] of this.byLine) {
|
|
2437
|
+
for (const [exprKey, entry] of entries) {
|
|
2438
|
+
callback(_LineCache.displayKey(line, exprKey), entry);
|
|
2938
2439
|
}
|
|
2939
|
-
const value = reconstructValue(wr);
|
|
2940
|
-
entry.result = value;
|
|
2941
|
-
this.warnIfUnwired();
|
|
2942
|
-
this.onLineResult?.(wr.lineNumber, value);
|
|
2943
|
-
updatedLineNumbers.push(wr.lineNumber);
|
|
2944
|
-
}
|
|
2945
|
-
if (this.cleared) return;
|
|
2946
|
-
if (updatedLineNumbers.length > 0 || allQueryKeys.length > 0) {
|
|
2947
|
-
this.notifyListeners({
|
|
2948
|
-
type: "lines-updated",
|
|
2949
|
-
lineNumbers: updatedLineNumbers,
|
|
2950
|
-
affectedQueryKeys: allQueryKeys
|
|
2951
|
-
});
|
|
2952
2440
|
}
|
|
2953
2441
|
}
|
|
2954
|
-
// ── Private: main-thread re-execution ────────────────────────────
|
|
2955
2442
|
/**
|
|
2956
|
-
*
|
|
2957
|
-
*
|
|
2958
|
-
*
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
*
|
|
2962
|
-
* **Per-line containment (fatal-bug fix)**: `executeBytecode()` used to
|
|
2963
|
-
* run here with NO try/catch anywhere in this method's call chain, and
|
|
2964
|
-
* this whole batch runs inside a bare `queueMicrotask` (see `add()`) with
|
|
2965
|
-
* no surrounding try/catch at any caller either, so if any ONE line's
|
|
2966
|
-
* cached bytecode threw (a stack/instruction-limit error, an undefined
|
|
2967
|
-
* variable, a corrupted-bytecode `TypeError`), the `for` loop aborted
|
|
2968
|
-
* immediately: every line scheduled AFTER the failure in this batch was
|
|
2969
|
-
* silently never re-executed or notified even though nothing was wrong
|
|
2970
|
-
* with them, every line BEFORE it had already had its `entry.result`
|
|
2971
|
-
* mutated in-place but `notifyListeners()` was never reached (a silent
|
|
2972
|
-
* `LineCache`/host desync), and, because a bare `queueMicrotask`
|
|
2973
|
-
* callback has no caller to catch it, the exception was uncatchable:
|
|
2974
|
-
* an `uncaughtException` that could crash the host process outright.
|
|
2975
|
-
* (`__tests__/async/AsyncResolutionBatcher.spec.ts`'s topological-sort
|
|
2976
|
-
* describe block used to have a test skipped specifically because of
|
|
2977
|
-
* this. See that file, now un-skipped and rewritten.) Each line's
|
|
2978
|
-
* execution is now its own try/catch: a failure is recorded as an
|
|
2979
|
-
* `Error` `Value` for THAT line (still counted as "updated" so the host
|
|
2980
|
-
* learns about it and stops showing a stale Pending state) and the loop
|
|
2981
|
-
* continues, one line's failure can no longer take out its neighbors.
|
|
2443
|
+
* Structured iteration for serialization: one record per stored entry,
|
|
2444
|
+
* carrying the raw line number and expression key rather than the lossy
|
|
2445
|
+
* "line:expression" display key `keys()`/`forEach()` produce (an expression
|
|
2446
|
+
* containing a colon cannot be split back apart from that string). Used by
|
|
2447
|
+
* the engine snapshot to walk every entry without re-parsing display keys.
|
|
2982
2448
|
*/
|
|
2983
|
-
|
|
2984
|
-
const
|
|
2985
|
-
for (const
|
|
2986
|
-
const entry
|
|
2987
|
-
|
|
2988
|
-
const stackBefore = this.vm.getStack().length;
|
|
2989
|
-
try {
|
|
2990
|
-
const result = executeBytecode(entry.bytecode, this.vm);
|
|
2991
|
-
while (this.vm.getStack().length > stackBefore) {
|
|
2992
|
-
this.vm.pop();
|
|
2993
|
-
}
|
|
2994
|
-
if (result.type === "value") {
|
|
2995
|
-
entry.result = result.value;
|
|
2996
|
-
this.warnIfUnwired();
|
|
2997
|
-
this.onLineResult?.(lineNumber, result.value);
|
|
2998
|
-
updatedLineNumbers.push(lineNumber);
|
|
2999
|
-
} else if (result.type === "error") {
|
|
3000
|
-
const value = errorValue(result.error.code, result.error.message);
|
|
3001
|
-
entry.result = value;
|
|
3002
|
-
this.warnIfUnwired();
|
|
3003
|
-
this.warnIfUnwired();
|
|
3004
|
-
this.onLineResult?.(lineNumber, value);
|
|
3005
|
-
updatedLineNumbers.push(lineNumber);
|
|
3006
|
-
}
|
|
3007
|
-
} catch (e) {
|
|
3008
|
-
while (this.vm.getStack().length > stackBefore) {
|
|
3009
|
-
this.vm.pop();
|
|
3010
|
-
}
|
|
3011
|
-
const engineError = normalizeUnknownError(e);
|
|
3012
|
-
const value = errorValue(engineError.code, engineError.message);
|
|
3013
|
-
entry.result = value;
|
|
3014
|
-
this.warnIfUnwired();
|
|
3015
|
-
this.onLineResult?.(lineNumber, value);
|
|
3016
|
-
updatedLineNumbers.push(lineNumber);
|
|
2449
|
+
snapshotEntries() {
|
|
2450
|
+
const out = [];
|
|
2451
|
+
for (const [line, entries] of this.byLine) {
|
|
2452
|
+
for (const [exprKey, entry] of entries) {
|
|
2453
|
+
out.push({ line, expression: exprKey, entry });
|
|
3017
2454
|
}
|
|
3018
2455
|
}
|
|
3019
|
-
|
|
3020
|
-
this.notifyListeners({
|
|
3021
|
-
type: "lines-updated",
|
|
3022
|
-
lineNumbers: updatedLineNumbers,
|
|
3023
|
-
affectedQueryKeys: allQueryKeys
|
|
3024
|
-
});
|
|
3025
|
-
}
|
|
3026
|
-
return updatedLineNumbers;
|
|
2456
|
+
return out;
|
|
3027
2457
|
}
|
|
3028
2458
|
/**
|
|
3029
|
-
*
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
if (this._testCaptures) {
|
|
3037
|
-
this._testCaptures.push(event);
|
|
3038
|
-
}
|
|
3039
|
-
if (this._streamController) {
|
|
3040
|
-
try {
|
|
3041
|
-
this._streamController.enqueue(event);
|
|
3042
|
-
} catch {
|
|
2459
|
+
* Get all entry keys.
|
|
2460
|
+
*/
|
|
2461
|
+
keys() {
|
|
2462
|
+
const result = [];
|
|
2463
|
+
for (const [line, entries] of this.byLine) {
|
|
2464
|
+
for (const exprKey of entries.keys()) {
|
|
2465
|
+
result.push(_LineCache.displayKey(line, exprKey));
|
|
3043
2466
|
}
|
|
3044
2467
|
}
|
|
2468
|
+
return result;
|
|
3045
2469
|
}
|
|
3046
2470
|
};
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
2471
|
+
|
|
2472
|
+
// src/vm/HolidayCalendar.ts
|
|
2473
|
+
function localDateKey(epochMs) {
|
|
2474
|
+
const d = new Date(epochMs);
|
|
2475
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
2476
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
2477
|
+
return `${d.getFullYear()}-${month}-${day}`;
|
|
2478
|
+
}
|
|
2479
|
+
function entryToKey(entry) {
|
|
2480
|
+
if (entry instanceof Date) return localDateKey(entry.getTime());
|
|
2481
|
+
if (typeof entry === "number") {
|
|
2482
|
+
return Number.isFinite(entry) ? localDateKey(entry) : null;
|
|
2483
|
+
}
|
|
2484
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(entry.trim());
|
|
2485
|
+
if (!match) return null;
|
|
2486
|
+
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
2487
|
+
}
|
|
2488
|
+
function resolveHolidayPredicate(calendar) {
|
|
2489
|
+
if (calendar === void 0) return void 0;
|
|
2490
|
+
if (typeof calendar === "function") {
|
|
2491
|
+
return (epochMs) => calendar(new Date(epochMs));
|
|
2492
|
+
}
|
|
2493
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2494
|
+
for (const entry of calendar) {
|
|
2495
|
+
const key = entryToKey(entry);
|
|
2496
|
+
if (key !== null) keys.add(key);
|
|
2497
|
+
}
|
|
2498
|
+
return (epochMs) => keys.has(localDateKey(epochMs));
|
|
2499
|
+
}
|
|
3055
2500
|
|
|
3056
2501
|
// src/telemetry/AllocationTracker.ts
|
|
3057
2502
|
var AllocationTracker = class {
|
|
@@ -3346,8 +2791,318 @@ function findInlineSolvesInLine(lineText, lineNumber) {
|
|
|
3346
2791
|
}));
|
|
3347
2792
|
}
|
|
3348
2793
|
|
|
2794
|
+
// src/packages/uom/UserUnitTable.ts
|
|
2795
|
+
function pluralInsensitiveKey(words) {
|
|
2796
|
+
const last = words[words.length - 1];
|
|
2797
|
+
const singular = last.length > 1 && last.endsWith("s") ? last.slice(0, -1) : last;
|
|
2798
|
+
return [...words.slice(0, -1), singular].join(" ");
|
|
2799
|
+
}
|
|
2800
|
+
var UserUnitTable = class {
|
|
2801
|
+
constructor() {
|
|
2802
|
+
this.byKey = /* @__PURE__ */ new Map();
|
|
2803
|
+
this.longestName = 0;
|
|
2804
|
+
}
|
|
2805
|
+
/** Whether any unit has been defined, a cheap guard for the hot path. */
|
|
2806
|
+
get isEmpty() {
|
|
2807
|
+
return this.byKey.size === 0;
|
|
2808
|
+
}
|
|
2809
|
+
/** The most words any registered name spans, bounding the lookup scan. */
|
|
2810
|
+
get maxWordCount() {
|
|
2811
|
+
return this.longestName;
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Register (or replace) a unit named by `nameWords`, defined as `ratioText`
|
|
2815
|
+
* of `baseUnit`. Re-defining a name overwrites the earlier definition, so a
|
|
2816
|
+
* corrected line wins over the one above it.
|
|
2817
|
+
*/
|
|
2818
|
+
define(nameWords, ratioText, baseUnit) {
|
|
2819
|
+
const key = pluralInsensitiveKey(nameWords);
|
|
2820
|
+
this.byKey.set(key, {
|
|
2821
|
+
displayName: nameWords.join(" "),
|
|
2822
|
+
ratioText,
|
|
2823
|
+
baseUnit
|
|
2824
|
+
});
|
|
2825
|
+
if (nameWords.length > this.longestName) this.longestName = nameWords.length;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Longest registered name that a run of identifier `words` begins with, or
|
|
2829
|
+
* `null` if none. `words` is the consecutive identifiers following a value,
|
|
2830
|
+
* so `story points each` matches the 2-word `story point` and leaves `each`.
|
|
2831
|
+
*/
|
|
2832
|
+
match(words) {
|
|
2833
|
+
const upper = Math.min(words.length, this.longestName);
|
|
2834
|
+
for (let length = upper; length >= 1; length--) {
|
|
2835
|
+
const definition = this.byKey.get(pluralInsensitiveKey(words.slice(0, length)));
|
|
2836
|
+
if (definition !== void 0) return { definition, wordCount: length };
|
|
2837
|
+
}
|
|
2838
|
+
return null;
|
|
2839
|
+
}
|
|
2840
|
+
/** Drop every definition, called when a fresh document pass begins. */
|
|
2841
|
+
clear() {
|
|
2842
|
+
this.byKey.clear();
|
|
2843
|
+
this.longestName = 0;
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
|
|
2847
|
+
// src/packages/uom/normalizer/UserUnitNormalizerRule.ts
|
|
2848
|
+
function userUnitExpansionRule(table, priority = 82) {
|
|
2849
|
+
return {
|
|
2850
|
+
name: "uom:user-unit",
|
|
2851
|
+
priority,
|
|
2852
|
+
match(tokens, pos) {
|
|
2853
|
+
if (table.isEmpty) return null;
|
|
2854
|
+
const value = tokens[pos];
|
|
2855
|
+
if (value.type !== "NUMBER" && value.type !== "RPAREN") return null;
|
|
2856
|
+
const words = [];
|
|
2857
|
+
const maxWords = table.maxWordCount;
|
|
2858
|
+
for (let i = pos + 1; i < tokens.length && words.length < maxWords; i++) {
|
|
2859
|
+
if (tokens[i].type !== "IDENT") break;
|
|
2860
|
+
words.push(tokens[i].value);
|
|
2861
|
+
}
|
|
2862
|
+
if (words.length === 0) return null;
|
|
2863
|
+
const matched = table.match(words);
|
|
2864
|
+
if (matched === null) return null;
|
|
2865
|
+
const after = tokens[pos + 1 + matched.wordCount];
|
|
2866
|
+
if (after?.type === "EQUALS") return null;
|
|
2867
|
+
const { ratioText, baseUnit } = matched.definition;
|
|
2868
|
+
const star = new LexerToken("STAR", tokenTypeId("STAR"), "*", "*", value.offset, 0, value.line, value.col);
|
|
2869
|
+
const ratio = new LexerToken("NUMBER", tokenTypeId("NUMBER"), ratioText, ratioText, value.offset, 0, value.line, value.col);
|
|
2870
|
+
const unit = new LexerToken("UNIT", tokenTypeId("UNIT"), baseUnit, baseUnit, value.offset, 0, value.line, value.col);
|
|
2871
|
+
return {
|
|
2872
|
+
consumed: 1 + matched.wordCount,
|
|
2873
|
+
replacement: [value, star, ratio, unit],
|
|
2874
|
+
ruleName: "uom:user-unit"
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// src/explain/LineExplainer.ts
|
|
2881
|
+
var SUM = 30;
|
|
2882
|
+
var PRODUCT = 40;
|
|
2883
|
+
var EXPONENT = 50;
|
|
2884
|
+
var ON_OFF = 24;
|
|
2885
|
+
function infixBindingPower(type) {
|
|
2886
|
+
switch (type) {
|
|
2887
|
+
case TokenTypes.PLUS:
|
|
2888
|
+
case TokenTypes.MINUS:
|
|
2889
|
+
return SUM;
|
|
2890
|
+
case TokenTypes.STAR:
|
|
2891
|
+
case TokenTypes.SLASH:
|
|
2892
|
+
case TokenTypes.MOD:
|
|
2893
|
+
case TokenTypes.OF:
|
|
2894
|
+
return PRODUCT;
|
|
2895
|
+
case TokenTypes.CARET:
|
|
2896
|
+
return EXPONENT;
|
|
2897
|
+
case TokenTypes.PCT_ON:
|
|
2898
|
+
case TokenTypes.PCT_OFF:
|
|
2899
|
+
return ON_OFF;
|
|
2900
|
+
default:
|
|
2901
|
+
return 0;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
function connective(type) {
|
|
2905
|
+
switch (type) {
|
|
2906
|
+
case TokenTypes.PLUS:
|
|
2907
|
+
return "plus";
|
|
2908
|
+
case TokenTypes.MINUS:
|
|
2909
|
+
return "minus";
|
|
2910
|
+
case TokenTypes.STAR:
|
|
2911
|
+
return "times";
|
|
2912
|
+
case TokenTypes.SLASH:
|
|
2913
|
+
return "divided by";
|
|
2914
|
+
case TokenTypes.MOD:
|
|
2915
|
+
return "mod";
|
|
2916
|
+
case TokenTypes.CARET:
|
|
2917
|
+
return "to the power of";
|
|
2918
|
+
case TokenTypes.OF:
|
|
2919
|
+
return "of";
|
|
2920
|
+
case TokenTypes.PCT_ON:
|
|
2921
|
+
return "plus";
|
|
2922
|
+
case TokenTypes.PCT_OFF:
|
|
2923
|
+
return "less";
|
|
2924
|
+
default:
|
|
2925
|
+
return type.toLowerCase();
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
function tokenEnd(t) {
|
|
2929
|
+
return t.sourceEnd ?? t.offset + t.value.length;
|
|
2930
|
+
}
|
|
2931
|
+
var OPERAND_TOKEN_TYPES = /* @__PURE__ */ new Set([
|
|
2932
|
+
TokenTypes.NUMBER,
|
|
2933
|
+
TokenTypes.BIGINT,
|
|
2934
|
+
TokenTypes.IDENT,
|
|
2935
|
+
TokenTypes.UNIT,
|
|
2936
|
+
TokenTypes.PERCENT,
|
|
2937
|
+
TokenTypes.PI,
|
|
2938
|
+
TokenTypes.E,
|
|
2939
|
+
TokenTypes.TRUE,
|
|
2940
|
+
TokenTypes.FALSE,
|
|
2941
|
+
TokenTypes.DOLLAR,
|
|
2942
|
+
TokenTypes.POUND,
|
|
2943
|
+
TokenTypes.EURO,
|
|
2944
|
+
TokenTypes.YEN,
|
|
2945
|
+
TokenTypes.RUBLE,
|
|
2946
|
+
TokenTypes.WON,
|
|
2947
|
+
TokenTypes.CURRENCY_SYMBOL
|
|
2948
|
+
]);
|
|
2949
|
+
function isBoundary(type) {
|
|
2950
|
+
return !OPERAND_TOKEN_TYPES.has(type);
|
|
2951
|
+
}
|
|
2952
|
+
var Parser = class {
|
|
2953
|
+
constructor(tokens) {
|
|
2954
|
+
this.tokens = tokens;
|
|
2955
|
+
this.pos = 0;
|
|
2956
|
+
}
|
|
2957
|
+
peek() {
|
|
2958
|
+
return this.tokens[this.pos];
|
|
2959
|
+
}
|
|
2960
|
+
next() {
|
|
2961
|
+
return this.tokens[this.pos++];
|
|
2962
|
+
}
|
|
2963
|
+
/** Parse the whole stream, requiring every token to be consumed. */
|
|
2964
|
+
parseAll() {
|
|
2965
|
+
const node = this.parseExpression(0);
|
|
2966
|
+
if (this.pos !== this.tokens.length) {
|
|
2967
|
+
throw new Error("explain: unconsumed tokens");
|
|
2968
|
+
}
|
|
2969
|
+
return node;
|
|
2970
|
+
}
|
|
2971
|
+
parseExpression(minBindingPower) {
|
|
2972
|
+
let left = this.parseOperand();
|
|
2973
|
+
for (; ; ) {
|
|
2974
|
+
const t = this.peek();
|
|
2975
|
+
if (!t) break;
|
|
2976
|
+
const bindingPower = infixBindingPower(t.type);
|
|
2977
|
+
if (bindingPower === 0 || bindingPower <= minBindingPower) break;
|
|
2978
|
+
this.next();
|
|
2979
|
+
const rightBindingPower = t.type === TokenTypes.CARET ? bindingPower - 1 : bindingPower;
|
|
2980
|
+
const right = this.parseExpression(rightBindingPower);
|
|
2981
|
+
left = {
|
|
2982
|
+
kind: "binary",
|
|
2983
|
+
op: t.type,
|
|
2984
|
+
left,
|
|
2985
|
+
right,
|
|
2986
|
+
start: left.start,
|
|
2987
|
+
end: right.end
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
return left;
|
|
2991
|
+
}
|
|
2992
|
+
/** A parenthesised group, an optional sign, then a run of operand tokens. */
|
|
2993
|
+
parseOperand() {
|
|
2994
|
+
const first = this.peek();
|
|
2995
|
+
if (!first) throw new Error("explain: expected an operand");
|
|
2996
|
+
if (first.type === TokenTypes.LPAREN) {
|
|
2997
|
+
return this.parseGroup();
|
|
2998
|
+
}
|
|
2999
|
+
const signStart = first.offset;
|
|
3000
|
+
let sawSign = false;
|
|
3001
|
+
while (this.peek() && (this.peek().type === TokenTypes.PLUS || this.peek().type === TokenTypes.MINUS)) {
|
|
3002
|
+
this.next();
|
|
3003
|
+
sawSign = true;
|
|
3004
|
+
}
|
|
3005
|
+
if (sawSign && this.peek() && this.peek().type === TokenTypes.LPAREN) {
|
|
3006
|
+
const group = this.parseGroup();
|
|
3007
|
+
return { kind: "wrap", child: group, start: signStart, end: group.end };
|
|
3008
|
+
}
|
|
3009
|
+
let end = -1;
|
|
3010
|
+
while (this.peek() && !isBoundary(this.peek().type)) {
|
|
3011
|
+
end = tokenEnd(this.next());
|
|
3012
|
+
}
|
|
3013
|
+
if (end === -1) {
|
|
3014
|
+
throw new Error("explain: expected an operand");
|
|
3015
|
+
}
|
|
3016
|
+
return { kind: "leaf", start: signStart, end };
|
|
3017
|
+
}
|
|
3018
|
+
parseGroup() {
|
|
3019
|
+
const open = this.next();
|
|
3020
|
+
const inner = this.parseExpression(0);
|
|
3021
|
+
const close = this.peek();
|
|
3022
|
+
if (!close || close.type !== TokenTypes.RPAREN) {
|
|
3023
|
+
throw new Error("explain: unbalanced parentheses");
|
|
3024
|
+
}
|
|
3025
|
+
this.next();
|
|
3026
|
+
return { kind: "wrap", child: inner, start: open.offset, end: tokenEnd(close) };
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
var Builder = class {
|
|
3030
|
+
constructor(source, evaluate, locale) {
|
|
3031
|
+
this.source = source;
|
|
3032
|
+
this.evaluate = evaluate;
|
|
3033
|
+
this.steps = [];
|
|
3034
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
3035
|
+
this.resultPrefix = getLocale(locale).display.resultPrefix;
|
|
3036
|
+
}
|
|
3037
|
+
build(root) {
|
|
3038
|
+
this.emit(root);
|
|
3039
|
+
return { steps: this.steps, result: this.valueOf(root) };
|
|
3040
|
+
}
|
|
3041
|
+
/** Walk in evaluation order, emitting one step per operation. */
|
|
3042
|
+
emit(node) {
|
|
3043
|
+
if (node.kind === "leaf") return;
|
|
3044
|
+
if (node.kind === "wrap") {
|
|
3045
|
+
this.emit(node.child);
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
this.emit(node.left);
|
|
3049
|
+
this.emit(node.right);
|
|
3050
|
+
this.steps.push({
|
|
3051
|
+
description: this.describe(node),
|
|
3052
|
+
value: this.valueOf(node)
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
describe(node) {
|
|
3056
|
+
const word = connective(node.op);
|
|
3057
|
+
if (node.op === TokenTypes.PCT_ON || node.op === TokenTypes.PCT_OFF) {
|
|
3058
|
+
return `${this.operand(node.right)} ${word} ${this.operand(node.left)}`;
|
|
3059
|
+
}
|
|
3060
|
+
return `${this.operand(node.left)} ${word} ${this.operand(node.right)}`;
|
|
3061
|
+
}
|
|
3062
|
+
/** How an operand appears inside a step's description. */
|
|
3063
|
+
operand(node) {
|
|
3064
|
+
if (node.kind === "leaf") {
|
|
3065
|
+
return this.source.slice(node.start, node.end).trim();
|
|
3066
|
+
}
|
|
3067
|
+
return this.render(this.valueOf(node));
|
|
3068
|
+
}
|
|
3069
|
+
/** A node's value, evaluated once from its own span and then cached. */
|
|
3070
|
+
valueOf(node) {
|
|
3071
|
+
const cached = this.cache.get(node);
|
|
3072
|
+
if (cached) return cached;
|
|
3073
|
+
const value = this.evaluate(this.source.slice(node.start, node.end));
|
|
3074
|
+
this.cache.set(node, value);
|
|
3075
|
+
return value;
|
|
3076
|
+
}
|
|
3077
|
+
/** Format a value for inline display, without the result prefix ("= "). */
|
|
3078
|
+
render(value) {
|
|
3079
|
+
const formatted = formatValue(value, DEFAULT_FORMATTING_SETTINGS);
|
|
3080
|
+
return formatted.startsWith(this.resultPrefix) ? formatted.slice(this.resultPrefix.length) : formatted;
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
function buildExplanation(params) {
|
|
3084
|
+
const { expression, tokens, evaluate, locale } = params;
|
|
3085
|
+
const terminal = () => ({
|
|
3086
|
+
expression,
|
|
3087
|
+
steps: [],
|
|
3088
|
+
result: evaluate(expression)
|
|
3089
|
+
});
|
|
3090
|
+
let root;
|
|
3091
|
+
try {
|
|
3092
|
+
root = new Parser(tokens).parseAll();
|
|
3093
|
+
} catch {
|
|
3094
|
+
return terminal();
|
|
3095
|
+
}
|
|
3096
|
+
try {
|
|
3097
|
+
const { steps, result } = new Builder(expression, evaluate, locale).build(root);
|
|
3098
|
+
return { expression, steps, result };
|
|
3099
|
+
} catch {
|
|
3100
|
+
return terminal();
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3349
3104
|
// src/engine/ExpressionEngine.ts
|
|
3350
|
-
var
|
|
3105
|
+
var _ExpressionEngine = class _ExpressionEngine {
|
|
3351
3106
|
//#endregion
|
|
3352
3107
|
//#region Constructor
|
|
3353
3108
|
constructor(localeCode = "en", diagnosticMode = false, config, diagnosticPipeline, packages) {
|
|
@@ -3384,6 +3139,25 @@ var ExpressionEngine = class {
|
|
|
3384
3139
|
* lifecycle itself.
|
|
3385
3140
|
*/
|
|
3386
3141
|
this.documentModel = null;
|
|
3142
|
+
/**
|
|
3143
|
+
* How deep goal seek is currently re-entering line evaluation, so a goal
|
|
3144
|
+
* seek whose target line is itself a goal seek is refused rather than
|
|
3145
|
+
* multiplying the search cost. One goal-seek line already re-runs its
|
|
3146
|
+
* target up to `maxGoalSeekIterations` times; allowing a nested one would
|
|
3147
|
+
* make that a product. See {@link makeLineContext}'s `evaluateLineWithBinding`.
|
|
3148
|
+
*/
|
|
3149
|
+
this.goalSeekDepth = 0;
|
|
3150
|
+
/**
|
|
3151
|
+
* Batch cross-line source, set only for the duration of a
|
|
3152
|
+
* `parseDocument`/`evaluateLines` pass (see {@link processScanResults}).
|
|
3153
|
+
* The incremental path uses {@link documentModel}; the batch path has no
|
|
3154
|
+
* such model, so cross-line closures read earlier lines from the scan and
|
|
3155
|
+
* the results array the pass is already building. Both are references to
|
|
3156
|
+
* arrays that exist regardless, so a document that uses no cross-line
|
|
3157
|
+
* feature pays nothing: the closures are simply never called.
|
|
3158
|
+
*/
|
|
3159
|
+
this.batchScanResults = null;
|
|
3160
|
+
this.batchParsedLines = null;
|
|
3387
3161
|
/**
|
|
3388
3162
|
* Package-contributed completion candidates (`IEnginePackage.completionItems`),
|
|
3389
3163
|
* keyed by package name, engine-instance-local, not a shared registry
|
|
@@ -3403,6 +3177,14 @@ var ExpressionEngine = class {
|
|
|
3403
3177
|
* are aborted together.
|
|
3404
3178
|
*/
|
|
3405
3179
|
this.keystrokeSignal = null;
|
|
3180
|
+
/**
|
|
3181
|
+
* Units defined by the current document (`1 sprint = 2 weeks`). Read by the
|
|
3182
|
+
* user-unit normalizer rule to expand a name back to its definition, written
|
|
3183
|
+
* by {@link tryDefineUserUnit} on a definition line. Document-scoped: cleared
|
|
3184
|
+
* at the start of every {@link parseDocument} pass, so definitions never
|
|
3185
|
+
* cross between documents.
|
|
3186
|
+
*/
|
|
3187
|
+
this.userUnits = new UserUnitTable();
|
|
3406
3188
|
// Bytecode cache, avoids re-parsing identical expressions.
|
|
3407
3189
|
// Bounded by config.performance.defaultCacheSize: when full, the oldest
|
|
3408
3190
|
// entry (Map insertion order) is evicted so unique expressions across a
|
|
@@ -3441,6 +3223,7 @@ var ExpressionEngine = class {
|
|
|
3441
3223
|
50,
|
|
3442
3224
|
(word) => this.normalizer.canStartPhrase(word)
|
|
3443
3225
|
));
|
|
3226
|
+
this.normalizer.register(userUnitExpansionRule(this.userUnits));
|
|
3444
3227
|
const pkgList = packages ?? BUILTIN_PACKAGES;
|
|
3445
3228
|
for (const pkg of pkgList) {
|
|
3446
3229
|
try {
|
|
@@ -3463,7 +3246,11 @@ var ExpressionEngine = class {
|
|
|
3463
3246
|
this.config.vm.maxFunctionCalls,
|
|
3464
3247
|
this.config.date.maxOffsetYears,
|
|
3465
3248
|
this.config.date.minOffsetYears,
|
|
3466
|
-
this.context
|
|
3249
|
+
this.context,
|
|
3250
|
+
// Resolved once here, not per evaluation: a host list becomes a
|
|
3251
|
+
// Set lookup the walk can run cheaply. Undefined stays undefined,
|
|
3252
|
+
// which the VM reads as weekends-only.
|
|
3253
|
+
resolveHolidayPredicate(this.config.date.holidays)
|
|
3467
3254
|
);
|
|
3468
3255
|
this.queryClient = createQueryClient();
|
|
3469
3256
|
this.batcher = new AsyncResolutionBatcher(this.dag, this.lineCache, this.vm);
|
|
@@ -3486,16 +3273,96 @@ var ExpressionEngine = class {
|
|
|
3486
3273
|
*/
|
|
3487
3274
|
makeLineContext(lineNumber) {
|
|
3488
3275
|
const doc = this.documentModel;
|
|
3276
|
+
const scan = doc ? null : this.batchScanResults;
|
|
3277
|
+
const parsed = doc ? null : this.batchParsedLines;
|
|
3489
3278
|
return {
|
|
3490
3279
|
lineIndex: lineNumber,
|
|
3491
|
-
getLineResult: doc ? (n) => doc.getLineAt(n)?.result ?? void 0 : void 0,
|
|
3280
|
+
getLineResult: doc ? (n) => doc.getLineAt(n)?.result ?? void 0 : parsed ? (n) => parsed[n - 1]?.result ?? void 0 : void 0,
|
|
3492
3281
|
isLineBoundary: doc ? (n) => {
|
|
3493
3282
|
const state = doc.getLineAt(n);
|
|
3494
3283
|
if (!state) return true;
|
|
3495
3284
|
return state.isEmpty || /^\s*#/.test(state.text);
|
|
3496
|
-
} :
|
|
3285
|
+
} : scan ? (n) => {
|
|
3286
|
+
const sr = scan[n - 1];
|
|
3287
|
+
if (!sr) return true;
|
|
3288
|
+
return sr.classification.skip || /^\s*#/.test(sr.text);
|
|
3289
|
+
} : void 0,
|
|
3290
|
+
getLineReads: doc ? (n) => {
|
|
3291
|
+
const state = doc.getLineAt(n);
|
|
3292
|
+
if (!state || state.bytecodes.length === 0) return void 0;
|
|
3293
|
+
return state.reads;
|
|
3294
|
+
} : void 0,
|
|
3295
|
+
evaluateLineWithBinding: doc ? (n, variable, bound, symbolicTolerant) => this.evaluateLineWithBinding(n, variable, bound, symbolicTolerant) : void 0,
|
|
3296
|
+
goalSeekMaxIterations: this.config.vm.maxGoalSeekIterations,
|
|
3297
|
+
// Raw source text of a line, for features that read markdown the
|
|
3298
|
+
// evaluator skipped (a table's rows). See the tables package. Reads
|
|
3299
|
+
// from the document when evaluating incrementally, and from the scan
|
|
3300
|
+
// in the batch parseDocument/evaluateLines path, so a table column
|
|
3301
|
+
// aggregate resolves the same way through both, matching the other
|
|
3302
|
+
// cross-line closures above.
|
|
3303
|
+
getLineText: doc ? (n) => doc.getLineAt(n)?.text : scan ? (n) => scan[n - 1]?.text : void 0
|
|
3497
3304
|
};
|
|
3498
3305
|
}
|
|
3306
|
+
/**
|
|
3307
|
+
* Re-evaluate line `targetLine`'s compiled expression with `variable` bound
|
|
3308
|
+
* to `bound` for that one run, the primitive goal seek drives while
|
|
3309
|
+
* narrowing in on an input. See {@link LineExecutionContext.evaluateLineWithBinding}.
|
|
3310
|
+
*
|
|
3311
|
+
* The binding is a call frame, so `LOAD_VAR` reads it in preference to the
|
|
3312
|
+
* document's own value and the document's value is untouched once the frame
|
|
3313
|
+
* is popped, the same shadowing a function parameter gets. The target's
|
|
3314
|
+
* bytecode is run as-is, so a line that defines a variable is refused: its
|
|
3315
|
+
* `STORE_VAR` would write the probe's candidate into the real variable store
|
|
3316
|
+
* as a side effect. The re-entry is bounded three ways, by
|
|
3317
|
+
* {@link goalSeekDepth} against nesting, by the VM's own per-run instruction
|
|
3318
|
+
* limit, and by the caller's iteration cap.
|
|
3319
|
+
*
|
|
3320
|
+
* @param targetLine - 1-based line whose expression is re-evaluated.
|
|
3321
|
+
* @param variable - The unknown to bind for this run.
|
|
3322
|
+
* @param bound - The value to bind it to (a number to probe, a symbolic node to read the relationship in closed form).
|
|
3323
|
+
* @param symbolicTolerant - Whether an otherwise-undefined variable reads as a symbolic placeholder, needed for the closed-form read.
|
|
3324
|
+
* @returns The line's re-evaluated Value, or an error Value when it cannot be probed.
|
|
3325
|
+
*/
|
|
3326
|
+
evaluateLineWithBinding(targetLine, variable, bound, symbolicTolerant) {
|
|
3327
|
+
const doc = this.documentModel;
|
|
3328
|
+
if (!doc) {
|
|
3329
|
+
return errorValue("GOAL_SEEK_NO_DOCUMENT", "Goal seek needs a document to solve against, which the single-expression entry point does not have.");
|
|
3330
|
+
}
|
|
3331
|
+
if (this.goalSeekDepth >= _ExpressionEngine.GOAL_SEEK_MAX_NESTING_DEPTH) {
|
|
3332
|
+
return errorValue("GOAL_SEEK_NESTED", `A goal-seek line cannot target another goal-seek line, since each already re-runs its target many times.`);
|
|
3333
|
+
}
|
|
3334
|
+
const state = doc.getLineAt(targetLine);
|
|
3335
|
+
if (!state || state.bytecodes.length === 0) {
|
|
3336
|
+
return errorValue("GOAL_SEEK_LINE_NOT_READY", `Line ${targetLine} has no evaluated expression to solve (forward reference, out of range, or not an expression).`);
|
|
3337
|
+
}
|
|
3338
|
+
if (state.isVariableDef) {
|
|
3339
|
+
return errorValue("GOAL_SEEK_TARGET_IS_DEFINITION", `Line ${targetLine} defines a variable, so re-running it would overwrite that variable. Target the line that uses ${variable}, not the one that sets a value.`);
|
|
3340
|
+
}
|
|
3341
|
+
const program = state.bytecodes[0];
|
|
3342
|
+
this.goalSeekDepth++;
|
|
3343
|
+
try {
|
|
3344
|
+
const frame = /* @__PURE__ */ new Map([[variable, bound]]);
|
|
3345
|
+
const stackBefore = this.vm.getStack().length;
|
|
3346
|
+
this.vm.pushCallFrame(frame);
|
|
3347
|
+
try {
|
|
3348
|
+
const result = executeBytecode(program, this.vm, void 0, void 0, this.makeLineContext(targetLine), symbolicTolerant);
|
|
3349
|
+
if (result.type === "pending") {
|
|
3350
|
+
return errorValue("GOAL_SEEK_ASYNC_UNSUPPORTED", `Line ${targetLine} depends on an async value (weather, stocks, currency, ...), which goal seek cannot re-run.`);
|
|
3351
|
+
}
|
|
3352
|
+
if (result.type === "error") {
|
|
3353
|
+
return errorValue(result.error.code ?? "GOAL_SEEK_TARGET_ERROR", result.error.message);
|
|
3354
|
+
}
|
|
3355
|
+
return result.value;
|
|
3356
|
+
} finally {
|
|
3357
|
+
this.vm.popCallFrame();
|
|
3358
|
+
while (this.vm.getStack().length > stackBefore) this.vm.pop();
|
|
3359
|
+
}
|
|
3360
|
+
} catch (e) {
|
|
3361
|
+
return errorValue("GOAL_SEEK_TARGET_ERROR", normalizeUnknownError(e).message);
|
|
3362
|
+
} finally {
|
|
3363
|
+
this.goalSeekDepth--;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3499
3366
|
/**
|
|
3500
3367
|
* Insert into the bytecode cache, evicting the oldest entry when full.
|
|
3501
3368
|
*
|
|
@@ -3888,6 +3755,8 @@ var ExpressionEngine = class {
|
|
|
3888
3755
|
{ maxLines }
|
|
3889
3756
|
);
|
|
3890
3757
|
}
|
|
3758
|
+
if (!this.userUnits.isEmpty) this.bytecodeCache.clear();
|
|
3759
|
+
this.userUnits.clear();
|
|
3891
3760
|
const scanResults = this.lexer.scanDocument(input);
|
|
3892
3761
|
const processedLines = this.processScanResults(scanResults);
|
|
3893
3762
|
const result = {
|
|
@@ -3933,6 +3802,8 @@ var ExpressionEngine = class {
|
|
|
3933
3802
|
* bypassing the split→join roundtrip that this method performs.
|
|
3934
3803
|
*/
|
|
3935
3804
|
evaluateLines(lines) {
|
|
3805
|
+
if (!this.userUnits.isEmpty) this.bytecodeCache.clear();
|
|
3806
|
+
this.userUnits.clear();
|
|
3936
3807
|
const documentText = lines.join("\n");
|
|
3937
3808
|
const scanResults = this.lexer.scanDocument(documentText);
|
|
3938
3809
|
return this.processScanResults(scanResults);
|
|
@@ -3947,65 +3818,74 @@ var ExpressionEngine = class {
|
|
|
3947
3818
|
*/
|
|
3948
3819
|
processScanResults(scanResults) {
|
|
3949
3820
|
const result = [];
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
const
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3821
|
+
const previousBatchScanResults = this.batchScanResults;
|
|
3822
|
+
const previousBatchParsedLines = this.batchParsedLines;
|
|
3823
|
+
this.batchScanResults = scanResults;
|
|
3824
|
+
this.batchParsedLines = result;
|
|
3825
|
+
try {
|
|
3826
|
+
for (const scanResult of scanResults) {
|
|
3827
|
+
const lineText = scanResult.text;
|
|
3828
|
+
const lineNumber = scanResult.lineNumber;
|
|
3829
|
+
const startPosition = scanResult.startOffset;
|
|
3830
|
+
const endPosition = scanResult.endOffset;
|
|
3831
|
+
const isEmpty = scanResult.classification.skip;
|
|
3832
|
+
const inlineSolves = scanResult.inlineSolves.map((s) => ({
|
|
3833
|
+
start: s.start,
|
|
3834
|
+
end: s.end,
|
|
3835
|
+
expression: s.expression,
|
|
3836
|
+
lineNumber,
|
|
3837
|
+
columnNumber: s.columnNumber
|
|
3838
|
+
}));
|
|
3839
|
+
const hasInlineSolves = inlineSolves.length > 0;
|
|
3840
|
+
const parsedLine = {
|
|
3841
|
+
lineNumber,
|
|
3842
|
+
text: lineText,
|
|
3843
|
+
startPosition,
|
|
3844
|
+
endPosition,
|
|
3845
|
+
isEmpty,
|
|
3846
|
+
hasInlineSolves,
|
|
3847
|
+
inlineSolves,
|
|
3848
|
+
expression: null,
|
|
3849
|
+
result: null,
|
|
3850
|
+
error: null
|
|
3851
|
+
};
|
|
3852
|
+
if (!isEmpty) {
|
|
3853
|
+
const isVariableAssignment = lineText.trim().startsWith(":");
|
|
3854
|
+
if (hasInlineSolves && !isVariableAssignment) {
|
|
3855
|
+
for (const solve of inlineSolves) {
|
|
3856
|
+
try {
|
|
3857
|
+
const values = this.evaluateLine(lineNumber, solve.expression);
|
|
3858
|
+
solve.result = values[0];
|
|
3859
|
+
} catch (error) {
|
|
3860
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3861
|
+
solve.error = errorMessage;
|
|
3862
|
+
}
|
|
3986
3863
|
}
|
|
3987
|
-
}
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
3864
|
+
} else {
|
|
3865
|
+
const contentOffset = scanResult.classification.contentOffset;
|
|
3866
|
+
const evaluable = contentOffset === void 0 ? lineText : lineText.slice(contentOffset - startPosition);
|
|
3867
|
+
const expression = evaluable.trim();
|
|
3868
|
+
if (expression) {
|
|
3869
|
+
try {
|
|
3870
|
+
const values = this.evaluateLineWithPreTokenized(
|
|
3871
|
+
lineNumber,
|
|
3872
|
+
expression,
|
|
3873
|
+
scanResult.tokens
|
|
3874
|
+
);
|
|
3875
|
+
parsedLine.expression = expression;
|
|
3876
|
+
parsedLine.result = values[0];
|
|
3877
|
+
} catch (error) {
|
|
3878
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3879
|
+
parsedLine.error = errorMessage;
|
|
3880
|
+
}
|
|
4004
3881
|
}
|
|
4005
3882
|
}
|
|
4006
3883
|
}
|
|
3884
|
+
result.push(parsedLine);
|
|
4007
3885
|
}
|
|
4008
|
-
|
|
3886
|
+
} finally {
|
|
3887
|
+
this.batchScanResults = previousBatchScanResults;
|
|
3888
|
+
this.batchParsedLines = previousBatchParsedLines;
|
|
4009
3889
|
}
|
|
4010
3890
|
return result;
|
|
4011
3891
|
}
|
|
@@ -4244,6 +4124,54 @@ var ExpressionEngine = class {
|
|
|
4244
4124
|
* session's own Phase H.2 scope decision (full symbolic MATRICES, not
|
|
4245
4125
|
* a general CAS).
|
|
4246
4126
|
*/
|
|
4127
|
+
/**
|
|
4128
|
+
* Registers a document-scoped user unit from a `1 <name> = <n> <unit>` line
|
|
4129
|
+
* and returns the `<name> defined` confirmation, or `null` when the line is
|
|
4130
|
+
* not a unit definition (so ordinary processing continues unchanged).
|
|
4131
|
+
*
|
|
4132
|
+
* Like the symbolic-grammar shapes it sits beside, this has an effect (a
|
|
4133
|
+
* stored definition) that no cached bytecode program could represent, so it
|
|
4134
|
+
* short-circuits compilation the same way. See {@link UserUnitTable}.
|
|
4135
|
+
*
|
|
4136
|
+
* The pattern is kept deliberately narrow so it cannot swallow an equation:
|
|
4137
|
+
*
|
|
4138
|
+
* - The coefficient must be exactly `1`. `2 x = 10` stays a scalar equation
|
|
4139
|
+
* (`x =>` solves it); only the natural `1 sprint = 2 weeks` shape is a
|
|
4140
|
+
* definition.
|
|
4141
|
+
* - The name is one or more identifiers, never a built-in unit (those lex as
|
|
4142
|
+
* UNIT, not IDENT), so a built-in unit cannot be redefined.
|
|
4143
|
+
* - The base must be a recognized unit (a UNIT token). A non-unit right side
|
|
4144
|
+
* declines here and is left to whatever handled it before, so a defined
|
|
4145
|
+
* unit is always dimensioned, never free-standing.
|
|
4146
|
+
*
|
|
4147
|
+
* `tokens` are already normalized, so the implicit-multiply pass has usually
|
|
4148
|
+
* inserted a STAR between the coefficient and the name (`1 * sprint`); it is
|
|
4149
|
+
* tolerated and skipped.
|
|
4150
|
+
*/
|
|
4151
|
+
tryDefineUserUnit(tokens) {
|
|
4152
|
+
if (tokens.length < 5) return null;
|
|
4153
|
+
if (tokens[0].type !== "NUMBER" || Number(tokens[0].value) !== 1) return null;
|
|
4154
|
+
let i = 1;
|
|
4155
|
+
if (tokens[i].type === "STAR") i++;
|
|
4156
|
+
const nameWords = [];
|
|
4157
|
+
while (i < tokens.length && tokens[i].type === "IDENT") {
|
|
4158
|
+
nameWords.push(tokens[i].value);
|
|
4159
|
+
i++;
|
|
4160
|
+
}
|
|
4161
|
+
if (nameWords.length === 0) return null;
|
|
4162
|
+
if (tokens[i]?.type !== "EQUALS") return null;
|
|
4163
|
+
i++;
|
|
4164
|
+
if (tokens[i]?.type !== "NUMBER") return null;
|
|
4165
|
+
const ratioToken = tokens[i];
|
|
4166
|
+
i++;
|
|
4167
|
+
if (tokens[i]?.type !== "UNIT") return null;
|
|
4168
|
+
const baseToken = tokens[i];
|
|
4169
|
+
i++;
|
|
4170
|
+
if (i !== tokens.length) return null;
|
|
4171
|
+
this.userUnits.define(nameWords, ratioToken.value, baseToken.value);
|
|
4172
|
+
this.bytecodeCache.clear();
|
|
4173
|
+
return stringValue(`${nameWords.join(" ")} defined`);
|
|
4174
|
+
}
|
|
4247
4175
|
trySymbolicGrammar(normalizedTokens, lineNumber = -1) {
|
|
4248
4176
|
if (normalizedTokens.length === 0) return null;
|
|
4249
4177
|
const last = normalizedTokens[normalizedTokens.length - 1];
|
|
@@ -4271,6 +4199,7 @@ var ExpressionEngine = class {
|
|
|
4271
4199
|
return this.simplifySymbolically(normalizedTokens, lineNumber);
|
|
4272
4200
|
}
|
|
4273
4201
|
if (normalizedTokens[0].type === "COLON" || normalizedTokens[0].type === "GLOBAL") return null;
|
|
4202
|
+
if (normalizedTokens[0].type === "GOAL_SEEK") return null;
|
|
4274
4203
|
const eqIdx = normalizedTokens.findIndex((t) => t.type === "EQUALS");
|
|
4275
4204
|
if (eqIdx === -1) return null;
|
|
4276
4205
|
const names = this.parseFactorChain(normalizedTokens.slice(0, eqIdx));
|
|
@@ -4461,7 +4390,7 @@ var ExpressionEngine = class {
|
|
|
4461
4390
|
}
|
|
4462
4391
|
let symbolicResult;
|
|
4463
4392
|
try {
|
|
4464
|
-
symbolicResult = this.trySymbolicGrammar(normalizedTokens, lineNumber);
|
|
4393
|
+
symbolicResult = this.tryDefineUserUnit(normalizedTokens) ?? this.trySymbolicGrammar(normalizedTokens, lineNumber);
|
|
4465
4394
|
} catch (e) {
|
|
4466
4395
|
const { reads: reads2, writes: writes2 } = extractReadsAndWrites(normalizedTokens);
|
|
4467
4396
|
return { kind: "error", stage: "parse", error: normalizeUnknownError(e), reads: reads2, writes: writes2, normalizedTokens };
|
|
@@ -5614,6 +5543,70 @@ var ExpressionEngine = class {
|
|
|
5614
5543
|
evaluateExpression(expression) {
|
|
5615
5544
|
return this.evaluateLine(-1, expression);
|
|
5616
5545
|
}
|
|
5546
|
+
/**
|
|
5547
|
+
* Explain how a line reached its answer, as a readable derivation.
|
|
5548
|
+
*
|
|
5549
|
+
* This is a companion to {@link evaluateExpression}, not a replacement:
|
|
5550
|
+
* `explainLine` is for the person reading the note, whereas the diagnostic
|
|
5551
|
+
* pipeline (`evaluateLineWithDiagnostic`) is for the developer and reports
|
|
5552
|
+
* stages, opcodes and timings. A host puts a derivation behind a hover or a
|
|
5553
|
+
* disclosure, so this is an API rather than an `explain` keyword: it never
|
|
5554
|
+
* consumes a word that a prose line might use, and it annotates a line the
|
|
5555
|
+
* host has already chosen to explain.
|
|
5556
|
+
*
|
|
5557
|
+
* The returned {@link Explanation} walks the line's operations in evaluation
|
|
5558
|
+
* order, each with the value it arrives at, and its `result` is identical to
|
|
5559
|
+
* what {@link evaluateExpression} returns for the same line. Every value in
|
|
5560
|
+
* the derivation is the engine's own, the operations are re-evaluated rather
|
|
5561
|
+
* than re-derived, so a step can never disagree with the answer.
|
|
5562
|
+
*
|
|
5563
|
+
* A line with nothing to break down (a bare literal), or one built from a
|
|
5564
|
+
* construct this slice does not derive yet (matrices, dates, function
|
|
5565
|
+
* calls), comes back with an empty `steps` array and the answer in
|
|
5566
|
+
* `result`, rather than an error.
|
|
5567
|
+
*
|
|
5568
|
+
* @param expression - The raw line to explain.
|
|
5569
|
+
* @returns The ordered derivation and final value.
|
|
5570
|
+
* @throws {EngineError} When the line does not evaluate at all, or resolves
|
|
5571
|
+
* data asynchronously (a derivation has no meaning for either).
|
|
5572
|
+
*/
|
|
5573
|
+
explainLine(expression) {
|
|
5574
|
+
const { tokens } = this.lexToTokens(expression);
|
|
5575
|
+
const exprTokens = tokens.filter((t) => t.type !== "COMMENT");
|
|
5576
|
+
const normalized = this.normalizer.normalize(exprTokens);
|
|
5577
|
+
return buildExplanation({
|
|
5578
|
+
expression,
|
|
5579
|
+
tokens: normalized,
|
|
5580
|
+
evaluate: (source) => this.evaluateIsolated(source),
|
|
5581
|
+
locale: this.localeCode
|
|
5582
|
+
});
|
|
5583
|
+
}
|
|
5584
|
+
/**
|
|
5585
|
+
* Evaluate a self-contained sub-expression without touching document state.
|
|
5586
|
+
*
|
|
5587
|
+
* Used only by {@link explainLine}. Unlike `evaluateExpression`, this never
|
|
5588
|
+
* writes to the line cache or the dependency graph: it is handed the spans
|
|
5589
|
+
* of a single line's own sub-expressions, and evaluating those to build a
|
|
5590
|
+
* derivation must not disturb the document the line belongs to. An async
|
|
5591
|
+
* (pending) result is rejected, a derivation cannot represent one.
|
|
5592
|
+
*/
|
|
5593
|
+
evaluateIsolated(expression) {
|
|
5594
|
+
const { tokens, hasParens } = this.lexToTokens(expression);
|
|
5595
|
+
const prep = this.prepareExpression(expression, tokens, hasParens, void 0, -1);
|
|
5596
|
+
if (prep.kind === "empty") return numberValue(0);
|
|
5597
|
+
if (prep.kind === "error") throw prep.error;
|
|
5598
|
+
if (prep.kind === "symbolic-solve") return prep.value;
|
|
5599
|
+
const result = this.executeRaw(prep.program, -1);
|
|
5600
|
+
if (result.type === "error") throw result.error;
|
|
5601
|
+
if (result.type === "pending") {
|
|
5602
|
+
throw ErrorFactory.execution(
|
|
5603
|
+
"EXPLAIN_ASYNC_UNSUPPORTED",
|
|
5604
|
+
`A derivation cannot be built for a line that resolves data asynchronously: "${expression}"`,
|
|
5605
|
+
{ expression }
|
|
5606
|
+
);
|
|
5607
|
+
}
|
|
5608
|
+
return result.value;
|
|
5609
|
+
}
|
|
5617
5610
|
//#endregion
|
|
5618
5611
|
//#region Compilation, Bytecode-only path
|
|
5619
5612
|
/**
|
|
@@ -5784,1796 +5777,1016 @@ var ExpressionEngine = class {
|
|
|
5784
5777
|
this.scopeManager.clear();
|
|
5785
5778
|
this.bytecodeCache.clear();
|
|
5786
5779
|
this.vm.reset();
|
|
5780
|
+
this.userUnits.clear();
|
|
5787
5781
|
this.lastTelemetry = null;
|
|
5788
5782
|
}
|
|
5789
5783
|
//#endregion
|
|
5790
|
-
//#region Public API,
|
|
5791
|
-
/**
|
|
5792
|
-
*
|
|
5793
|
-
*
|
|
5794
|
-
*
|
|
5795
|
-
*
|
|
5796
|
-
*
|
|
5784
|
+
//#region Public API, Snapshot / restore
|
|
5785
|
+
/**
|
|
5786
|
+
* Serialise this engine's session state into a plain, JSON-safe snapshot.
|
|
5787
|
+
*
|
|
5788
|
+
* Carries the three things a session accumulates in memory: named
|
|
5789
|
+
* {@link ExpressionEngine} variables, user-defined functions, and the
|
|
5790
|
+
* per-line result/bytecode cache (plus the expression-keyed bytecode cache).
|
|
5791
|
+
* Restore it with {@link ExpressionEngine.fromJSON} onto a fresh engine, and
|
|
5792
|
+
* later expressions resolve exactly as they would have on the engine that
|
|
5793
|
+
* evaluated the document.
|
|
5794
|
+
*
|
|
5795
|
+
* What is deliberately NOT carried:
|
|
5796
|
+
* - **Resolved async values.** Weather, stocks, currency, any package that
|
|
5797
|
+
* fetches: those results are point-in-time and must be re-fetched, not
|
|
5798
|
+
* restored stale. Every line backed by an async resolver (a DAG
|
|
5799
|
+
* data-source dependency, or an async plugin call in its bytecode) is
|
|
5800
|
+
* dropped from the line cache, and any variable whose most-recent
|
|
5801
|
+
* definition was such a line is dropped too. An in-flight (Pending) value
|
|
5802
|
+
* is likewise never written.
|
|
5803
|
+
* - **Package-contributed state.** Core state only for v1; a package opt-in
|
|
5804
|
+
* is a follow-up.
|
|
5805
|
+
* - **Symbolic (algebra) values.** Deferred: a variable holding one makes
|
|
5806
|
+
* this method throw {@link SnapshotErrorCodes.SNAPSHOT_UNSUPPORTED_VALUE}
|
|
5807
|
+
* (refused by name rather than dropped silently), and a cached line whose
|
|
5808
|
+
* result is symbolic is skipped (it re-evaluates on restore, algebra is
|
|
5809
|
+
* synchronous).
|
|
5810
|
+
*
|
|
5811
|
+
* The result survives `JSON.stringify` then `JSON.parse` unchanged.
|
|
5812
|
+
*
|
|
5813
|
+
* @returns A snapshot safe to store and hand back to `fromJSON`.
|
|
5814
|
+
* @throws {@link SnapshotErrorCodes.SNAPSHOT_UNSUPPORTED_VALUE} if a variable
|
|
5815
|
+
* holds a value this v1 format cannot represent (a symbolic value).
|
|
5797
5816
|
*/
|
|
5798
|
-
|
|
5799
|
-
const
|
|
5800
|
-
|
|
5801
|
-
const
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5817
|
+
toJSON() {
|
|
5818
|
+
const dagSnapshot = this.dag.getSnapshot();
|
|
5819
|
+
const asyncLines = /* @__PURE__ */ new Set();
|
|
5820
|
+
for (const key of Object.keys(dagSnapshot.dataSourceDeps)) {
|
|
5821
|
+
const line = Number(key);
|
|
5822
|
+
if (!Number.isNaN(line)) asyncLines.add(line);
|
|
5823
|
+
}
|
|
5824
|
+
const lineEntries = this.lineCache.snapshotEntries();
|
|
5825
|
+
const latestWriter = /* @__PURE__ */ new Map();
|
|
5826
|
+
for (const { line, entry } of lineEntries) {
|
|
5827
|
+
const isAsync = asyncLines.has(line) || entry.bytecode.hasAsync;
|
|
5828
|
+
if (isAsync) asyncLines.add(line);
|
|
5829
|
+
const writeVar = entry.writeVariable;
|
|
5830
|
+
if (writeVar) {
|
|
5831
|
+
const prev = latestWriter.get(writeVar);
|
|
5832
|
+
if (!prev || line >= prev.line) latestWriter.set(writeVar, { line, async: isAsync });
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5835
|
+
const variables = {};
|
|
5836
|
+
for (const [name, value] of this.vm.getVariableEntries()) {
|
|
5837
|
+
if (value.type === 12 /* Pending */) continue;
|
|
5838
|
+
if (latestWriter.get(name)?.async) continue;
|
|
5839
|
+
variables[name] = serializeValue(value, `variable "${name}"`);
|
|
5840
|
+
}
|
|
5841
|
+
const userFunctions = this.vm.getUserFunctionDefs().map(serializeUserFunction);
|
|
5842
|
+
const lineCache = [];
|
|
5843
|
+
for (const { line, expression, entry } of lineEntries) {
|
|
5844
|
+
if (asyncLines.has(line)) continue;
|
|
5845
|
+
if (entry.result.type === 12 /* Pending */) continue;
|
|
5846
|
+
let result;
|
|
5847
|
+
try {
|
|
5848
|
+
result = serializeValue(entry.result, `line ${line}`);
|
|
5849
|
+
} catch (e) {
|
|
5850
|
+
if (e instanceof EngineError && e.code === SnapshotErrorCodes.SNAPSHOT_UNSUPPORTED_VALUE) continue;
|
|
5851
|
+
throw e;
|
|
5815
5852
|
}
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
};
|
|
5824
|
-
|
|
5825
|
-
// src/engine/SegmentTree.ts
|
|
5826
|
-
var SegmentTree = class {
|
|
5827
|
-
constructor() {
|
|
5828
|
-
this.root = null;
|
|
5829
|
-
}
|
|
5830
|
-
// ── Public API ───────────────────────────────────────────────────────
|
|
5831
|
-
/** Total number of elements in the tree. */
|
|
5832
|
-
get length() {
|
|
5833
|
-
return this.root ? this.root.size : 0;
|
|
5834
|
-
}
|
|
5835
|
-
/** Whether the tree is empty. */
|
|
5836
|
-
get isEmpty() {
|
|
5837
|
-
return this.root === null;
|
|
5838
|
-
}
|
|
5839
|
-
/**
|
|
5840
|
-
* Get the lineId at the given 0-based index.
|
|
5841
|
-
* Returns undefined if index is out of bounds.
|
|
5842
|
-
* O(log N) expected.
|
|
5843
|
-
*/
|
|
5844
|
-
getAt(index) {
|
|
5845
|
-
if (index < 0 || index >= (this.root?.size ?? 0)) return void 0;
|
|
5846
|
-
return this.nodeAt(this.root, index).lineId;
|
|
5847
|
-
}
|
|
5848
|
-
/**
|
|
5849
|
-
* Insert a single lineId at the given 0-based index.
|
|
5850
|
-
* O(log N) expected.
|
|
5851
|
-
*/
|
|
5852
|
-
insertAt(index, lineId) {
|
|
5853
|
-
const [left, right] = split(this.root, index);
|
|
5854
|
-
this.root = merge(merge(left, createNode(lineId)), right);
|
|
5855
|
-
}
|
|
5856
|
-
/**
|
|
5857
|
-
* Delete a single element at the given 0-based index.
|
|
5858
|
-
* Returns the removed lineId, or undefined if out of bounds.
|
|
5859
|
-
* O(log N) expected.
|
|
5860
|
-
*/
|
|
5861
|
-
deleteAt(index) {
|
|
5862
|
-
if (index < 0 || index >= (this.root?.size ?? 0)) return void 0;
|
|
5863
|
-
const [left, midRight] = split(this.root, index);
|
|
5864
|
-
const [mid, right] = split(midRight, 1);
|
|
5865
|
-
const removed = mid ? mid.lineId : void 0;
|
|
5866
|
-
this.root = merge(left, right);
|
|
5867
|
-
return removed;
|
|
5868
|
-
}
|
|
5869
|
-
/**
|
|
5870
|
-
* Bulk splice: delete `deleteCount` elements at `startIndex` (0-based),
|
|
5871
|
-
* then insert `newIds` in their place.
|
|
5872
|
-
*
|
|
5873
|
-
* Returns the removed lineId array.
|
|
5874
|
-
* O(log N + deleteCount + insertCount) expected.
|
|
5875
|
-
*/
|
|
5876
|
-
spliceAt(startIndex, deleteCount, newIds) {
|
|
5877
|
-
const clampedStart = Math.max(0, startIndex);
|
|
5878
|
-
const clampedCount = Math.min(
|
|
5879
|
-
Math.max(0, deleteCount),
|
|
5880
|
-
(this.root?.size ?? 0) - clampedStart
|
|
5881
|
-
);
|
|
5882
|
-
const [left, midRight] = split(this.root, clampedStart);
|
|
5883
|
-
const [mid, right] = split(midRight, clampedCount);
|
|
5884
|
-
const removed = collectInOrder(mid);
|
|
5885
|
-
const newTree = buildTreap(newIds, 0, newIds.length);
|
|
5886
|
-
this.root = merge(merge(left, newTree), right);
|
|
5887
|
-
return removed;
|
|
5888
|
-
}
|
|
5889
|
-
/**
|
|
5890
|
-
* Replace the entire tree with a new set of lineIds.
|
|
5891
|
-
* O(N), builds a balanced treap from a flat array.
|
|
5892
|
-
*/
|
|
5893
|
-
replaceAll(lineIds) {
|
|
5894
|
-
this.root = buildTreap(lineIds, 0, lineIds.length);
|
|
5895
|
-
}
|
|
5896
|
-
/**
|
|
5897
|
-
* Get a contiguous range of lineIds by index.
|
|
5898
|
-
* O(rangeSize + log N), single in-order walk instead of N × O(log N).
|
|
5899
|
-
*
|
|
5900
|
-
* This is the hot path for viewport rendering in DocumentModel.getVisibleLines().
|
|
5901
|
-
*/
|
|
5902
|
-
getRange(startIndex, endIndex) {
|
|
5903
|
-
const clampedStart = Math.max(0, startIndex);
|
|
5904
|
-
const clampedEnd = Math.min((this.root?.size ?? 0) - 1, endIndex);
|
|
5905
|
-
if (clampedStart > clampedEnd) return [];
|
|
5906
|
-
const result = [];
|
|
5907
|
-
this.collectRange(this.root, clampedStart, clampedEnd, 0, result);
|
|
5908
|
-
return result;
|
|
5909
|
-
}
|
|
5910
|
-
/**
|
|
5911
|
-
* Clear the tree.
|
|
5912
|
-
*/
|
|
5913
|
-
clear() {
|
|
5914
|
-
this.root = null;
|
|
5915
|
-
}
|
|
5916
|
-
// ── Iteration ─────────────────────────────────────────────────────────
|
|
5917
|
-
/**
|
|
5918
|
-
* In-order iterator yielding all lineIds.
|
|
5919
|
-
*/
|
|
5920
|
-
*[Symbol.iterator]() {
|
|
5921
|
-
yield* this.inOrder(this.root);
|
|
5922
|
-
}
|
|
5923
|
-
*inOrder(node) {
|
|
5924
|
-
if (!node) return;
|
|
5925
|
-
yield* this.inOrder(node.left);
|
|
5926
|
-
yield node.lineId;
|
|
5927
|
-
yield* this.inOrder(node.right);
|
|
5928
|
-
}
|
|
5929
|
-
// ── Private helpers ───────────────────────────────────────────────────
|
|
5930
|
-
nodeAt(node, index) {
|
|
5931
|
-
const leftSize = node.left ? node.left.size : 0;
|
|
5932
|
-
if (index < leftSize) {
|
|
5933
|
-
return this.nodeAt(node.left, index);
|
|
5934
|
-
} else if (index === leftSize) {
|
|
5935
|
-
return node;
|
|
5936
|
-
} else {
|
|
5937
|
-
return this.nodeAt(node.right, index - leftSize - 1);
|
|
5938
|
-
}
|
|
5939
|
-
}
|
|
5940
|
-
/**
|
|
5941
|
-
* Collect all node values in the range [targetStart, targetEnd] into result.
|
|
5942
|
-
* `offset` is the 0-based start of the current subtree within the full tree.
|
|
5943
|
-
* O(rangeSize + log N), skips entire subtrees outside the target range.
|
|
5944
|
-
*/
|
|
5945
|
-
collectRange(node, targetStart, targetEnd, offset, result) {
|
|
5946
|
-
if (!node) return;
|
|
5947
|
-
const leftSize = node.left ? node.left.size : 0;
|
|
5948
|
-
const nodeIndex = offset + leftSize;
|
|
5949
|
-
if (nodeIndex > targetStart && node.left) {
|
|
5950
|
-
this.collectRange(node.left, targetStart, targetEnd, offset, result);
|
|
5951
|
-
}
|
|
5952
|
-
if (nodeIndex >= targetStart && nodeIndex <= targetEnd) {
|
|
5953
|
-
result.push(node.lineId);
|
|
5954
|
-
}
|
|
5955
|
-
if (nodeIndex < targetEnd && node.right) {
|
|
5956
|
-
this.collectRange(node.right, targetStart, targetEnd, nodeIndex + 1, result);
|
|
5853
|
+
lineCache.push({
|
|
5854
|
+
line,
|
|
5855
|
+
expression,
|
|
5856
|
+
result,
|
|
5857
|
+
bytecode: serializeBytecode(entry.bytecode),
|
|
5858
|
+
reads: entry.readVariables.slice(),
|
|
5859
|
+
writeVar: entry.writeVariable
|
|
5860
|
+
});
|
|
5957
5861
|
}
|
|
5862
|
+
const bytecodeCache = Array.from(this.bytecodeCache.entries()).map(([expression, program]) => ({
|
|
5863
|
+
expression,
|
|
5864
|
+
program: serializeBytecode(program)
|
|
5865
|
+
}));
|
|
5866
|
+
return {
|
|
5867
|
+
format: SNAPSHOT_FORMAT,
|
|
5868
|
+
version: SNAPSHOT_VERSION,
|
|
5869
|
+
engineVersion: ENGINE_VERSION,
|
|
5870
|
+
locale: this.localeCode,
|
|
5871
|
+
variables,
|
|
5872
|
+
userFunctions,
|
|
5873
|
+
lineCache,
|
|
5874
|
+
bytecodeCache
|
|
5875
|
+
};
|
|
5958
5876
|
}
|
|
5959
|
-
};
|
|
5960
|
-
function createNode(lineId) {
|
|
5961
|
-
return {
|
|
5962
|
-
lineId,
|
|
5963
|
-
size: 1,
|
|
5964
|
-
priority: Math.random(),
|
|
5965
|
-
left: null,
|
|
5966
|
-
right: null
|
|
5967
|
-
};
|
|
5968
|
-
}
|
|
5969
|
-
function updateSize(node) {
|
|
5970
|
-
node.size = 1 + (node.left ? node.left.size : 0) + (node.right ? node.right.size : 0);
|
|
5971
|
-
}
|
|
5972
|
-
function merge(left, right) {
|
|
5973
|
-
if (!left) return right;
|
|
5974
|
-
if (!right) return left;
|
|
5975
|
-
if (left.priority > right.priority) {
|
|
5976
|
-
left.right = merge(left.right, right);
|
|
5977
|
-
updateSize(left);
|
|
5978
|
-
return left;
|
|
5979
|
-
} else {
|
|
5980
|
-
right.left = merge(left, right.left);
|
|
5981
|
-
updateSize(right);
|
|
5982
|
-
return right;
|
|
5983
|
-
}
|
|
5984
|
-
}
|
|
5985
|
-
function split(node, index) {
|
|
5986
|
-
if (!node) return [null, null];
|
|
5987
|
-
const leftSize = node.left ? node.left.size : 0;
|
|
5988
|
-
if (index <= leftSize) {
|
|
5989
|
-
const [left, right] = split(node.left, index);
|
|
5990
|
-
node.left = right;
|
|
5991
|
-
updateSize(node);
|
|
5992
|
-
return [left, node];
|
|
5993
|
-
} else {
|
|
5994
|
-
const [left, right] = split(node.right, index - leftSize - 1);
|
|
5995
|
-
node.right = left;
|
|
5996
|
-
updateSize(node);
|
|
5997
|
-
return [node, right];
|
|
5998
|
-
}
|
|
5999
|
-
}
|
|
6000
|
-
function collectInOrder(node) {
|
|
6001
|
-
const result = [];
|
|
6002
|
-
collect(node, result);
|
|
6003
|
-
return result;
|
|
6004
|
-
}
|
|
6005
|
-
function collect(node, result) {
|
|
6006
|
-
if (!node) return;
|
|
6007
|
-
collect(node.left, result);
|
|
6008
|
-
result.push(node.lineId);
|
|
6009
|
-
collect(node.right, result);
|
|
6010
|
-
}
|
|
6011
|
-
function buildTreap(lineIds, start, end) {
|
|
6012
|
-
if (start >= end) return null;
|
|
6013
|
-
const mid = Math.floor((start + end) / 2);
|
|
6014
|
-
const priority = pseudoRandom(mid);
|
|
6015
|
-
const node = {
|
|
6016
|
-
lineId: lineIds[mid],
|
|
6017
|
-
size: end - start,
|
|
6018
|
-
priority,
|
|
6019
|
-
left: buildTreap(lineIds, start, mid),
|
|
6020
|
-
right: buildTreap(lineIds, mid + 1, end)
|
|
6021
|
-
};
|
|
6022
|
-
return node;
|
|
6023
|
-
}
|
|
6024
|
-
function pseudoRandom(seed) {
|
|
6025
|
-
let x = seed;
|
|
6026
|
-
x = (x >> 16 ^ x) * 73244475;
|
|
6027
|
-
x = (x >> 16 ^ x) * 73244475;
|
|
6028
|
-
x = x >> 16 ^ x;
|
|
6029
|
-
return x / 4294967295;
|
|
6030
|
-
}
|
|
6031
|
-
|
|
6032
|
-
// src/engine/DocumentModel.ts
|
|
6033
|
-
var DocumentModel = class {
|
|
6034
|
-
/**
|
|
6035
|
-
* @param maxLines - Ceiling on the line count, defaulting to the engine's
|
|
6036
|
-
* configured one. Every line costs a LineState with six arrays in it
|
|
6037
|
-
* whatever the line says, so the cost of a document is its line count and
|
|
6038
|
-
* nothing else bounds it: two hundred thousand lines of `1 + 1` exhausted
|
|
6039
|
-
* the heap here, before a single expression had been looked at.
|
|
6040
|
-
*/
|
|
6041
|
-
constructor(maxLines = DEFAULT_CONFIG.performance.maxDocumentLines) {
|
|
6042
|
-
/** Persistent line ID → LineState. */
|
|
6043
|
-
this.lines = /* @__PURE__ */ new Map();
|
|
6044
|
-
/** Order-statistic treap representing the current document line order. */
|
|
6045
|
-
this.orderTree = new SegmentTree();
|
|
6046
|
-
/** Monotonically increasing counter for new line IDs. */
|
|
6047
|
-
this.nextLineId = 1;
|
|
6048
|
-
/**
|
|
6049
|
-
* Lazy position cache: lineId → 1-based position.
|
|
6050
|
-
* Built on first `getLinePosition()` call, invalidated on structural edits.
|
|
6051
|
-
*/
|
|
6052
|
-
this._positionCache = null;
|
|
6053
|
-
/**
|
|
6054
|
-
* Line IDs currently marked dirty, maintained alongside every
|
|
6055
|
-
* `state.dirty` mutation (in this class and in every other module that
|
|
6056
|
-
* holds a direct `LineState` reference: ThreeTierEvaluator, PageManager).
|
|
6057
|
-
* Lets {@link hasAnyDirtyLineBefore} answer "is anything before position
|
|
6058
|
-
* X dirty" in O(d log N), d = current dirty count, typically tiny once
|
|
6059
|
-
* a document has settled after its initial evaluation, instead of
|
|
6060
|
-
* O(N log N), which used to mean every scroll event re-walked the WHOLE
|
|
6061
|
-
* document via `getLineAt()` regardless of how little of it was actually
|
|
6062
|
-
* dirty. Benchmarked: ~10.6ms per setViewport() call scrolled near the
|
|
6063
|
-
* bottom of a 20k-line document before this fix.
|
|
6064
|
-
*/
|
|
6065
|
-
this.dirtyLineIds = /* @__PURE__ */ new Set();
|
|
6066
|
-
this.maxLines = maxLines;
|
|
6067
|
-
}
|
|
6068
|
-
// ── Initialization ──────────────────────────────────────────────────
|
|
6069
5877
|
/**
|
|
6070
|
-
*
|
|
6071
|
-
*
|
|
6072
|
-
*
|
|
6073
|
-
*
|
|
6074
|
-
*
|
|
6075
|
-
*
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
5878
|
+
* Restore a snapshot produced by {@link ExpressionEngine.toJSON} onto a
|
|
5879
|
+
* fresh engine.
|
|
5880
|
+
*
|
|
5881
|
+
* The version gate runs first: a snapshot whose {@link EngineSnapshot.format}
|
|
5882
|
+
* or {@link EngineSnapshot.version} does not match this engine's reader is
|
|
5883
|
+
* refused with {@link SnapshotErrorCodes.SNAPSHOT_VERSION_MISMATCH} rather
|
|
5884
|
+
* than restored wrongly. The engine is then built with the snapshot's locale
|
|
5885
|
+
* (unless overridden) and the given `packages`, and its variable table,
|
|
5886
|
+
* user-function registry, line cache, dependency graph, and bytecode cache
|
|
5887
|
+
* are rehydrated from the snapshot.
|
|
5888
|
+
*
|
|
5889
|
+
* @param snapshot - A snapshot object, typically straight from `JSON.parse`.
|
|
5890
|
+
* @param options - Packages (must match those the snapshot was taken with),
|
|
5891
|
+
* plus optional config, diagnostic mode, and locale override. See
|
|
5892
|
+
* {@link EngineRestoreOptions}.
|
|
5893
|
+
* @returns A ready engine that behaves as though it had evaluated the
|
|
5894
|
+
* original document.
|
|
5895
|
+
* @throws {@link SnapshotErrorCodes.SNAPSHOT_VERSION_MISMATCH} for a missing
|
|
5896
|
+
* or mismatched envelope, and
|
|
5897
|
+
* {@link SnapshotErrorCodes.SNAPSHOT_MALFORMED} for internally
|
|
5898
|
+
* inconsistent contents.
|
|
5899
|
+
*/
|
|
5900
|
+
static fromJSON(snapshot, options = {}) {
|
|
5901
|
+
assertRestorable(snapshot);
|
|
5902
|
+
const locale = options.locale ?? snapshot.locale ?? "en";
|
|
5903
|
+
const engine = new _ExpressionEngine(locale, options.diagnosticMode ?? false, options.config, void 0, options.packages);
|
|
5904
|
+
engine.restoreSnapshot(snapshot);
|
|
5905
|
+
return engine;
|
|
5906
|
+
}
|
|
5907
|
+
/**
|
|
5908
|
+
* Rehydrate this engine's state from a snapshot whose envelope has already
|
|
5909
|
+
* been validated by {@link assertRestorable}. Instance-private, the public
|
|
5910
|
+
* entry point is the static {@link ExpressionEngine.fromJSON}.
|
|
5911
|
+
*/
|
|
5912
|
+
restoreSnapshot(snapshot) {
|
|
5913
|
+
for (const [name, sv] of Object.entries(snapshot.variables)) {
|
|
5914
|
+
this.vm.setVar(name, deserializeValue(sv));
|
|
5915
|
+
}
|
|
5916
|
+
for (const fn of snapshot.userFunctions) {
|
|
5917
|
+
const def = deserializeUserFunction(fn);
|
|
5918
|
+
this.vm.defineUserFunction(def.name, def.params, def.program);
|
|
5919
|
+
}
|
|
5920
|
+
for (const e of snapshot.lineCache) {
|
|
5921
|
+
const entry = new LineCacheEntry(
|
|
5922
|
+
deserializeValue(e.result),
|
|
5923
|
+
deserializeBytecode(e.bytecode),
|
|
5924
|
+
e.reads.slice(),
|
|
5925
|
+
e.writeVar
|
|
6084
5926
|
);
|
|
5927
|
+
this.lineCache.set(e.line, entry, e.expression);
|
|
5928
|
+
this.dag.registerLine(e.line, e.reads, e.writeVar ? [e.writeVar] : []);
|
|
6085
5929
|
}
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
this._positionCache = null;
|
|
6089
|
-
this.dirtyLineIds.clear();
|
|
6090
|
-
this.nextLineId = 1;
|
|
6091
|
-
const rawLines = text.split("\n");
|
|
6092
|
-
const lineIds = new Array(rawLines.length);
|
|
6093
|
-
for (let i = 0; i < rawLines.length; i++) {
|
|
6094
|
-
const lineId = this.nextLineId++;
|
|
6095
|
-
lineIds[i] = lineId;
|
|
6096
|
-
this.lines.set(lineId, {
|
|
6097
|
-
lineId,
|
|
6098
|
-
textHash: djb2Hash(rawLines[i]),
|
|
6099
|
-
text: rawLines[i],
|
|
6100
|
-
expressions: [],
|
|
6101
|
-
bytecodes: [],
|
|
6102
|
-
reads: [],
|
|
6103
|
-
writes: [],
|
|
6104
|
-
results: [],
|
|
6105
|
-
result: null,
|
|
6106
|
-
dirty: true,
|
|
6107
|
-
isVariableDef: false,
|
|
6108
|
-
isEmpty: rawLines[i].trim().length === 0,
|
|
6109
|
-
inlineSolveCount: 0
|
|
6110
|
-
});
|
|
5930
|
+
for (const { expression, program } of snapshot.bytecodeCache) {
|
|
5931
|
+
this.bytecodeCache.set(expression, deserializeBytecode(program));
|
|
6111
5932
|
}
|
|
6112
|
-
this.orderTree.replaceAll(lineIds);
|
|
6113
|
-
for (const id of lineIds) this.dirtyLineIds.add(id);
|
|
6114
5933
|
}
|
|
6115
|
-
|
|
5934
|
+
//#endregion
|
|
5935
|
+
//#region Public API, Incremental evaluation
|
|
6116
5936
|
/**
|
|
6117
|
-
*
|
|
6118
|
-
*
|
|
6119
|
-
*
|
|
6120
|
-
*
|
|
6121
|
-
*
|
|
6122
|
-
* change shifts the line numbers that the second change references.
|
|
6123
|
-
*
|
|
6124
|
-
* Changes are applied in **reverse order** (highest startLine first) so
|
|
6125
|
-
* that earlier changes in the document don't shift the indices of later
|
|
6126
|
-
* changes during processing.
|
|
6127
|
-
*
|
|
6128
|
-
* Returns both the newly inserted line IDs and the removed line IDs.
|
|
6129
|
-
* Callers should use `removed` to clean up the dependency graph and
|
|
6130
|
-
* other data structures keyed by lineId.
|
|
5937
|
+
* Incrementally re-evaluate lines affected by a variable change.
|
|
5938
|
+
* Walks the DAG from the changed variable to find exactly which lines
|
|
5939
|
+
* need re-execution, no dirty-set indirection, no sorting guesswork.
|
|
5940
|
+
* Uses Kahn's algorithm for topological ordering: producers always
|
|
5941
|
+
* execute before consumers, regardless of document line order.
|
|
6131
5942
|
*/
|
|
6132
|
-
|
|
6133
|
-
const
|
|
6134
|
-
|
|
6135
|
-
const
|
|
6136
|
-
for (const
|
|
6137
|
-
const
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
this.lines.set(lineId, {
|
|
6144
|
-
lineId,
|
|
6145
|
-
textHash: djb2Hash(text),
|
|
6146
|
-
text,
|
|
6147
|
-
expressions: [],
|
|
6148
|
-
bytecodes: [],
|
|
6149
|
-
reads: [],
|
|
6150
|
-
writes: [],
|
|
6151
|
-
results: [],
|
|
6152
|
-
result: null,
|
|
6153
|
-
dirty: true,
|
|
6154
|
-
isVariableDef: false,
|
|
6155
|
-
isEmpty: text.trim().length === 0,
|
|
6156
|
-
inlineSolveCount: 0
|
|
6157
|
-
});
|
|
5943
|
+
evaluateIncremental(variable, newValue) {
|
|
5944
|
+
const affectedLines = this.dag.getAffectedLinesInOrder(variable);
|
|
5945
|
+
this.vm.setVar(variable, numberValue(newValue));
|
|
5946
|
+
const updated = /* @__PURE__ */ new Map();
|
|
5947
|
+
for (const lineNumber of affectedLines) {
|
|
5948
|
+
const entry = this.lineCache.getEntryForLine(lineNumber);
|
|
5949
|
+
if (!entry || entry.bytecode.opcodes.length === 0) continue;
|
|
5950
|
+
const evalResult = this.executeRaw(entry.bytecode, lineNumber);
|
|
5951
|
+
if (evalResult.type === "pending") {
|
|
5952
|
+
void this.resolveAsync(evalResult);
|
|
5953
|
+
continue;
|
|
6158
5954
|
}
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
);
|
|
6165
|
-
for (const id of removedIds) {
|
|
6166
|
-
removed.push(id);
|
|
6167
|
-
this.lines.delete(id);
|
|
6168
|
-
this.dirtyLineIds.delete(id);
|
|
5955
|
+
if (evalResult.type === "error") {
|
|
5956
|
+
const value = errorValue(evalResult.error.code, evalResult.error.message);
|
|
5957
|
+
updated.set(lineNumber, value);
|
|
5958
|
+
entry.result = value;
|
|
5959
|
+
continue;
|
|
6169
5960
|
}
|
|
5961
|
+
const result = evalResult.value;
|
|
5962
|
+
updated.set(lineNumber, result);
|
|
5963
|
+
entry.result = result;
|
|
6170
5964
|
}
|
|
6171
|
-
|
|
6172
|
-
return { inserted, removed };
|
|
5965
|
+
return updated;
|
|
6173
5966
|
}
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
5967
|
+
//#endregion
|
|
5968
|
+
};
|
|
5969
|
+
/** The most nesting {@link makeLineContext}'s `evaluateLineWithBinding` allows before refusing, so the bisection re-runs can never compound. */
|
|
5970
|
+
_ExpressionEngine.GOAL_SEEK_MAX_NESTING_DEPTH = 1;
|
|
5971
|
+
var ExpressionEngine = _ExpressionEngine;
|
|
5972
|
+
|
|
5973
|
+
// src/workers/engine.worker.ts
|
|
5974
|
+
var engine_worker_default = (() => {
|
|
5975
|
+
throw new Error("engine.worker.ts was not processed by esbuild-plugin-inline-worker, so worker offload is unavailable");
|
|
5976
|
+
});
|
|
5977
|
+
var compileEngine = null;
|
|
5978
|
+
function getCompileEngine() {
|
|
5979
|
+
if (!compileEngine) {
|
|
5980
|
+
compileEngine = new ExpressionEngine("en", false);
|
|
6186
5981
|
}
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
startLine,
|
|
6194
|
-
deleteCount: endLine - startLine + 1,
|
|
6195
|
-
insertLines: []
|
|
6196
|
-
};
|
|
6197
|
-
const result = this.applyChanges([change]);
|
|
6198
|
-
return result.removed;
|
|
5982
|
+
return compileEngine;
|
|
5983
|
+
}
|
|
5984
|
+
var executeVm = null;
|
|
5985
|
+
function getExecuteVm() {
|
|
5986
|
+
if (!executeVm) {
|
|
5987
|
+
executeVm = createVM(sharedOpRegistry, 200, 5e4);
|
|
6199
5988
|
}
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
5989
|
+
return executeVm;
|
|
5990
|
+
}
|
|
5991
|
+
function compileOne(item) {
|
|
5992
|
+
const base = {
|
|
5993
|
+
lineId: item.lineId,
|
|
5994
|
+
expression: item.expression,
|
|
5995
|
+
compiledAgainstHash: item.textHash,
|
|
5996
|
+
strings: [],
|
|
5997
|
+
reads: [],
|
|
5998
|
+
writes: [],
|
|
5999
|
+
isVariableDef: false,
|
|
6000
|
+
error: null
|
|
6001
|
+
};
|
|
6002
|
+
try {
|
|
6003
|
+
const eng = getCompileEngine();
|
|
6004
|
+
const { program, reads, writes } = eng.compileExpression(item.expression);
|
|
6005
|
+
const isVariableDef = writes.length > 0;
|
|
6006
|
+
const opcodes = program.opcodes;
|
|
6007
|
+
const numbers = program.numbers;
|
|
6008
|
+
const opcodesBuffer = opcodes.buffer.slice(
|
|
6009
|
+
opcodes.byteOffset,
|
|
6010
|
+
opcodes.byteOffset + opcodes.byteLength
|
|
6011
|
+
);
|
|
6012
|
+
const numbersBuffer = numbers.buffer.slice(
|
|
6013
|
+
numbers.byteOffset,
|
|
6014
|
+
numbers.byteOffset + numbers.byteLength
|
|
6015
|
+
);
|
|
6016
|
+
return {
|
|
6017
|
+
...base,
|
|
6018
|
+
opcodesBuffer,
|
|
6019
|
+
numbersBuffer,
|
|
6020
|
+
opcodesLength: opcodes.length,
|
|
6021
|
+
numbersLength: numbers.length,
|
|
6022
|
+
strings: [...program.strings],
|
|
6023
|
+
reads,
|
|
6024
|
+
writes,
|
|
6025
|
+
isVariableDef
|
|
6026
|
+
};
|
|
6027
|
+
} catch (e) {
|
|
6028
|
+
return {
|
|
6029
|
+
...base,
|
|
6030
|
+
opcodesBuffer: new ArrayBuffer(0),
|
|
6031
|
+
numbersBuffer: new ArrayBuffer(0),
|
|
6032
|
+
opcodesLength: 0,
|
|
6033
|
+
numbersLength: 0,
|
|
6034
|
+
error: e instanceof Error ? e.message : String(e)
|
|
6035
|
+
};
|
|
6223
6036
|
}
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
const
|
|
6230
|
-
|
|
6231
|
-
if (
|
|
6232
|
-
|
|
6037
|
+
}
|
|
6038
|
+
function handleCompileBatch(msg) {
|
|
6039
|
+
const results = [];
|
|
6040
|
+
const transferList = [];
|
|
6041
|
+
for (const item of msg.items) {
|
|
6042
|
+
const result = compileOne(item);
|
|
6043
|
+
results.push(result);
|
|
6044
|
+
if (result.opcodesBuffer.byteLength > 0) transferList.push(result.opcodesBuffer);
|
|
6045
|
+
if (result.numbersBuffer.byteLength > 0) transferList.push(result.numbersBuffer);
|
|
6233
6046
|
}
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6047
|
+
self.postMessage(
|
|
6048
|
+
{ id: msg.id, type: "COMPILE_RESULT", results },
|
|
6049
|
+
transferList
|
|
6050
|
+
);
|
|
6051
|
+
}
|
|
6052
|
+
function executeOne(item) {
|
|
6053
|
+
const vmm = getExecuteVm();
|
|
6054
|
+
vmm.reset();
|
|
6055
|
+
const opcodes = new Uint8Array(item.opcodesBuffer, 0, item.opcodesLength);
|
|
6056
|
+
const numbers = new Float64Array(item.numbersBuffer, 0, item.numbersLength);
|
|
6057
|
+
const bytecode = { opcodes, numbers, strings: item.strings };
|
|
6058
|
+
try {
|
|
6059
|
+
const result = executeBytecode(bytecode, vmm);
|
|
6060
|
+
if (result.type === "pending") {
|
|
6061
|
+
return {
|
|
6062
|
+
lineNumber: item.lineNumber,
|
|
6063
|
+
valueType: 12 /* Pending */,
|
|
6064
|
+
value: 0,
|
|
6065
|
+
isPending: true,
|
|
6066
|
+
queryKey: result.queryKey
|
|
6067
|
+
};
|
|
6244
6068
|
}
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6069
|
+
if (result.type === "error") {
|
|
6070
|
+
return {
|
|
6071
|
+
lineNumber: item.lineNumber,
|
|
6072
|
+
valueType: 13 /* Error */,
|
|
6073
|
+
value: 0,
|
|
6074
|
+
isPending: false,
|
|
6075
|
+
unit: result.error.message
|
|
6076
|
+
};
|
|
6249
6077
|
}
|
|
6250
|
-
|
|
6078
|
+
const val = result.value;
|
|
6079
|
+
return {
|
|
6080
|
+
lineNumber: item.lineNumber,
|
|
6081
|
+
valueType: val.type,
|
|
6082
|
+
value: typeof val.value === "number" ? val.value : typeof val.value === "bigint" ? Number(val.value) : 0,
|
|
6083
|
+
unit: val.unit,
|
|
6084
|
+
isPending: false
|
|
6085
|
+
};
|
|
6086
|
+
} catch {
|
|
6087
|
+
return {
|
|
6088
|
+
lineNumber: item.lineNumber,
|
|
6089
|
+
valueType: 13 /* Error */,
|
|
6090
|
+
value: 0,
|
|
6091
|
+
isPending: false,
|
|
6092
|
+
unit: "Worker execution failed"
|
|
6093
|
+
};
|
|
6251
6094
|
}
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
getVisibleLines(startLine, endLine) {
|
|
6258
|
-
const lineIds = this.orderTree.getRange(startLine - 1, endLine - 1);
|
|
6259
|
-
const result = [];
|
|
6260
|
-
for (const lineId of lineIds) {
|
|
6261
|
-
const state = this.lines.get(lineId);
|
|
6262
|
-
if (state) result.push(state);
|
|
6263
|
-
}
|
|
6264
|
-
return result;
|
|
6095
|
+
}
|
|
6096
|
+
function handleExecuteBatch(msg) {
|
|
6097
|
+
const results = [];
|
|
6098
|
+
for (const item of msg.items) {
|
|
6099
|
+
results.push(executeOne(item));
|
|
6265
6100
|
}
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6101
|
+
self.postMessage({
|
|
6102
|
+
id: msg.id,
|
|
6103
|
+
type: "EXECUTE_RESULT",
|
|
6104
|
+
results
|
|
6105
|
+
});
|
|
6106
|
+
}
|
|
6107
|
+
var inWorkerScope = typeof self !== "undefined" && typeof window === "undefined";
|
|
6108
|
+
var handleMessage = (event) => {
|
|
6109
|
+
if (event.origin && self.location && event.origin !== self.location.origin) return;
|
|
6110
|
+
const msg = event.data;
|
|
6111
|
+
switch (msg.type) {
|
|
6112
|
+
case "COMPILE_BATCH":
|
|
6113
|
+
handleCompileBatch(msg);
|
|
6114
|
+
break;
|
|
6115
|
+
case "EXECUTE_BATCH":
|
|
6116
|
+
handleExecuteBatch(msg);
|
|
6117
|
+
break;
|
|
6118
|
+
case "TERMINATE": {
|
|
6119
|
+
if (compileEngine) {
|
|
6120
|
+
compileEngine.clear();
|
|
6121
|
+
compileEngine = null;
|
|
6122
|
+
}
|
|
6123
|
+
if (executeVm) {
|
|
6124
|
+
executeVm.reset();
|
|
6125
|
+
executeVm = null;
|
|
6126
|
+
}
|
|
6127
|
+
self.postMessage({
|
|
6128
|
+
id: msg.id,
|
|
6129
|
+
type: "COMPILE_RESULT",
|
|
6130
|
+
results: []
|
|
6131
|
+
});
|
|
6132
|
+
break;
|
|
6133
|
+
}
|
|
6134
|
+
default: {
|
|
6135
|
+
self.postMessage({
|
|
6136
|
+
id: -1,
|
|
6137
|
+
type: "COMPILE_RESULT",
|
|
6138
|
+
results: [],
|
|
6139
|
+
error: `Unknown message type: ${msg.type}`
|
|
6140
|
+
});
|
|
6141
|
+
}
|
|
6271
6142
|
}
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6143
|
+
};
|
|
6144
|
+
if (inWorkerScope) {
|
|
6145
|
+
self.onmessage = handleMessage;
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
// src/engine/ExecutionPool.ts
|
|
6149
|
+
var WORKER_BATCH_TIMEOUT_MS = 3e4;
|
|
6150
|
+
var WORKER_OFFLOAD_THRESHOLD = 50;
|
|
6151
|
+
var ExecutionPool = class {
|
|
6152
|
+
constructor(poolSize) {
|
|
6153
|
+
this.workers = [];
|
|
6154
|
+
this.nextWorker = 0;
|
|
6155
|
+
this.nextId = 1;
|
|
6156
|
+
this.pendingBatches = /* @__PURE__ */ new Map();
|
|
6157
|
+
this.terminated = false;
|
|
6158
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
6159
|
+
/** Cached availability flag. Checked once; createExecutionWorker never called again. */
|
|
6160
|
+
this._available = null;
|
|
6161
|
+
this.poolSize = Math.min(poolSize ?? this.defaultPoolSize(), 4);
|
|
6277
6162
|
}
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
getDirtyLines() {
|
|
6282
|
-
const result = [];
|
|
6283
|
-
for (const state of this.lines.values()) {
|
|
6284
|
-
if (state.dirty) result.push(state);
|
|
6163
|
+
defaultPoolSize() {
|
|
6164
|
+
if (typeof navigator !== "undefined" && navigator.hardwareConcurrency) {
|
|
6165
|
+
return Math.min(navigator.hardwareConcurrency, 4);
|
|
6285
6166
|
}
|
|
6286
|
-
return
|
|
6167
|
+
return 2;
|
|
6287
6168
|
}
|
|
6288
6169
|
/**
|
|
6289
|
-
* Whether
|
|
6290
|
-
*
|
|
6291
|
-
*
|
|
6292
|
-
* checkpoint state might be stale and a full evaluate() (from line 1) is
|
|
6293
|
-
* needed instead of the cheap viewport-only path.
|
|
6294
|
-
*
|
|
6295
|
-
* O(d log N) where d = current dirty line count via {@link dirtyLineIds},
|
|
6296
|
-
* not O(N log N), a document that's mostly clean (the steady state after
|
|
6297
|
-
* initial load) answers this in the cost of resolving a handful of
|
|
6298
|
-
* lineIds to positions, not walking every line up to `position`.
|
|
6170
|
+
* Whether worker-based execution is available.
|
|
6171
|
+
* Returns false in Node.js / jsdom test environments where Worker
|
|
6172
|
+
* may be polyfilled but execution workers aren't functional.
|
|
6299
6173
|
*/
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6174
|
+
isAvailable() {
|
|
6175
|
+
if (this.terminated) return false;
|
|
6176
|
+
if (this._available !== null) return this._available;
|
|
6177
|
+
if (typeof Worker === "undefined") {
|
|
6178
|
+
this._available = false;
|
|
6179
|
+
return false;
|
|
6304
6180
|
}
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
* VMCheckpoints.ts), so a dirty plain-expression line before the viewport
|
|
6314
|
-
* cannot have invalidated any checkpoint, there's no checkpoint entry
|
|
6315
|
-
* for it to invalidate. Only a dirty variable-def line can mean the VM
|
|
6316
|
-
* state a checkpoint would restore is stale.
|
|
6317
|
-
*
|
|
6318
|
-
* This distinction matters because `PageManager.evictPageBytecode()`
|
|
6319
|
-
* marks evicted non-variable-def lines dirty (so they get Tier 1 if
|
|
6320
|
-
* scrolled back into view), and Tier 3's compile-only path never clears
|
|
6321
|
-
* `dirty` for non-variable-def lines by design. Using the broader
|
|
6322
|
-
* `hasAnyDirtyLineBefore` here meant scrolling far into a large,
|
|
6323
|
-
* variable-def-free document would trip `setViewport()`'s fallback to
|
|
6324
|
-
* `evaluate()` on every single call, evaluate() reprocesses the evicted
|
|
6325
|
-
* lines via Tier 3, which recompiles their bytecode without clearing
|
|
6326
|
-
* dirty, so the very next `maintainAfterEval()` re-evicts and re-dirties
|
|
6327
|
-
* the same lines, forever re-triggering the fallback on an otherwise
|
|
6328
|
-
* unchanged viewport.
|
|
6329
|
-
*/
|
|
6330
|
-
hasAnyDirtyVariableDefLineBefore(position) {
|
|
6331
|
-
for (const lineId of this.dirtyLineIds) {
|
|
6332
|
-
const state = this.lines.get(lineId);
|
|
6333
|
-
if (!state || !state.isVariableDef) continue;
|
|
6334
|
-
const pos = this.getLinePosition(lineId);
|
|
6335
|
-
if (pos >= 1 && pos < position) return true;
|
|
6181
|
+
try {
|
|
6182
|
+
const w = engine_worker_default();
|
|
6183
|
+
w.terminate();
|
|
6184
|
+
this._available = true;
|
|
6185
|
+
return true;
|
|
6186
|
+
} catch {
|
|
6187
|
+
this._available = false;
|
|
6188
|
+
return false;
|
|
6336
6189
|
}
|
|
6337
|
-
return false;
|
|
6338
6190
|
}
|
|
6339
|
-
/** Number of lines currently marked dirty. For diagnostics/tests. */
|
|
6340
|
-
get dirtyCount() {
|
|
6341
|
-
return this.dirtyLineIds.size;
|
|
6342
|
-
}
|
|
6343
|
-
// ── Thread-safety validation ────────────────────────────────────────
|
|
6344
6191
|
/**
|
|
6345
|
-
*
|
|
6192
|
+
* Execute a batch of line entries via the worker pool.
|
|
6346
6193
|
*
|
|
6347
|
-
*
|
|
6348
|
-
*
|
|
6349
|
-
*
|
|
6350
|
-
* main thread check whether the bytecode is still applicable.
|
|
6194
|
+
* Takes ordered line numbers and their LineCache entries. Clones bytecode
|
|
6195
|
+
* ArrayBuffers for transfer, dispatches to workers round-robin, and
|
|
6196
|
+
* returns the serialized results (with 30s timeout fallback).
|
|
6351
6197
|
*
|
|
6352
|
-
*
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
return state !== void 0 && state.textHash === compiledAgainstHash;
|
|
6357
|
-
}
|
|
6358
|
-
// ── State mutations ─────────────────────────────────────────────────
|
|
6359
|
-
/**
|
|
6360
|
-
* Mark a line as clean (re-evaluated successfully).
|
|
6198
|
+
* Falls back to undefined when workers are unavailable, caller should
|
|
6199
|
+
* use the main-thread path.
|
|
6200
|
+
*
|
|
6201
|
+
* @returns ExecuteResult[] on success, undefined if workers unavailable.
|
|
6361
6202
|
*/
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
if (
|
|
6365
|
-
|
|
6366
|
-
|
|
6203
|
+
executeBatch(orderedLineNumbers, entries) {
|
|
6204
|
+
if (!this.isAvailable()) return void 0;
|
|
6205
|
+
if (orderedLineNumbers.length === 0) return Promise.resolve([]);
|
|
6206
|
+
this.ensureWorkers();
|
|
6207
|
+
const items = [];
|
|
6208
|
+
for (const lineNumber of orderedLineNumbers) {
|
|
6209
|
+
const entry = entries.get(lineNumber);
|
|
6210
|
+
if (!entry || entry.bytecode.opcodes.length === 0) continue;
|
|
6211
|
+
const opcodes = entry.bytecode.opcodes;
|
|
6212
|
+
const numbers = entry.bytecode.numbers;
|
|
6213
|
+
const opcodesClone = opcodes.buffer.slice(
|
|
6214
|
+
opcodes.byteOffset,
|
|
6215
|
+
opcodes.byteOffset + opcodes.byteLength
|
|
6216
|
+
);
|
|
6217
|
+
const numbersClone = numbers.buffer.slice(
|
|
6218
|
+
numbers.byteOffset,
|
|
6219
|
+
numbers.byteOffset + numbers.byteLength
|
|
6220
|
+
);
|
|
6221
|
+
items.push({
|
|
6222
|
+
lineNumber,
|
|
6223
|
+
opcodesBuffer: opcodesClone,
|
|
6224
|
+
numbersBuffer: numbersClone,
|
|
6225
|
+
opcodesLength: opcodes.length,
|
|
6226
|
+
numbersLength: numbers.length,
|
|
6227
|
+
strings: [...entry.bytecode.strings]
|
|
6228
|
+
});
|
|
6367
6229
|
}
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
markDirtyByLineNumber(lineNumber) {
|
|
6374
|
-
const state = this.getLineAt(lineNumber);
|
|
6375
|
-
if (state) {
|
|
6376
|
-
state.dirty = true;
|
|
6377
|
-
this.dirtyLineIds.add(state.lineId);
|
|
6230
|
+
if (items.length === 0) return Promise.resolve([]);
|
|
6231
|
+
const transferList = [];
|
|
6232
|
+
for (const item of items) {
|
|
6233
|
+
if (item.opcodesBuffer.byteLength > 0) transferList.push(item.opcodesBuffer);
|
|
6234
|
+
if (item.numbersBuffer.byteLength > 0) transferList.push(item.numbersBuffer);
|
|
6378
6235
|
}
|
|
6236
|
+
const batchId = this.nextId++;
|
|
6237
|
+
const worker = this.getNextWorker();
|
|
6238
|
+
return new Promise((resolve) => {
|
|
6239
|
+
const timeoutId = setTimeout(() => {
|
|
6240
|
+
this.pendingBatches.delete(batchId);
|
|
6241
|
+
resolve([]);
|
|
6242
|
+
}, WORKER_BATCH_TIMEOUT_MS);
|
|
6243
|
+
this.pendingBatches.set(batchId, {
|
|
6244
|
+
id: batchId,
|
|
6245
|
+
lineNumbers: orderedLineNumbers,
|
|
6246
|
+
resolve: (results) => {
|
|
6247
|
+
clearTimeout(timeoutId);
|
|
6248
|
+
resolve(results);
|
|
6249
|
+
}
|
|
6250
|
+
});
|
|
6251
|
+
worker.postMessage(
|
|
6252
|
+
{ type: "EXECUTE_BATCH", id: batchId, items },
|
|
6253
|
+
transferList
|
|
6254
|
+
);
|
|
6255
|
+
});
|
|
6379
6256
|
}
|
|
6380
6257
|
/**
|
|
6381
|
-
*
|
|
6258
|
+
* Clean up all workers and pending batches. Called on engine clear.
|
|
6259
|
+
* Workers are recreated lazily on next dispatch.
|
|
6382
6260
|
*/
|
|
6383
|
-
|
|
6384
|
-
const
|
|
6385
|
-
|
|
6386
|
-
state.dirty = true;
|
|
6387
|
-
this.dirtyLineIds.add(lineId);
|
|
6261
|
+
clear() {
|
|
6262
|
+
for (const [, batch] of this.pendingBatches) {
|
|
6263
|
+
batch.resolve([]);
|
|
6388
6264
|
}
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
*/
|
|
6393
|
-
invalidateAll() {
|
|
6394
|
-
for (const state of this.lines.values()) {
|
|
6395
|
-
state.dirty = true;
|
|
6265
|
+
this.pendingBatches.clear();
|
|
6266
|
+
for (const w of this.workers) {
|
|
6267
|
+
w.terminate();
|
|
6396
6268
|
}
|
|
6397
|
-
this.
|
|
6398
|
-
|
|
6399
|
-
/**
|
|
6400
|
-
* Update a line's evaluation state after successful execution (Tier 1 / Tier 2).
|
|
6401
|
-
*
|
|
6402
|
-
* Sets results, bytecodes, reads, writes, and marks the line clean.
|
|
6403
|
-
* Supports multi-expression lines (inline solves) via parallel arrays.
|
|
6404
|
-
*
|
|
6405
|
-
* @param lineId - Persistent line identifier.
|
|
6406
|
-
* @param results - Evaluation result groups for each expression (in order). Each element is a Value[].
|
|
6407
|
-
* @param bytecodes - Compiled bytecode for each expression (in order).
|
|
6408
|
-
* @param expressions - Extracted expression strings (in order).
|
|
6409
|
-
* @param reads - Aggregated read variables across all expressions.
|
|
6410
|
-
* @param writes - Aggregated write variables across all expressions.
|
|
6411
|
-
* @param isVariableDef - True if any expression defines a variable.
|
|
6412
|
-
* @param inlineSolveCount - Number of inline solves (0 for full-line).
|
|
6413
|
-
*/
|
|
6414
|
-
updateLineResult(lineId, results, bytecodes, expressions, reads, writes, isVariableDef, inlineSolveCount = 0) {
|
|
6415
|
-
const state = this.lines.get(lineId);
|
|
6416
|
-
if (!state) return;
|
|
6417
|
-
state.results = results;
|
|
6418
|
-
state.result = results[0]?.[0] ?? null;
|
|
6419
|
-
state.bytecodes = bytecodes;
|
|
6420
|
-
state.expressions = expressions;
|
|
6421
|
-
state.reads = reads;
|
|
6422
|
-
state.writes = writes;
|
|
6423
|
-
state.isVariableDef = isVariableDef;
|
|
6424
|
-
state.inlineSolveCount = inlineSolveCount;
|
|
6425
|
-
state.dirty = false;
|
|
6426
|
-
this.dirtyLineIds.delete(lineId);
|
|
6269
|
+
this.workers = [];
|
|
6270
|
+
this.nextWorker = 0;
|
|
6427
6271
|
}
|
|
6428
6272
|
/**
|
|
6429
|
-
*
|
|
6430
|
-
*
|
|
6431
|
-
* Stores expressions, bytecodes, reads, and writes. Does NOT set results
|
|
6432
|
-
* and does NOT mark the line clean, it still needs execution (Tier 1 or
|
|
6433
|
-
* Tier 2) to produce results. This distinction allows the three-tier
|
|
6434
|
-
* evaluation strategy: compile invisible lines in the background without
|
|
6435
|
-
* executing them, then execute from cached bytecode when scrolled into view.
|
|
6436
|
-
*
|
|
6437
|
-
* @param lineId - Persistent line identifier.
|
|
6438
|
-
* @param expressions - Extracted expression strings (in order).
|
|
6439
|
-
* @param bytecodes - Compiled bytecode for each expression (in order).
|
|
6440
|
-
* @param reads - Aggregated read variables across all expressions.
|
|
6441
|
-
* @param writes - Aggregated write variables across all expressions.
|
|
6442
|
-
* @param isVariableDef - True if any expression defines a variable.
|
|
6443
|
-
* @param inlineSolveCount - Number of inline solves (0 for full-line).
|
|
6273
|
+
* Full teardown. After destroy(), the pool is permanently unusable.
|
|
6444
6274
|
*/
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
state.expressions = expressions;
|
|
6449
|
-
state.bytecodes = bytecodes;
|
|
6450
|
-
state.reads = reads;
|
|
6451
|
-
state.writes = writes;
|
|
6452
|
-
state.isVariableDef = isVariableDef;
|
|
6453
|
-
state.inlineSolveCount = inlineSolveCount;
|
|
6454
|
-
}
|
|
6455
|
-
// ── Properties ──────────────────────────────────────────────────────
|
|
6456
|
-
get lineCount() {
|
|
6457
|
-
return this.orderTree.length;
|
|
6458
|
-
}
|
|
6459
|
-
get isEmpty() {
|
|
6460
|
-
return this.orderTree.isEmpty;
|
|
6275
|
+
destroy() {
|
|
6276
|
+
this.terminated = true;
|
|
6277
|
+
this.clear();
|
|
6461
6278
|
}
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6279
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
6280
|
+
ensureWorkers() {
|
|
6281
|
+
if (this.workers.length > 0) return;
|
|
6282
|
+
for (let i = 0; i < this.poolSize; i++) {
|
|
6283
|
+
const worker = engine_worker_default();
|
|
6284
|
+
worker.onmessage = (event) => {
|
|
6285
|
+
this.handleWorkerMessage(event.data);
|
|
6286
|
+
};
|
|
6287
|
+
worker.onerror = (err) => {
|
|
6288
|
+
console.error(`[ExecutionPool] Worker ${i} error:`, err.message);
|
|
6289
|
+
for (const [id, batch] of this.pendingBatches) {
|
|
6290
|
+
batch.resolve([]);
|
|
6291
|
+
this.pendingBatches.delete(id);
|
|
6292
|
+
}
|
|
6293
|
+
};
|
|
6294
|
+
this.workers.push(worker);
|
|
6469
6295
|
}
|
|
6470
6296
|
}
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
this.
|
|
6474
|
-
|
|
6475
|
-
this._positionCache = null;
|
|
6476
|
-
this.nextLineId = 1;
|
|
6297
|
+
getNextWorker() {
|
|
6298
|
+
const w = this.workers[this.nextWorker];
|
|
6299
|
+
this.nextWorker = (this.nextWorker + 1) % this.workers.length;
|
|
6300
|
+
return w;
|
|
6477
6301
|
}
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
lines: this.getAllLines().map((s) => ({
|
|
6485
|
-
lineId: s.lineId,
|
|
6486
|
-
text: s.text.substring(0, 80),
|
|
6487
|
-
// truncate for readability
|
|
6488
|
-
textHash: s.textHash,
|
|
6489
|
-
dirty: s.dirty,
|
|
6490
|
-
isVariableDef: s.isVariableDef,
|
|
6491
|
-
isEmpty: s.isEmpty,
|
|
6492
|
-
hasBytecode: s.bytecodes.length > 0,
|
|
6493
|
-
hasResult: s.results.length > 0,
|
|
6494
|
-
inlineSolveCount: s.inlineSolveCount,
|
|
6495
|
-
reads: s.reads,
|
|
6496
|
-
writes: s.writes
|
|
6497
|
-
}))
|
|
6498
|
-
};
|
|
6302
|
+
handleWorkerMessage(data) {
|
|
6303
|
+
if (data.type !== "EXECUTE_RESULT") return;
|
|
6304
|
+
const batch = this.pendingBatches.get(data.id);
|
|
6305
|
+
if (!batch) return;
|
|
6306
|
+
this.pendingBatches.delete(data.id);
|
|
6307
|
+
batch.resolve(data.results);
|
|
6499
6308
|
}
|
|
6500
6309
|
};
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
};
|
|
6528
|
-
this.worker.onerror = (err) => {
|
|
6529
|
-
for (const [id, pending] of this.pending) {
|
|
6530
|
-
pending.reject(
|
|
6531
|
-
new Error(`Compilation worker error: ${err.message}`)
|
|
6532
|
-
);
|
|
6533
|
-
this.pending.delete(id);
|
|
6534
|
-
}
|
|
6535
|
-
};
|
|
6536
|
-
return this.worker;
|
|
6310
|
+
function reconstructValue(result) {
|
|
6311
|
+
switch (result.valueType) {
|
|
6312
|
+
case 0 /* Number */:
|
|
6313
|
+
return numberValue(result.value);
|
|
6314
|
+
case 1 /* Hex */:
|
|
6315
|
+
return hexValue(result.value);
|
|
6316
|
+
case 2 /* BigInt */:
|
|
6317
|
+
return bigIntValue(BigInt(result.value));
|
|
6318
|
+
case 3 /* String */:
|
|
6319
|
+
return stringValue(String(result.value));
|
|
6320
|
+
case 4 /* Datetime */:
|
|
6321
|
+
return datetimeValue(result.value);
|
|
6322
|
+
case 5 /* Percentage */:
|
|
6323
|
+
return percentageValue(result.value);
|
|
6324
|
+
case 6 /* Uom */:
|
|
6325
|
+
return uomValue(result.value, result.unit ?? "");
|
|
6326
|
+
case 7 /* Matrix */:
|
|
6327
|
+
return numberValue(result.value);
|
|
6328
|
+
case 10 /* Boolean */:
|
|
6329
|
+
return boolValue(result.value !== 0);
|
|
6330
|
+
case 12 /* Pending */:
|
|
6331
|
+
return pendingValue(result.queryKey ?? "");
|
|
6332
|
+
case 13 /* Error */:
|
|
6333
|
+
return errorValue("WORKER_EXECUTION_ERROR", result.unit ?? "Unknown worker error");
|
|
6334
|
+
default:
|
|
6335
|
+
return numberValue(result.value);
|
|
6537
6336
|
}
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6337
|
+
}
|
|
6338
|
+
|
|
6339
|
+
// src/engine/AsyncResolutionBatcher.ts
|
|
6340
|
+
var _AsyncResolutionBatcher = class _AsyncResolutionBatcher {
|
|
6341
|
+
constructor(dag, lineCache, vm, highWaterMark = _AsyncResolutionBatcher.DEFAULT_HIGH_WATER_MARK) {
|
|
6342
|
+
this.pending = [];
|
|
6343
|
+
this.scheduled = false;
|
|
6344
|
+
/** Set to true by clearPending(), flush() checks this to abort stale work. */
|
|
6345
|
+
this.cleared = false;
|
|
6346
|
+
/**
|
|
6347
|
+
* Worker pool for offloading VM re-execution when the affected line
|
|
6348
|
+
* count exceeds WORKER_OFFLOAD_THRESHOLD (50). Lazily created on
|
|
6349
|
+
* first dispatch; cleared on clearAll().
|
|
6350
|
+
*/
|
|
6351
|
+
this.executionPool = null;
|
|
6352
|
+
/**
|
|
6353
|
+
* Number of flushes actually dispatched to the worker pool (as opposed
|
|
6354
|
+
* to falling back to the main thread because Worker is unavailable).
|
|
6355
|
+
* Exposed via {@link workerOffloadCount} for the Workers diagnostic tab.
|
|
6356
|
+
*/
|
|
6357
|
+
this.workerOffloadDispatchCount = 0;
|
|
6358
|
+
/**
|
|
6359
|
+
* Controller for the internal event stream. Set during stream
|
|
6360
|
+
* initialization; cleared on stream cancellation or clearAll().
|
|
6361
|
+
*/
|
|
6362
|
+
this._streamController = null;
|
|
6363
|
+
/**
|
|
6364
|
+
* Test-only synchronous capture array. When enabled (non-null), every
|
|
6365
|
+
* event is synchronously pushed here in addition to the stream.
|
|
6366
|
+
* Tests read from this array to avoid async stream reader timing issues.
|
|
6367
|
+
*/
|
|
6368
|
+
this._testCaptures = null;
|
|
6369
|
+
/**
|
|
6370
|
+
* Called for each line whose result is patched after an async resolution,
|
|
6371
|
+
* on both the main-thread and worker-pool paths.
|
|
6372
|
+
*
|
|
6373
|
+
* **A host that displays async results must set this.** It is the only
|
|
6374
|
+
* mechanism that moves a resolved value out of the LineCache and into the
|
|
6375
|
+
* host's own document state. The engine cannot do it itself: it does not own
|
|
6376
|
+
* a document, the host does, and the batcher has no reference to one.
|
|
6377
|
+
*
|
|
6378
|
+
* Nullable rather than a constructor parameter because it is cleared by
|
|
6379
|
+
* `clearAll()` and re-wired on re-subscribe, so it cannot be readonly. That
|
|
6380
|
+
* makes it easy to miss, which is why {@link warnIfUnwired} exists: leaving
|
|
6381
|
+
* it unset means async values resolve into the cache and are never shown,
|
|
6382
|
+
* with nothing to indicate why. A host that genuinely does not want async
|
|
6383
|
+
* results should not register async resolvers at all.
|
|
6384
|
+
*/
|
|
6385
|
+
this.onLineResult = null;
|
|
6386
|
+
/**
|
|
6387
|
+
* Whether {@link warnIfUnwired} has already fired.
|
|
6388
|
+
*
|
|
6389
|
+
* Once per instance, not once per resolution. A document with fifty live
|
|
6390
|
+
* data lines would otherwise produce fifty identical warnings on the first
|
|
6391
|
+
* fetch, which is how a useful warning becomes noise people filter out.
|
|
6392
|
+
*/
|
|
6393
|
+
this.warnedAboutMissingHook = false;
|
|
6394
|
+
this.dag = dag;
|
|
6395
|
+
this.lineCache = lineCache;
|
|
6396
|
+
this.vm = vm;
|
|
6397
|
+
this.highWaterMark = highWaterMark;
|
|
6398
|
+
this._eventStream = this.createEventStream();
|
|
6562
6399
|
}
|
|
6563
6400
|
/**
|
|
6564
|
-
*
|
|
6565
|
-
* validation. Only stores bytecode for lines whose text has not changed
|
|
6566
|
-
* since the compilation request was dispatched.
|
|
6567
|
-
*
|
|
6568
|
-
* Results for the same lineId are batched, all bytecodes from
|
|
6569
|
-
* successful compilations are passed to updateLineCompiled in a single
|
|
6570
|
-
* call. This supports multi-expression lines (inline solves).
|
|
6401
|
+
* Warn once if an async result resolved with no {@link onLineResult} wired.
|
|
6571
6402
|
*
|
|
6572
|
-
*
|
|
6573
|
-
*
|
|
6574
|
-
*
|
|
6575
|
-
*/
|
|
6576
|
-
storeResults(results, doc) {
|
|
6577
|
-
const byLineId = /* @__PURE__ */ new Map();
|
|
6578
|
-
for (const result of results) {
|
|
6579
|
-
if (result.error) continue;
|
|
6580
|
-
const state = doc.getLineById(result.lineId);
|
|
6581
|
-
if (!state) continue;
|
|
6582
|
-
if (state.textHash !== result.compiledAgainstHash) continue;
|
|
6583
|
-
let batch = byLineId.get(result.lineId);
|
|
6584
|
-
if (!batch) {
|
|
6585
|
-
batch = {
|
|
6586
|
-
bytecodes: [],
|
|
6587
|
-
reads: /* @__PURE__ */ new Set(),
|
|
6588
|
-
writes: /* @__PURE__ */ new Set(),
|
|
6589
|
-
isVariableDef: false,
|
|
6590
|
-
expressions: state.expressions.length > 0 ? [...state.expressions] : [],
|
|
6591
|
-
textHash: result.compiledAgainstHash
|
|
6592
|
-
};
|
|
6593
|
-
byLineId.set(result.lineId, batch);
|
|
6594
|
-
}
|
|
6595
|
-
batch.bytecodes.push(result.program);
|
|
6596
|
-
for (const r of result.reads) batch.reads.add(r);
|
|
6597
|
-
for (const w of result.writes) batch.writes.add(w);
|
|
6598
|
-
if (result.isVariableDef) batch.isVariableDef = true;
|
|
6599
|
-
}
|
|
6600
|
-
let stored = 0;
|
|
6601
|
-
for (const [lineId, batch] of byLineId) {
|
|
6602
|
-
if (batch.bytecodes.length === 0) continue;
|
|
6603
|
-
const state = doc.getLineById(lineId);
|
|
6604
|
-
const inlineSolveCount = state?.inlineSolveCount ?? 0;
|
|
6605
|
-
doc.updateLineCompiled(
|
|
6606
|
-
lineId,
|
|
6607
|
-
batch.expressions,
|
|
6608
|
-
batch.bytecodes,
|
|
6609
|
-
[...batch.reads],
|
|
6610
|
-
[...batch.writes],
|
|
6611
|
-
batch.isVariableDef,
|
|
6612
|
-
inlineSolveCount
|
|
6613
|
-
);
|
|
6614
|
-
stored += batch.bytecodes.length;
|
|
6615
|
-
}
|
|
6616
|
-
return stored;
|
|
6617
|
-
}
|
|
6618
|
-
/**
|
|
6619
|
-
* Terminate the worker and clean up.
|
|
6403
|
+
* The failure this catches is silent by nature: the value arrives, the cache
|
|
6404
|
+
* updates, and the line keeps showing pending forever. Without this a host
|
|
6405
|
+
* author has no thread to pull on.
|
|
6620
6406
|
*/
|
|
6621
|
-
|
|
6622
|
-
if (this.
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6407
|
+
warnIfUnwired() {
|
|
6408
|
+
if (this.onLineResult || this.warnedAboutMissingHook) return;
|
|
6409
|
+
this.warnedAboutMissingHook = true;
|
|
6410
|
+
console.warn(
|
|
6411
|
+
"[solve-engine] An async result resolved but AsyncResolutionBatcher.onLineResult is not set, so the value cannot reach your document and the line will keep showing as pending. Set it to mirror resolved values into your own state. This warning appears once per batcher."
|
|
6412
|
+
);
|
|
6627
6413
|
}
|
|
6628
6414
|
/**
|
|
6629
|
-
*
|
|
6415
|
+
* Create a fresh internal event stream and wire its controller.
|
|
6416
|
+
* Called from the constructor and again from clearAll() so the batcher
|
|
6417
|
+
* keeps emitting events after an engine clear, the engine instance
|
|
6418
|
+
* (and this batcher) live on across clear() calls.
|
|
6630
6419
|
*/
|
|
6631
|
-
|
|
6632
|
-
return
|
|
6420
|
+
createEventStream() {
|
|
6421
|
+
return new ReadableStream({
|
|
6422
|
+
start: (controller) => {
|
|
6423
|
+
this._streamController = controller;
|
|
6424
|
+
},
|
|
6425
|
+
cancel: () => {
|
|
6426
|
+
this._streamController = null;
|
|
6427
|
+
}
|
|
6428
|
+
}, new CountQueuingStrategy({ highWaterMark: this.highWaterMark }));
|
|
6633
6429
|
}
|
|
6634
|
-
// ──
|
|
6430
|
+
// ── Public API ────────────────────────────────────────────────────
|
|
6635
6431
|
/**
|
|
6636
|
-
*
|
|
6432
|
+
* Add a resolved query key to the pending batch.
|
|
6637
6433
|
*
|
|
6638
|
-
*
|
|
6639
|
-
*
|
|
6434
|
+
* Called by ExpressionEngine.resolveAsync() after a promise resolves or errors.
|
|
6435
|
+
* If this is the first entry in the current tick, schedules a microtask flush.
|
|
6640
6436
|
*/
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
reads: [],
|
|
6648
|
-
writes: [],
|
|
6649
|
-
isVariableDef: false,
|
|
6650
|
-
error: raw.error
|
|
6651
|
-
};
|
|
6437
|
+
add(entry) {
|
|
6438
|
+
this.cleared = false;
|
|
6439
|
+
for (const existing of this.pending) {
|
|
6440
|
+
if (existing.packageId === entry.packageId && existing.queryKey === entry.queryKey) {
|
|
6441
|
+
return;
|
|
6442
|
+
}
|
|
6652
6443
|
}
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
program,
|
|
6665
|
-
reads: raw.reads,
|
|
6666
|
-
writes: raw.writes,
|
|
6667
|
-
isVariableDef: raw.isVariableDef,
|
|
6668
|
-
error: null
|
|
6669
|
-
};
|
|
6670
|
-
}
|
|
6671
|
-
};
|
|
6672
|
-
|
|
6673
|
-
// src/engine/PageManager.ts
|
|
6674
|
-
var PAGE_SIZE = 128;
|
|
6675
|
-
var HOT_PAGE_RADIUS = 3;
|
|
6676
|
-
var WARM_PAGE_RADIUS = 6;
|
|
6677
|
-
var PRELOAD_PAGE_COUNT = 2;
|
|
6678
|
-
var COLD_EVICT_BUFFER = HOT_PAGE_RADIUS;
|
|
6679
|
-
var PageManager = class _PageManager {
|
|
6680
|
-
constructor() {
|
|
6681
|
-
/** Previous viewport for scroll direction detection. null = no previous viewport. */
|
|
6682
|
-
this.lastViewportStart = null;
|
|
6683
|
-
/** Saved scroll direction from the most recent maintainAfterEval call. */
|
|
6684
|
-
this.savedDirection = null;
|
|
6685
|
-
/** Per-page access counter for LRU tracking. */
|
|
6686
|
-
this.pageAccess = /* @__PURE__ */ new Map();
|
|
6687
|
-
/** Monotonically increasing access sequence number. */
|
|
6688
|
-
this.accessSeq = 0;
|
|
6689
|
-
}
|
|
6690
|
-
// ── Static helpers ─────────────────────────────────────────────────
|
|
6691
|
-
/** Get the page number for a 1-based line number (0-based page index). */
|
|
6692
|
-
static pageForLine(lineNumber) {
|
|
6693
|
-
return Math.floor((lineNumber - 1) / PAGE_SIZE);
|
|
6694
|
-
}
|
|
6695
|
-
/** Get the 1-based inclusive line range for a page. */
|
|
6696
|
-
static pageRange(pageNum, docLineCount) {
|
|
6697
|
-
return {
|
|
6698
|
-
startLine: pageNum * PAGE_SIZE + 1,
|
|
6699
|
-
endLine: Math.min((pageNum + 1) * PAGE_SIZE, docLineCount)
|
|
6700
|
-
};
|
|
6701
|
-
}
|
|
6702
|
-
// ── Public API ─────────────────────────────────────────────────────
|
|
6703
|
-
/**
|
|
6704
|
-
* Detect scroll direction from viewport movement.
|
|
6705
|
-
* Returns null on first call (no previous viewport) or no movement.
|
|
6706
|
-
*/
|
|
6707
|
-
detectDirection(newViewport) {
|
|
6708
|
-
if (this.lastViewportStart === null) return null;
|
|
6709
|
-
if (newViewport.startLine > this.lastViewportStart) return "down";
|
|
6710
|
-
if (newViewport.startLine < this.lastViewportStart) return "up";
|
|
6711
|
-
return null;
|
|
6712
|
-
}
|
|
6713
|
-
/**
|
|
6714
|
-
* Maintain page tiers after evaluation.
|
|
6715
|
-
*
|
|
6716
|
-
* 1. Records the new viewport position
|
|
6717
|
-
* 2. Touches hot pages (viewport ± HOT_PAGE_RADIUS) for LRU
|
|
6718
|
-
* 3. Evicts cold/warm pages based on distance from viewport
|
|
6719
|
-
*
|
|
6720
|
-
* Call this after every `evaluate()`, `setViewport()`, or `evaluateAll()`.
|
|
6721
|
-
*/
|
|
6722
|
-
maintainAfterEval(viewport, doc) {
|
|
6723
|
-
this.savedDirection = this.detectDirection(viewport);
|
|
6724
|
-
this.lastViewportStart = viewport.startLine;
|
|
6725
|
-
const docLineCount = doc.lineCount;
|
|
6726
|
-
const viewportStartPage = _PageManager.pageForLine(viewport.startLine);
|
|
6727
|
-
const viewportEndPage = _PageManager.pageForLine(viewport.endLine);
|
|
6728
|
-
const lastPage = _PageManager.pageForLine(docLineCount);
|
|
6729
|
-
const hotStart = Math.max(0, viewportStartPage - HOT_PAGE_RADIUS);
|
|
6730
|
-
const hotEnd = Math.min(lastPage, viewportEndPage + HOT_PAGE_RADIUS);
|
|
6731
|
-
const warmStart = Math.max(0, viewportStartPage - WARM_PAGE_RADIUS);
|
|
6732
|
-
const warmEnd = Math.min(lastPage, viewportEndPage + WARM_PAGE_RADIUS);
|
|
6733
|
-
for (let p = hotStart; p <= hotEnd; p++) {
|
|
6734
|
-
this.touchPage(p);
|
|
6735
|
-
}
|
|
6736
|
-
for (let p = warmStart; p <= warmEnd; p++) {
|
|
6737
|
-
if (p >= hotStart && p <= hotEnd) continue;
|
|
6738
|
-
this.evictPageResults(p, doc, docLineCount);
|
|
6739
|
-
}
|
|
6740
|
-
const coldLeftEnd = warmStart - 1;
|
|
6741
|
-
const coldLeftStart = Math.max(0, coldLeftEnd - COLD_EVICT_BUFFER + 1);
|
|
6742
|
-
for (let p = coldLeftStart; p <= coldLeftEnd; p++) {
|
|
6743
|
-
this.evictPageBytecode(p, doc, docLineCount);
|
|
6744
|
-
}
|
|
6745
|
-
const coldRightStart = warmEnd + 1;
|
|
6746
|
-
const coldRightEnd = Math.min(lastPage, coldRightStart + COLD_EVICT_BUFFER - 1);
|
|
6747
|
-
for (let p = coldRightStart; p <= coldRightEnd; p++) {
|
|
6748
|
-
this.evictPageBytecode(p, doc, docLineCount);
|
|
6444
|
+
this.pending.push(entry);
|
|
6445
|
+
if (!this.scheduled) {
|
|
6446
|
+
this.scheduled = true;
|
|
6447
|
+
queueMicrotask(() => {
|
|
6448
|
+
try {
|
|
6449
|
+
this.flush();
|
|
6450
|
+
} catch (e) {
|
|
6451
|
+
const engineError = normalizeUnknownError(e);
|
|
6452
|
+
console.error(`[AsyncResolutionBatcher] flush() failed unexpectedly \u2014 this should never happen; please report: ${engineError.format()}`);
|
|
6453
|
+
}
|
|
6454
|
+
});
|
|
6749
6455
|
}
|
|
6750
6456
|
}
|
|
6751
6457
|
/**
|
|
6752
|
-
* Get
|
|
6458
|
+
* Get the native event stream for stream-based consumers.
|
|
6753
6459
|
*
|
|
6754
|
-
*
|
|
6755
|
-
*
|
|
6756
|
-
* and are not empty/markdown-only.
|
|
6460
|
+
* Use this for backpressure, cancellation, or the ability
|
|
6461
|
+
* to `pipeTo()` / `pipeThrough()` the event flow.
|
|
6757
6462
|
*
|
|
6758
|
-
* @
|
|
6759
|
-
*
|
|
6760
|
-
* @returns Items ready to send to the compilation worker.
|
|
6761
|
-
*/
|
|
6762
|
-
getPreloadTargets(viewport, doc) {
|
|
6763
|
-
const direction = this.savedDirection;
|
|
6764
|
-
if (!direction) return [];
|
|
6765
|
-
const items = [];
|
|
6766
|
-
const viewportEndPage = _PageManager.pageForLine(viewport.endLine);
|
|
6767
|
-
const viewportStartPage = _PageManager.pageForLine(viewport.startLine);
|
|
6768
|
-
const lastPage = _PageManager.pageForLine(doc.lineCount);
|
|
6769
|
-
const pages = [];
|
|
6770
|
-
if (direction === "down") {
|
|
6771
|
-
const startPage = viewportEndPage + HOT_PAGE_RADIUS + 1;
|
|
6772
|
-
const endPage = Math.min(startPage + PRELOAD_PAGE_COUNT - 1, lastPage);
|
|
6773
|
-
for (let p = startPage; p <= endPage && p <= lastPage; p++) {
|
|
6774
|
-
pages.push(p);
|
|
6775
|
-
}
|
|
6776
|
-
} else {
|
|
6777
|
-
const firstPreloadPage = viewportStartPage - HOT_PAGE_RADIUS - 1;
|
|
6778
|
-
for (let p = firstPreloadPage; p > firstPreloadPage - PRELOAD_PAGE_COUNT && p >= 0; p--) {
|
|
6779
|
-
pages.push(p);
|
|
6780
|
-
}
|
|
6781
|
-
}
|
|
6782
|
-
for (const pageNum of pages) {
|
|
6783
|
-
const range = _PageManager.pageRange(pageNum, doc.lineCount);
|
|
6784
|
-
for (let pos = range.startLine; pos <= range.endLine; pos++) {
|
|
6785
|
-
const state = doc.getLineAt(pos);
|
|
6786
|
-
if (!state) continue;
|
|
6787
|
-
if (!state.dirty) continue;
|
|
6788
|
-
if (state.bytecodes.length > 0 && !state.isVariableDef) continue;
|
|
6789
|
-
if (state.isEmpty) continue;
|
|
6790
|
-
if (state.expressions.length > 0) {
|
|
6791
|
-
for (const expression of state.expressions) {
|
|
6792
|
-
if (!expression.trim()) continue;
|
|
6793
|
-
items.push({
|
|
6794
|
-
lineId: state.lineId,
|
|
6795
|
-
expression,
|
|
6796
|
-
textHash: state.textHash
|
|
6797
|
-
});
|
|
6798
|
-
}
|
|
6799
|
-
} else {
|
|
6800
|
-
const inlineSpans = sharedLexer.findInlineSolves(state.text);
|
|
6801
|
-
if (inlineSpans.length > 0) {
|
|
6802
|
-
for (const span of inlineSpans) {
|
|
6803
|
-
if (!span.expression.trim()) continue;
|
|
6804
|
-
items.push({
|
|
6805
|
-
lineId: state.lineId,
|
|
6806
|
-
expression: span.expression,
|
|
6807
|
-
textHash: state.textHash
|
|
6808
|
-
});
|
|
6809
|
-
}
|
|
6810
|
-
} else {
|
|
6811
|
-
const expression = state.text.trim();
|
|
6812
|
-
if (expression) {
|
|
6813
|
-
items.push({
|
|
6814
|
-
lineId: state.lineId,
|
|
6815
|
-
expression,
|
|
6816
|
-
textHash: state.textHash
|
|
6817
|
-
});
|
|
6818
|
-
}
|
|
6819
|
-
}
|
|
6820
|
-
}
|
|
6821
|
-
}
|
|
6822
|
-
}
|
|
6823
|
-
return items;
|
|
6824
|
-
}
|
|
6825
|
-
/**
|
|
6826
|
-
* Reset internal state (e.g., after document switch).
|
|
6463
|
+
* @returns A {@link ReadableStream} that emits {@link AsyncResolutionEvent}
|
|
6464
|
+
* items as the batcher processes async resolutions.
|
|
6827
6465
|
*/
|
|
6828
|
-
|
|
6829
|
-
this.
|
|
6830
|
-
this.accessSeq = 0;
|
|
6831
|
-
this.lastViewportStart = null;
|
|
6832
|
-
this.savedDirection = null;
|
|
6466
|
+
getEventStream() {
|
|
6467
|
+
return this._eventStream;
|
|
6833
6468
|
}
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
this.pageAccess.set(pageNum, ++this.accessSeq);
|
|
6469
|
+
/** Number of resolutions currently queued for the next flush. */
|
|
6470
|
+
get pendingCount() {
|
|
6471
|
+
return this.pending.length;
|
|
6838
6472
|
}
|
|
6839
|
-
/**
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
const range = _PageManager.pageRange(pageNum, docLineCount);
|
|
6845
|
-
for (let pos = range.startLine; pos <= range.endLine; pos++) {
|
|
6846
|
-
const state = doc.getLineAt(pos);
|
|
6847
|
-
if (state && !state.isVariableDef && state.results.length > 0) {
|
|
6848
|
-
state.results = [];
|
|
6849
|
-
state.result = null;
|
|
6850
|
-
}
|
|
6473
|
+
/** Number of pending entries collapsed by (packageId, queryKey) deduplication. */
|
|
6474
|
+
get dedupCount() {
|
|
6475
|
+
const dedup = /* @__PURE__ */ new Set();
|
|
6476
|
+
for (const entry of this.pending) {
|
|
6477
|
+
dedup.add(`${entry.packageId}:${entry.queryKey}`);
|
|
6851
6478
|
}
|
|
6479
|
+
return Math.max(0, this.pending.length - dedup.size);
|
|
6852
6480
|
}
|
|
6853
6481
|
/**
|
|
6854
|
-
*
|
|
6855
|
-
*
|
|
6856
|
-
*
|
|
6857
|
-
*
|
|
6858
|
-
* Evicted lines are marked dirty so they get Tier 1 re-evaluation
|
|
6859
|
-
* when scrolled back into view.
|
|
6482
|
+
* Whether the internal event stream currently has an active reader.
|
|
6483
|
+
* `1` if a consumer has called `getEventStream().getReader()` (or
|
|
6484
|
+
* otherwise locked the stream) and not released it, `0` otherwise.
|
|
6860
6485
|
*/
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
for (let pos = range.startLine; pos <= range.endLine; pos++) {
|
|
6864
|
-
const state = doc.getLineAt(pos);
|
|
6865
|
-
if (!state || state.isVariableDef) continue;
|
|
6866
|
-
if (state.bytecodes.length > 0 || state.results.length > 0) {
|
|
6867
|
-
state.bytecodes = [];
|
|
6868
|
-
state.results = [];
|
|
6869
|
-
state.result = null;
|
|
6870
|
-
doc.markDirty(state.lineId);
|
|
6871
|
-
}
|
|
6872
|
-
}
|
|
6486
|
+
get listenerCount() {
|
|
6487
|
+
return this._eventStream.locked ? 1 : 0;
|
|
6873
6488
|
}
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
var EvalTier = /* @__PURE__ */ ((EvalTier2) => {
|
|
6878
|
-
EvalTier2[EvalTier2["Tier1"] = 1] = "Tier1";
|
|
6879
|
-
EvalTier2[EvalTier2["Tier2"] = 2] = "Tier2";
|
|
6880
|
-
EvalTier2[EvalTier2["Tier3"] = 3] = "Tier3";
|
|
6881
|
-
EvalTier2[EvalTier2["Skipped"] = 0] = "Skipped";
|
|
6882
|
-
return EvalTier2;
|
|
6883
|
-
})(EvalTier || {});
|
|
6884
|
-
var ThreeTierEvaluator = class {
|
|
6885
|
-
/**
|
|
6886
|
-
* @param doc The persistent document model.
|
|
6887
|
-
* @param engine The expression engine (shared VM is accessed via engine.getVM()).
|
|
6888
|
-
* @param checkpointer Optional VM state checkpointer. If provided, the evaluator
|
|
6889
|
-
* will create checkpoints after variable-definition lines and support fast VM
|
|
6890
|
-
* restoration via `restoreTo()`. If omitted, checkpointing is disabled.
|
|
6891
|
-
*/
|
|
6892
|
-
constructor(doc, engine, checkpointer) {
|
|
6893
|
-
this.compilationWorker = null;
|
|
6894
|
-
/**
|
|
6895
|
-
* Unsubscribe from sharedGlobalVariableStore, set in the constructor
|
|
6896
|
-
* called from terminateWorker(). See the subscription itself below for
|
|
6897
|
-
* why this only marks lines dirty and never re-evaluates synchronously.
|
|
6898
|
-
*/
|
|
6899
|
-
this.globalUnsubscribe = null;
|
|
6900
|
-
this.doc = doc;
|
|
6901
|
-
this.engine = engine;
|
|
6902
|
-
this.dag = engine.getDag();
|
|
6903
|
-
this.checkpointer = checkpointer ?? null;
|
|
6904
|
-
this.pageManager = new PageManager();
|
|
6905
|
-
this.engine.setDocumentModel(this.doc);
|
|
6906
|
-
this.globalUnsubscribe = sharedGlobalVariableStore.subscribe((name) => {
|
|
6907
|
-
for (const lineNumber of this.dag.getAffectedLines(globalDagKey(name))) {
|
|
6908
|
-
this.doc.markDirtyByLineNumber(lineNumber);
|
|
6909
|
-
}
|
|
6910
|
-
});
|
|
6489
|
+
/** Number of flushes that were actually dispatched to the worker pool. */
|
|
6490
|
+
get workerOffloadCount() {
|
|
6491
|
+
return this.workerOffloadDispatchCount;
|
|
6911
6492
|
}
|
|
6912
|
-
/**
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
*
|
|
6920
|
-
* @returns Results for all processed lines, including tier metadata.
|
|
6921
|
-
*/
|
|
6922
|
-
evaluate(viewport, signal) {
|
|
6923
|
-
this.engine.setKeystrokeSignal(signal ?? null);
|
|
6924
|
-
enableValueArena();
|
|
6493
|
+
/** Remove all listeners and cancel pending batch. Called on engine clear. */
|
|
6494
|
+
clearAll() {
|
|
6495
|
+
this.pending = [];
|
|
6496
|
+
this.scheduled = false;
|
|
6497
|
+
this.cleared = true;
|
|
6498
|
+
this._testCaptures = null;
|
|
6499
|
+
this.onLineResult = null;
|
|
6925
6500
|
try {
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
const tierCounts = { tier1: 0, tier2: 0, tier3: 0, skipped: 0 };
|
|
6929
|
-
const docEnd = this.doc.lineCount;
|
|
6930
|
-
const evalEnd = Math.min(viewport.endLine, docEnd);
|
|
6931
|
-
for (let pos = 1; pos <= evalEnd; pos++) {
|
|
6932
|
-
const state = this.doc.getLineAt(pos);
|
|
6933
|
-
if (!state) {
|
|
6934
|
-
tierCounts.skipped++;
|
|
6935
|
-
continue;
|
|
6936
|
-
}
|
|
6937
|
-
const inViewport = pos >= viewport.startLine && pos <= viewport.endLine;
|
|
6938
|
-
const lineResult = this.evaluateSingleLine(state, pos, inViewport);
|
|
6939
|
-
lines.push(lineResult);
|
|
6940
|
-
if (lineResult.tier === 1 /* Tier1 */) tierCounts.tier1++;
|
|
6941
|
-
else if (lineResult.tier === 2 /* Tier2 */) tierCounts.tier2++;
|
|
6942
|
-
else if (lineResult.tier === 3 /* Tier3 */) tierCounts.tier3++;
|
|
6943
|
-
else tierCounts.skipped++;
|
|
6944
|
-
if (lineResult.results && inViewport) {
|
|
6945
|
-
resultMap.set(pos, lineResult.results.flat());
|
|
6946
|
-
}
|
|
6947
|
-
}
|
|
6948
|
-
this.pageManager.maintainAfterEval(viewport, this.doc);
|
|
6949
|
-
return { lines, resultMap, tierCounts };
|
|
6950
|
-
} finally {
|
|
6951
|
-
this.engine.setKeystrokeSignal(null);
|
|
6952
|
-
disableValueArena();
|
|
6501
|
+
this._streamController?.close();
|
|
6502
|
+
} catch {
|
|
6953
6503
|
}
|
|
6504
|
+
this._streamController = null;
|
|
6505
|
+
this._eventStream = this.createEventStream();
|
|
6506
|
+
if (this.executionPool) {
|
|
6507
|
+
this.executionPool.clear();
|
|
6508
|
+
this.executionPool = null;
|
|
6509
|
+
}
|
|
6510
|
+
this.workerOffloadDispatchCount = 0;
|
|
6954
6511
|
}
|
|
6512
|
+
// ── Private: flush ────────────────────────────────────────────────
|
|
6955
6513
|
/**
|
|
6956
|
-
*
|
|
6957
|
-
*
|
|
6958
|
-
* Compiles expressions to discover reads/writes for the dependency graph
|
|
6959
|
-
* without executing display-only expressions. Variable definitions are
|
|
6960
|
-
* executed to maintain VM state for future Tier-2 executions.
|
|
6961
|
-
*
|
|
6962
|
-
* This is intended to be called after evaluate() so visible lines are
|
|
6963
|
-
* rendered first, then background work fills in the dependency graph.
|
|
6964
|
-
*
|
|
6965
|
-
* **Phase 5.2h:** This synchronous method is retained for environments
|
|
6966
|
-
* without Worker support. Prefer `dispatchBackgroundCompiles()` which
|
|
6967
|
-
* offloads compilation to a Web Worker with Transferable bytecode.
|
|
6968
|
-
*/
|
|
6969
|
-
backgroundCompile(viewport) {
|
|
6970
|
-
const results = [];
|
|
6971
|
-
const docEnd = this.doc.lineCount;
|
|
6972
|
-
const startPos = viewport.endLine + 1;
|
|
6973
|
-
for (let pos = startPos; pos <= docEnd; pos++) {
|
|
6974
|
-
const state = this.doc.getLineAt(pos);
|
|
6975
|
-
if (!state) continue;
|
|
6976
|
-
if (!state.dirty) continue;
|
|
6977
|
-
if (state.bytecodes.length > 0 && !state.isVariableDef) continue;
|
|
6978
|
-
const lineResult = this.evaluateSingleLine(state, pos, false);
|
|
6979
|
-
results.push(lineResult);
|
|
6980
|
-
}
|
|
6981
|
-
return results;
|
|
6982
|
-
}
|
|
6983
|
-
/**
|
|
6984
|
-
* Dispatch background compilation to a Web Worker (Phase 5.2h).
|
|
6985
|
-
*
|
|
6986
|
-
* Collects invisible dirty lines beyond the viewport that need compilation,
|
|
6987
|
-
* sends them to the compilation worker, and asynchronously stores the
|
|
6988
|
-
* transferred bytecode in the DocumentModel when the worker responds.
|
|
6989
|
-
*
|
|
6990
|
-
* This is the non-blocking alternative to `backgroundCompile()`. The worker
|
|
6991
|
-
* compiles expressions with Transferable ArrayBuffers (zero-copy postMessage),
|
|
6992
|
-
* so bytecode appears on the main thread without serialization overhead.
|
|
6993
|
-
*
|
|
6994
|
-
* Lines that already have cached bytecode (from a previous worker pass or
|
|
6995
|
-
* synchronous compile) are skipped, only truly uncompiled dirty lines are
|
|
6996
|
-
* sent to the worker.
|
|
6997
|
-
*
|
|
6998
|
-
* **Usage:** Call after `evaluate()` so visible lines render first, then
|
|
6999
|
-
* this fills the bytecode cache for future Tier-2 scrolls.
|
|
6514
|
+
* Flush all pending resolutions in a single batched pass.
|
|
7000
6515
|
*
|
|
7001
|
-
*
|
|
7002
|
-
* that are dirty and don't have bytecode are dispatched.
|
|
7003
|
-
*/
|
|
7004
|
-
dispatchBackgroundCompiles(viewport) {
|
|
7005
|
-
const items = this.collectInvisibleCompileTargets(viewport);
|
|
7006
|
-
if (items.length === 0) return;
|
|
7007
|
-
if (!this.compilationWorker) {
|
|
7008
|
-
this.compilationWorker = new CompilationWorkerManager();
|
|
7009
|
-
}
|
|
7010
|
-
this.compilationWorker.compileBatch(items).then((results) => {
|
|
7011
|
-
this.compilationWorker.storeResults(results, this.doc);
|
|
7012
|
-
}).catch((_err) => {
|
|
7013
|
-
});
|
|
7014
|
-
}
|
|
7015
|
-
/**
|
|
7016
|
-
* Terminate the compilation worker if active, and unsubscribe from
|
|
7017
|
-
* sharedGlobalVariableStore. Call this when the evaluator is no longer
|
|
7018
|
-
* needed to clean up resources, every call site that retires a
|
|
7019
|
-
* ThreeTierEvaluator (document switch, pane destroy()) already calls
|
|
7020
|
-
* this unconditionally, so folding the global-store unsubscribe in here
|
|
7021
|
-
* needs no new call sites anywhere.
|
|
6516
|
+
* Called automatically via queueMicrotask. Never called directly.
|
|
7022
6517
|
*/
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
6518
|
+
flush() {
|
|
6519
|
+
this.scheduled = false;
|
|
6520
|
+
if (this.cleared) return;
|
|
6521
|
+
if (this.pending.length === 0) return;
|
|
6522
|
+
const batch = this.pending;
|
|
6523
|
+
this.pending = [];
|
|
6524
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
6525
|
+
for (const entry of batch) {
|
|
6526
|
+
const compositeKey = `${entry.packageId}:${entry.queryKey}`;
|
|
6527
|
+
deduped.set(compositeKey, entry);
|
|
6528
|
+
}
|
|
6529
|
+
const errorEntries = [];
|
|
6530
|
+
const okEntries = [];
|
|
6531
|
+
for (const entry of deduped.values()) {
|
|
6532
|
+
if (entry.isError) {
|
|
6533
|
+
errorEntries.push(entry);
|
|
6534
|
+
} else {
|
|
6535
|
+
okEntries.push(entry);
|
|
6536
|
+
}
|
|
7027
6537
|
}
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
this.
|
|
6538
|
+
for (const entry of errorEntries) {
|
|
6539
|
+
if (entry.signal.aborted) continue;
|
|
6540
|
+
this.notifyListeners({
|
|
6541
|
+
type: "error",
|
|
6542
|
+
queryKey: entry.queryKey,
|
|
6543
|
+
packageId: entry.packageId,
|
|
6544
|
+
error: entry.error ?? new Error("Unknown async resolution error")
|
|
6545
|
+
});
|
|
7031
6546
|
}
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
* records state for lines that write a variable, so a dirty plain-expression
|
|
7066
|
-
* line before the viewport has no checkpoint to invalidate (see
|
|
7067
|
-
* `DocumentModel.hasAnyDirtyVariableDefLineBefore()`).
|
|
7068
|
-
*
|
|
7069
|
-
* **Performance:** O(visible lines) instead of O(document length). Target:
|
|
7070
|
-
* < 1ms for a typical ~30-line viewport, independent of document size.
|
|
7071
|
-
*
|
|
7072
|
-
* @param viewport The visible line range.
|
|
7073
|
-
* @returns Results for visible lines only. Lines before the viewport are
|
|
7074
|
-
* not included in `lines[]` or `resultMap`.
|
|
7075
|
-
*/
|
|
7076
|
-
setViewport(viewport, signal) {
|
|
7077
|
-
this.engine.setKeystrokeSignal(signal ?? null);
|
|
7078
|
-
if (viewport.startLine > 1 && this.hasDirtyLinesBefore(viewport.startLine)) {
|
|
7079
|
-
this.checkpointer?.clear();
|
|
7080
|
-
return this.evaluate(viewport, signal);
|
|
7081
|
-
}
|
|
7082
|
-
this.pageManager.maintainAfterEval(viewport, this.doc);
|
|
7083
|
-
this.preloadNextPages(viewport);
|
|
7084
|
-
this.restoreTo(viewport.startLine - 1);
|
|
7085
|
-
enableValueArena();
|
|
7086
|
-
try {
|
|
7087
|
-
const result = this.collectEvalResults(viewport.startLine, viewport.endLine);
|
|
7088
|
-
return result;
|
|
7089
|
-
} finally {
|
|
7090
|
-
this.engine.setKeystrokeSignal(null);
|
|
7091
|
-
disableValueArena();
|
|
6547
|
+
const allAffected = /* @__PURE__ */ new Set();
|
|
6548
|
+
const allQueryKeys = [];
|
|
6549
|
+
const allEntries = [...okEntries, ...errorEntries];
|
|
6550
|
+
for (const entry of allEntries) {
|
|
6551
|
+
if (entry.signal.aborted) continue;
|
|
6552
|
+
allQueryKeys.push(entry.queryKey);
|
|
6553
|
+
const affected = this.dag.getAffectedLinesByDataSource(
|
|
6554
|
+
entry.packageId,
|
|
6555
|
+
[entry.queryKey]
|
|
6556
|
+
);
|
|
6557
|
+
for (const line of affected) {
|
|
6558
|
+
allAffected.add(line);
|
|
6559
|
+
}
|
|
6560
|
+
}
|
|
6561
|
+
if (allAffected.size === 0) {
|
|
6562
|
+
this.notifyListeners({
|
|
6563
|
+
type: "lines-updated",
|
|
6564
|
+
lineNumbers: [],
|
|
6565
|
+
affectedQueryKeys: allQueryKeys
|
|
6566
|
+
});
|
|
6567
|
+
return;
|
|
6568
|
+
}
|
|
6569
|
+
const ordered = this.topologicalSort(Array.from(allAffected));
|
|
6570
|
+
if (ordered.length > WORKER_OFFLOAD_THRESHOLD) {
|
|
6571
|
+
const entryMap = /* @__PURE__ */ new Map();
|
|
6572
|
+
for (const lineNumber of ordered) {
|
|
6573
|
+
entryMap.set(lineNumber, this.lineCache.getEntryForLine(lineNumber));
|
|
6574
|
+
}
|
|
6575
|
+
void this.reExecuteViaWorkerPool(ordered, entryMap, allQueryKeys).catch((e) => {
|
|
6576
|
+
const engineError = normalizeUnknownError(e);
|
|
6577
|
+
console.error(`[AsyncResolutionBatcher] reExecuteViaWorkerPool() failed unexpectedly: ${engineError.format()}`);
|
|
6578
|
+
});
|
|
6579
|
+
return;
|
|
7092
6580
|
}
|
|
6581
|
+
this.reExecuteMainThread(ordered, allQueryKeys);
|
|
7093
6582
|
}
|
|
6583
|
+
// ── Private: topological sort ─────────────────────────────────────
|
|
7094
6584
|
/**
|
|
7095
|
-
*
|
|
7096
|
-
*
|
|
7097
|
-
* **Phase 5.2f:** Replaces the O(N) `setDocument()` + full re-evaluation
|
|
7098
|
-
* with O(changed) incremental updates. Key benefits:
|
|
7099
|
-
*
|
|
7100
|
-
* 1. Unchanged lines retain their persistent lineIds → bytecode survives
|
|
7101
|
-
* 2. Only changed + DAG-downstream lines are marked dirty → Tier 1 re-evaluation
|
|
7102
|
-
* 3. Clean lines in viewport use Tier 2 (cached bytecode execution)
|
|
7103
|
-
* 4. Clean lines outside viewport are skipped entirely
|
|
7104
|
-
*
|
|
7105
|
-
* The DAG is fully cleared after propagation: shifted lines would have
|
|
7106
|
-
* stale entries keyed by old line numbers, so the DAG is rebuilt from
|
|
7107
|
-
* scratch during the subsequent `evaluate()` call.
|
|
7108
|
-
*
|
|
7109
|
-
* **Caller should follow up with `evaluate(viewport)`** to re-evaluate
|
|
7110
|
-
* dirty lines from line 1 and rebuild the DAG + checkpoints.
|
|
6585
|
+
* Topologically sort affected lines using Kahn's algorithm.
|
|
7111
6586
|
*
|
|
7112
|
-
*
|
|
7113
|
-
*
|
|
6587
|
+
* Lines that produce variables come before lines that consume them.
|
|
6588
|
+
* This ensures correct evaluation order when multiple interdependent
|
|
6589
|
+
* lines are affected by async resolution.
|
|
7114
6590
|
*/
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
6591
|
+
topologicalSort(lines) {
|
|
6592
|
+
if (lines.length <= 1) return lines;
|
|
6593
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
6594
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
6595
|
+
const producerOf = /* @__PURE__ */ new Map();
|
|
6596
|
+
for (const line of lines) {
|
|
6597
|
+
inDegree.set(line, 0);
|
|
6598
|
+
adjacency.set(line, []);
|
|
6599
|
+
}
|
|
6600
|
+
for (const line of lines) {
|
|
6601
|
+
const writes = this.dag.getWrites(line);
|
|
6602
|
+
for (const w of writes) {
|
|
6603
|
+
producerOf.set(w, line);
|
|
7126
6604
|
}
|
|
7127
6605
|
}
|
|
7128
|
-
const
|
|
7129
|
-
|
|
7130
|
-
const
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
6606
|
+
for (const line of lines) {
|
|
6607
|
+
const reads = this.dag.getDependencies(line);
|
|
6608
|
+
for (const readVar of reads) {
|
|
6609
|
+
const producer = producerOf.get(readVar);
|
|
6610
|
+
if (producer !== void 0 && producer !== line) {
|
|
6611
|
+
adjacency.get(producer).push(line);
|
|
6612
|
+
inDegree.set(line, (inDegree.get(line) ?? 0) + 1);
|
|
7135
6613
|
}
|
|
7136
6614
|
}
|
|
7137
6615
|
}
|
|
7138
|
-
const
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
6616
|
+
const queue = [];
|
|
6617
|
+
for (const [line, degree] of inDegree) {
|
|
6618
|
+
if (degree === 0) queue.push(line);
|
|
6619
|
+
}
|
|
6620
|
+
if (queue.length === 0 && lines.length > 0) {
|
|
6621
|
+
return [...lines].sort((a, b) => a - b);
|
|
7142
6622
|
}
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
* Collect evaluation results for a contiguous range of lines.
|
|
7152
|
-
*
|
|
7153
|
-
* Used by both `evaluate()` (startLine=1) and `setViewport()` (any start).
|
|
7154
|
-
* All lines in the range are treated as in-viewport (visible), callers that
|
|
7155
|
-
* need the invisible/dirty → Tier 3 handling should use `evaluate()` instead.
|
|
7156
|
-
*
|
|
7157
|
-
* @param startLine First line to evaluate (1-based, inclusive).
|
|
7158
|
-
* @param endLine Last line to evaluate (1-based, inclusive). Clamped to docEnd.
|
|
7159
|
-
*/
|
|
7160
|
-
collectEvalResults(startLine, endLine) {
|
|
7161
|
-
const lines = [];
|
|
7162
|
-
const resultMap = /* @__PURE__ */ new Map();
|
|
7163
|
-
const tierCounts = { tier1: 0, tier2: 0, tier3: 0, skipped: 0 };
|
|
7164
|
-
const docEnd = this.doc.lineCount;
|
|
7165
|
-
const evalEnd = Math.min(endLine, docEnd);
|
|
7166
|
-
for (let pos = startLine; pos <= evalEnd; pos++) {
|
|
7167
|
-
const state = this.doc.getLineAt(pos);
|
|
7168
|
-
if (!state) continue;
|
|
7169
|
-
const lineResult = this.evaluateSingleLine(state, pos, true);
|
|
7170
|
-
lines.push(lineResult);
|
|
7171
|
-
if (lineResult.tier === 1 /* Tier1 */) tierCounts.tier1++;
|
|
7172
|
-
else if (lineResult.tier === 2 /* Tier2 */) tierCounts.tier2++;
|
|
7173
|
-
else if (lineResult.tier === 3 /* Tier3 */) tierCounts.tier3++;
|
|
7174
|
-
else tierCounts.skipped++;
|
|
7175
|
-
if (lineResult.results) {
|
|
7176
|
-
resultMap.set(pos, lineResult.results.flat());
|
|
7177
|
-
}
|
|
7178
|
-
}
|
|
7179
|
-
return { lines, resultMap, tierCounts };
|
|
7180
|
-
}
|
|
7181
|
-
/**
|
|
7182
|
-
* Check whether any **variable-definition** line before `position`
|
|
7183
|
-
* (1-based, exclusive) is dirty.
|
|
7184
|
-
*
|
|
7185
|
-
* Used by `setViewport()` to decide whether to fall back to `evaluate()`:
|
|
7186
|
-
* if a variable-def before the viewport is dirty, the checkpoint state
|
|
7187
|
-
* `restoreTo()` would use may be stale and we need to reprocess from
|
|
7188
|
-
* line 1 to rebuild checkpoints correctly.
|
|
7189
|
-
*
|
|
7190
|
-
* Deliberately narrower than `DocumentModel.hasAnyDirtyLineBefore()`:
|
|
7191
|
-
* checkpoints only snapshot variable-def lines (see VMCheckpointer), so a
|
|
7192
|
-
* dirty plain-expression line before the viewport can't have invalidated
|
|
7193
|
-
* one, there's nothing checkpointed for it to invalidate. Using the
|
|
7194
|
-
* broader check here previously caused a real perf bug: `PageManager`'s
|
|
7195
|
-
* cold-page eviction marks evicted non-variable-def lines dirty, so
|
|
7196
|
-
* scrolling far into a large, variable-def-free document would trip this
|
|
7197
|
-
* guard, fall back to `evaluate()`, which recompiles those lines via
|
|
7198
|
-
* Tier 3 (never clearing their dirty flag by design), causing the very
|
|
7199
|
-
* next `maintainAfterEval()` to re-evict and re-dirty them, a
|
|
7200
|
-
* self-sustaining loop that pinned every subsequent `setViewport()` call
|
|
7201
|
-
* to the cost of a full re-evaluation instead of O(visible lines).
|
|
7202
|
-
*
|
|
7203
|
-
* Delegates to DocumentModel.hasAnyDirtyVariableDefLineBefore(), which
|
|
7204
|
-
* tracks dirty lineIds incrementally instead of scanning every line up to
|
|
7205
|
-
* `position` on every call. This used to be a real per-scroll cost
|
|
7206
|
-
* (benchmarked at ~10ms scrolled near the bottom of a 20k-line document)
|
|
7207
|
-
* since it fired on every viewport change, not just edits.
|
|
7208
|
-
*/
|
|
7209
|
-
hasDirtyLinesBefore(position) {
|
|
7210
|
-
return this.doc.hasAnyDirtyVariableDefLineBefore(position);
|
|
7211
|
-
}
|
|
7212
|
-
/**
|
|
7213
|
-
* Evaluate a single line using the appropriate tier.
|
|
7214
|
-
*
|
|
7215
|
-
* Tier assignment logic:
|
|
7216
|
-
* - Empty/markdown-only lines → skipped
|
|
7217
|
-
* - Dirty + in-viewport → Tier 1 (full pipeline)
|
|
7218
|
-
* - Dirty + not in viewport → Tier 3 (compile-only, execute variable defs)
|
|
7219
|
-
* - Clean + has bytecode + in viewport → Tier 2 (execute from cache)
|
|
7220
|
-
* - Clean + no bytecode → skipped (non-evaluable)
|
|
7221
|
-
*/
|
|
7222
|
-
evaluateSingleLine(state, lineNumber, inViewport) {
|
|
7223
|
-
const baseResult = {
|
|
7224
|
-
lineId: state.lineId,
|
|
7225
|
-
lineNumber
|
|
7226
|
-
};
|
|
7227
|
-
if (state.isEmpty || isEmptyLine(state.text)) {
|
|
7228
|
-
state.isEmpty = true;
|
|
7229
|
-
this.doc.markClean(state.lineId);
|
|
7230
|
-
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
7231
|
-
}
|
|
7232
|
-
const { expressions, inlineSolveCount } = this.extractExpressions(state);
|
|
7233
|
-
if (expressions.length === 0) {
|
|
7234
|
-
state.isEmpty = true;
|
|
7235
|
-
this.doc.markClean(state.lineId);
|
|
7236
|
-
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
7237
|
-
}
|
|
7238
|
-
if (state.dirty) {
|
|
7239
|
-
if (inViewport) {
|
|
7240
|
-
return this.evaluateTier1(state, lineNumber, expressions, inlineSolveCount, baseResult);
|
|
7241
|
-
} else {
|
|
7242
|
-
if (state.bytecodes.length > 0 && state.bytecodes.length === expressions.length && !state.isVariableDef) {
|
|
7243
|
-
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
7244
|
-
}
|
|
7245
|
-
return this.evaluateTier3(state, lineNumber, expressions, inlineSolveCount, baseResult);
|
|
6623
|
+
const ordered = [];
|
|
6624
|
+
while (queue.length > 0) {
|
|
6625
|
+
const current = queue.shift();
|
|
6626
|
+
ordered.push(current);
|
|
6627
|
+
for (const downstream of adjacency.get(current) ?? []) {
|
|
6628
|
+
const newDegree = (inDegree.get(downstream) ?? 1) - 1;
|
|
6629
|
+
inDegree.set(downstream, newDegree);
|
|
6630
|
+
if (newDegree === 0) queue.push(downstream);
|
|
7246
6631
|
}
|
|
7247
6632
|
}
|
|
7248
|
-
if (
|
|
7249
|
-
|
|
6633
|
+
if (ordered.length < lines.length) {
|
|
6634
|
+
const remaining = lines.filter((l) => !ordered.includes(l)).sort((a, b) => a - b);
|
|
6635
|
+
ordered.push(...remaining);
|
|
7250
6636
|
}
|
|
7251
|
-
return
|
|
6637
|
+
return ordered;
|
|
7252
6638
|
}
|
|
6639
|
+
// ── Private: worker-pool re-execution ─────────────────────────────
|
|
7253
6640
|
/**
|
|
7254
|
-
*
|
|
7255
|
-
*
|
|
7256
|
-
*
|
|
6641
|
+
* Offload VM re-execution to the worker pool for large batches.
|
|
6642
|
+
*
|
|
6643
|
+
* Called when ordered.length > WORKER_OFFLOAD_THRESHOLD (50).
|
|
6644
|
+
* Clones bytecode ArrayBuffers, dispatches to workers, and asynchronously
|
|
6645
|
+
* patches results back into LineCache before notifying listeners.
|
|
6646
|
+
*
|
|
6647
|
+
* Handles pending results: lines that return { type: 'pending' } from the
|
|
6648
|
+
* worker are NOT marked as updated, the engine's resolveAsync will handle
|
|
6649
|
+
* them when the async resolver completes.
|
|
7257
6650
|
*
|
|
7258
|
-
*
|
|
7259
|
-
*
|
|
7260
|
-
* earlier solves update the VM state before later solves are evaluated.
|
|
7261
|
-
* Reads/writes are aggregated across all expressions for the DAG.
|
|
6651
|
+
* Safety: checks this.cleared before applying results, if the engine was
|
|
6652
|
+
* cleared while the worker batch was in-flight, results are discarded.
|
|
7262
6653
|
*/
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
const allReads = /* @__PURE__ */ new Set();
|
|
7267
|
-
const allWrites = /* @__PURE__ */ new Set();
|
|
7268
|
-
let hasVariableDef = false;
|
|
7269
|
-
let lastValue = null;
|
|
7270
|
-
let firstError = null;
|
|
7271
|
-
let anyFailed = false;
|
|
7272
|
-
let anyPending = false;
|
|
7273
|
-
for (const expression of expressions) {
|
|
7274
|
-
if (!expression.trim()) continue;
|
|
7275
|
-
let value = null;
|
|
7276
|
-
let entry;
|
|
7277
|
-
try {
|
|
7278
|
-
const evaluation = this.engine.evaluateLineDetailed(lineNumber, expression);
|
|
7279
|
-
value = evaluation.values;
|
|
7280
|
-
lastValue = value[0];
|
|
7281
|
-
entry = this.engine.getLineCache().get(lineNumber, expression);
|
|
7282
|
-
} catch (e) {
|
|
7283
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
7284
|
-
if (!firstError) firstError = errorMessage;
|
|
7285
|
-
anyFailed = true;
|
|
7286
|
-
value = null;
|
|
7287
|
-
}
|
|
7288
|
-
if (value && value.some((v) => v.type === 12 /* Pending */)) {
|
|
7289
|
-
anyPending = true;
|
|
7290
|
-
}
|
|
7291
|
-
if (value) {
|
|
7292
|
-
allResults.push(value);
|
|
7293
|
-
} else {
|
|
7294
|
-
allResults.push([errorValue("eval_failed", firstError ?? "unknown error")]);
|
|
7295
|
-
}
|
|
7296
|
-
if (entry) {
|
|
7297
|
-
allBytecodes.push(entry.bytecode);
|
|
7298
|
-
for (const r of entry.readVariables) allReads.add(r);
|
|
7299
|
-
if (entry.writeVariable) {
|
|
7300
|
-
allWrites.add(entry.writeVariable);
|
|
7301
|
-
hasVariableDef = true;
|
|
7302
|
-
}
|
|
7303
|
-
} else {
|
|
7304
|
-
try {
|
|
7305
|
-
const { program, reads: reads2, writes: writes2 } = this.engine.compileExpression(expression);
|
|
7306
|
-
allBytecodes.push(program);
|
|
7307
|
-
for (const r of reads2) allReads.add(r);
|
|
7308
|
-
for (const w of writes2) allWrites.add(w);
|
|
7309
|
-
if (writes2.length > 0) hasVariableDef = true;
|
|
7310
|
-
} catch (compileErr) {
|
|
7311
|
-
allBytecodes.push({ opcodes: new Uint8Array(0), numbers: new Float64Array(0), strings: [], hasAsync: false });
|
|
7312
|
-
if (compileErr instanceof EngineError && compileErr.context) {
|
|
7313
|
-
const errReads = compileErr.context.reads;
|
|
7314
|
-
const errWrites = compileErr.context.writes;
|
|
7315
|
-
if (Array.isArray(errReads)) for (const r of errReads) allReads.add(r);
|
|
7316
|
-
if (Array.isArray(errWrites)) for (const w of errWrites) allWrites.add(w);
|
|
7317
|
-
}
|
|
7318
|
-
}
|
|
7319
|
-
}
|
|
7320
|
-
}
|
|
7321
|
-
const reads = [...allReads];
|
|
7322
|
-
const writes = [...allWrites];
|
|
7323
|
-
if (anyFailed || anyPending) {
|
|
7324
|
-
this.doc.updateLineCompiled(
|
|
7325
|
-
state.lineId,
|
|
7326
|
-
expressions,
|
|
7327
|
-
allBytecodes,
|
|
7328
|
-
reads,
|
|
7329
|
-
writes,
|
|
7330
|
-
hasVariableDef,
|
|
7331
|
-
inlineSolveCount
|
|
7332
|
-
);
|
|
7333
|
-
state.results = allResults;
|
|
7334
|
-
state.result = allResults[0]?.[0] ?? null;
|
|
7335
|
-
state.inlineSolveCount = inlineSolveCount;
|
|
7336
|
-
state.expressions = expressions;
|
|
7337
|
-
} else {
|
|
7338
|
-
this.doc.updateLineResult(
|
|
7339
|
-
state.lineId,
|
|
7340
|
-
allResults,
|
|
7341
|
-
allBytecodes,
|
|
7342
|
-
expressions,
|
|
7343
|
-
reads,
|
|
7344
|
-
writes,
|
|
7345
|
-
hasVariableDef,
|
|
7346
|
-
inlineSolveCount
|
|
7347
|
-
);
|
|
6654
|
+
async reExecuteViaWorkerPool(ordered, entryMap, allQueryKeys) {
|
|
6655
|
+
if (!this.executionPool) {
|
|
6656
|
+
this.executionPool = new ExecutionPool();
|
|
7348
6657
|
}
|
|
7349
|
-
this.
|
|
7350
|
-
if (
|
|
7351
|
-
this.
|
|
6658
|
+
const results = this.executionPool.executeBatch(ordered, entryMap);
|
|
6659
|
+
if (!results) {
|
|
6660
|
+
this.reExecuteMainThread(ordered, allQueryKeys, entryMap);
|
|
6661
|
+
return;
|
|
7352
6662
|
}
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
* Tier 2: Execute from cached bytecode only.
|
|
7363
|
-
* Skips lexing, parsing, and compiling, runs the pre-compiled bytecode
|
|
7364
|
-
* against the engine's shared VM. Supports multiple bytecodes per line
|
|
7365
|
-
* (inline solves), each is executed left-to-right so variable definitions
|
|
7366
|
-
* in earlier bytecodes update the VM before later ones run.
|
|
7367
|
-
* Assumes the VM already has correct variable state from preceding
|
|
7368
|
-
* Tier-1 evaluations.
|
|
7369
|
-
*/
|
|
7370
|
-
evaluateTier2(state, lineNumber, baseResult) {
|
|
7371
|
-
if (state.bytecodes.length === 0) {
|
|
7372
|
-
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
7373
|
-
}
|
|
7374
|
-
const results = [];
|
|
7375
|
-
let lastValue = null;
|
|
7376
|
-
let firstError = null;
|
|
7377
|
-
let anyFailed = false;
|
|
7378
|
-
for (const bytecode of state.bytecodes) {
|
|
7379
|
-
if (bytecode.opcodes.length === 0) continue;
|
|
7380
|
-
try {
|
|
7381
|
-
const value = this.engine.executeCached(bytecode, lineNumber);
|
|
7382
|
-
lastValue = value;
|
|
7383
|
-
results.push([value]);
|
|
7384
|
-
} catch (e) {
|
|
7385
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
7386
|
-
if (!firstError) firstError = errorMessage;
|
|
7387
|
-
anyFailed = true;
|
|
7388
|
-
results.push([errorValue("exec_failed", errorMessage)]);
|
|
6663
|
+
this.workerOffloadDispatchCount++;
|
|
6664
|
+
const workerResults = await results;
|
|
6665
|
+
if (this.cleared) return;
|
|
6666
|
+
const updatedLineNumbers = [];
|
|
6667
|
+
for (const wr of workerResults) {
|
|
6668
|
+
const entry = entryMap.get(wr.lineNumber);
|
|
6669
|
+
if (!entry) continue;
|
|
6670
|
+
if (wr.isPending) {
|
|
6671
|
+
continue;
|
|
7389
6672
|
}
|
|
6673
|
+
const value = reconstructValue(wr);
|
|
6674
|
+
entry.result = value;
|
|
6675
|
+
this.warnIfUnwired();
|
|
6676
|
+
this.onLineResult?.(wr.lineNumber, value);
|
|
6677
|
+
updatedLineNumbers.push(wr.lineNumber);
|
|
7390
6678
|
}
|
|
7391
|
-
this.
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
6679
|
+
if (this.cleared) return;
|
|
6680
|
+
if (updatedLineNumbers.length > 0 || allQueryKeys.length > 0) {
|
|
6681
|
+
this.notifyListeners({
|
|
6682
|
+
type: "lines-updated",
|
|
6683
|
+
lineNumbers: updatedLineNumbers,
|
|
6684
|
+
affectedQueryKeys: allQueryKeys
|
|
6685
|
+
});
|
|
7396
6686
|
}
|
|
7397
|
-
return { ...baseResult, tier: 2 /* Tier2 */, result: lastValue, results, error: firstError };
|
|
7398
6687
|
}
|
|
6688
|
+
// ── Private: main-thread re-execution ────────────────────────────
|
|
7399
6689
|
/**
|
|
7400
|
-
*
|
|
7401
|
-
*
|
|
7402
|
-
* Executes the bytecode ONLY if the line defines a variable (isVariableDef
|
|
7403
|
-
* or writes.length > 0), because variable assignments affect VM state
|
|
7404
|
-
* that other lines depend on. Pure expression lines are compiled but NOT
|
|
7405
|
-
* executed, saving CPU for large documents.
|
|
6690
|
+
* Execute ordered lines on the main thread, update LineCache, and notify
|
|
6691
|
+
* listeners. Returns the list of line numbers that actually changed.
|
|
7406
6692
|
*
|
|
7407
|
-
*
|
|
7408
|
-
*
|
|
6693
|
+
* Used by both flush() (≤50 lines) and reExecuteViaWorkerPool() (fallback
|
|
6694
|
+
* when workers are unavailable). Extracted to avoid code duplication.
|
|
6695
|
+
*
|
|
6696
|
+
* **Per-line containment (fatal-bug fix)**: `executeBytecode()` used to
|
|
6697
|
+
* run here with NO try/catch anywhere in this method's call chain, and
|
|
6698
|
+
* this whole batch runs inside a bare `queueMicrotask` (see `add()`) with
|
|
6699
|
+
* no surrounding try/catch at any caller either, so if any ONE line's
|
|
6700
|
+
* cached bytecode threw (a stack/instruction-limit error, an undefined
|
|
6701
|
+
* variable, a corrupted-bytecode `TypeError`), the `for` loop aborted
|
|
6702
|
+
* immediately: every line scheduled AFTER the failure in this batch was
|
|
6703
|
+
* silently never re-executed or notified even though nothing was wrong
|
|
6704
|
+
* with them, every line BEFORE it had already had its `entry.result`
|
|
6705
|
+
* mutated in-place but `notifyListeners()` was never reached (a silent
|
|
6706
|
+
* `LineCache`/host desync), and, because a bare `queueMicrotask`
|
|
6707
|
+
* callback has no caller to catch it, the exception was uncatchable:
|
|
6708
|
+
* an `uncaughtException` that could crash the host process outright.
|
|
6709
|
+
* (`__tests__/async/AsyncResolutionBatcher.spec.ts`'s topological-sort
|
|
6710
|
+
* describe block used to have a test skipped specifically because of
|
|
6711
|
+
* this. See that file, now un-skipped and rewritten.) Each line's
|
|
6712
|
+
* execution is now its own try/catch: a failure is recorded as an
|
|
6713
|
+
* `Error` `Value` for THAT line (still counted as "updated" so the host
|
|
6714
|
+
* learns about it and stops showing a stale Pending state) and the loop
|
|
6715
|
+
* continues, one line's failure can no longer take out its neighbors.
|
|
7409
6716
|
*/
|
|
7410
|
-
|
|
7411
|
-
const
|
|
7412
|
-
const
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
let firstError = null;
|
|
7417
|
-
let anyFailed = false;
|
|
7418
|
-
let anyPending = false;
|
|
7419
|
-
for (const expression of expressions) {
|
|
7420
|
-
if (!expression.trim()) continue;
|
|
6717
|
+
reExecuteMainThread(ordered, allQueryKeys, entryMap) {
|
|
6718
|
+
const updatedLineNumbers = [];
|
|
6719
|
+
for (const lineNumber of ordered) {
|
|
6720
|
+
const entry = entryMap ? entryMap.get(lineNumber) : this.lineCache.getEntryForLine(lineNumber);
|
|
6721
|
+
if (!entry || entry.bytecode.opcodes.length === 0) continue;
|
|
6722
|
+
const stackBefore = this.vm.getStack().length;
|
|
7421
6723
|
try {
|
|
7422
|
-
const
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
if (
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
6724
|
+
const result = executeBytecode(entry.bytecode, this.vm);
|
|
6725
|
+
while (this.vm.getStack().length > stackBefore) {
|
|
6726
|
+
this.vm.pop();
|
|
6727
|
+
}
|
|
6728
|
+
if (result.type === "value") {
|
|
6729
|
+
entry.result = result.value;
|
|
6730
|
+
this.warnIfUnwired();
|
|
6731
|
+
this.onLineResult?.(lineNumber, result.value);
|
|
6732
|
+
updatedLineNumbers.push(lineNumber);
|
|
6733
|
+
} else if (result.type === "error") {
|
|
6734
|
+
const value = errorValue(result.error.code, result.error.message);
|
|
6735
|
+
entry.result = value;
|
|
6736
|
+
this.warnIfUnwired();
|
|
6737
|
+
this.warnIfUnwired();
|
|
6738
|
+
this.onLineResult?.(lineNumber, value);
|
|
6739
|
+
updatedLineNumbers.push(lineNumber);
|
|
7432
6740
|
}
|
|
7433
6741
|
} catch (e) {
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
state.lineId,
|
|
7444
|
-
expressions,
|
|
7445
|
-
allBytecodes,
|
|
7446
|
-
reads,
|
|
7447
|
-
writes,
|
|
7448
|
-
hasVariableDef,
|
|
7449
|
-
inlineSolveCount
|
|
7450
|
-
);
|
|
7451
|
-
this.dag.registerLine(lineNumber, reads, writes);
|
|
7452
|
-
if (hasVariableDef && lastResult && !anyFailed && !anyPending) {
|
|
7453
|
-
state.results = [[lastResult]];
|
|
7454
|
-
state.result = lastResult;
|
|
7455
|
-
this.doc.markClean(state.lineId);
|
|
7456
|
-
if (this.checkpointer) {
|
|
7457
|
-
this.checkpointer.snapshot(lineNumber, state.lineId, writes);
|
|
6742
|
+
while (this.vm.getStack().length > stackBefore) {
|
|
6743
|
+
this.vm.pop();
|
|
6744
|
+
}
|
|
6745
|
+
const engineError = normalizeUnknownError(e);
|
|
6746
|
+
const value = errorValue(engineError.code, engineError.message);
|
|
6747
|
+
entry.result = value;
|
|
6748
|
+
this.warnIfUnwired();
|
|
6749
|
+
this.onLineResult?.(lineNumber, value);
|
|
6750
|
+
updatedLineNumbers.push(lineNumber);
|
|
7458
6751
|
}
|
|
7459
6752
|
}
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
* Finds the nearest checkpoint at or before `lineNumber` and replays
|
|
7467
|
-
* all variable definitions from the checkpoint chain into the VM.
|
|
7468
|
-
* After calling this, the VM is ready to evaluate lines starting at
|
|
7469
|
-
* `lineNumber + 1` without re-evaluating all preceding lines.
|
|
7470
|
-
*
|
|
7471
|
-
* **Usage:** Phase 5.2e's `setViewport()` calls `restoreTo(viewport.startLine - 1)`
|
|
7472
|
-
* before evaluating only the newly visible lines. This is the key to
|
|
7473
|
-
* O(visible lines) scrolling.
|
|
7474
|
-
*
|
|
7475
|
-
* @param lineNumber The line number to restore to. Variables defined
|
|
7476
|
-
* at lines ≤ this number will be available in the VM.
|
|
7477
|
-
*/
|
|
7478
|
-
restoreTo(lineNumber) {
|
|
7479
|
-
if (this.checkpointer) {
|
|
7480
|
-
this.checkpointer.restoreTo(lineNumber);
|
|
6753
|
+
if (updatedLineNumbers.length > 0 || allQueryKeys.length > 0) {
|
|
6754
|
+
this.notifyListeners({
|
|
6755
|
+
type: "lines-updated",
|
|
6756
|
+
lineNumbers: updatedLineNumbers,
|
|
6757
|
+
affectedQueryKeys: allQueryKeys
|
|
6758
|
+
});
|
|
7481
6759
|
}
|
|
6760
|
+
return updatedLineNumbers;
|
|
7482
6761
|
}
|
|
7483
6762
|
/**
|
|
7484
|
-
*
|
|
7485
|
-
*/
|
|
7486
|
-
getCheckpointer() {
|
|
7487
|
-
return this.checkpointer;
|
|
7488
|
-
}
|
|
7489
|
-
/**
|
|
7490
|
-
* Get the PageManager (Phase 5.2g).
|
|
7491
|
-
* Exposed for testing.
|
|
7492
|
-
*/
|
|
7493
|
-
getPageManager() {
|
|
7494
|
-
return this.pageManager;
|
|
7495
|
-
}
|
|
7496
|
-
// ── Phase 5.2g: Directional preloading ──────────────────────────
|
|
7497
|
-
/**
|
|
7498
|
-
* Preload the next 1–2 pages in the current scroll direction.
|
|
7499
|
-
*
|
|
7500
|
-
* Called during `setViewport()` (scroll-only path). Collects dirty
|
|
7501
|
-
* uncompiled lines in pages just beyond the viewport and dispatches
|
|
7502
|
-
* them to the background compilation worker so bytecode is ready
|
|
7503
|
-
* before the user scrolls those lines into view.
|
|
7504
|
-
*/
|
|
7505
|
-
preloadNextPages(viewport) {
|
|
7506
|
-
const targets = this.pageManager.getPreloadTargets(viewport, this.doc);
|
|
7507
|
-
if (targets.length === 0) return;
|
|
7508
|
-
if (!this.compilationWorker) {
|
|
7509
|
-
this.compilationWorker = new CompilationWorkerManager();
|
|
7510
|
-
}
|
|
7511
|
-
this.compilationWorker.compileBatch(targets).then((results) => {
|
|
7512
|
-
this.compilationWorker.storeResults(results, this.doc);
|
|
7513
|
-
}).catch((_err) => {
|
|
7514
|
-
});
|
|
7515
|
-
}
|
|
7516
|
-
/**
|
|
7517
|
-
* Collect invisible dirty lines that need background compilation.
|
|
7518
|
-
*
|
|
7519
|
-
* Iterates lines beyond `viewport.endLine`, filtering for:
|
|
7520
|
-
* - Dirty lines (need re-compilation)
|
|
7521
|
-
* - Non-empty, non-markdown lines
|
|
7522
|
-
* - No existing bytecode (skip already-compiled Tier 3 lines)
|
|
6763
|
+
* Notify all consumers of an async resolution event.
|
|
7523
6764
|
*
|
|
7524
|
-
*
|
|
6765
|
+
* Enqueues the event into the internal {@link ReadableStream}.
|
|
6766
|
+
* If the stream has been closed or errored (consumer cancelled),
|
|
6767
|
+
* the enqueue silently fails (caught by try/catch).
|
|
7525
6768
|
*/
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
const startPos = viewport.endLine + 1;
|
|
7530
|
-
for (let pos = startPos; pos <= docEnd; pos++) {
|
|
7531
|
-
const state = this.doc.getLineAt(pos);
|
|
7532
|
-
if (!state) continue;
|
|
7533
|
-
if (!state.dirty) continue;
|
|
7534
|
-
if (state.bytecodes.length > 0 && !state.isVariableDef) continue;
|
|
7535
|
-
if (state.isEmpty || isEmptyLine(state.text)) continue;
|
|
7536
|
-
const { expressions } = this.extractExpressions(state);
|
|
7537
|
-
if (expressions.length === 0) continue;
|
|
7538
|
-
for (const expression of expressions) {
|
|
7539
|
-
if (!expression.trim()) continue;
|
|
7540
|
-
items.push({
|
|
7541
|
-
lineId: state.lineId,
|
|
7542
|
-
expression,
|
|
7543
|
-
textHash: state.textHash
|
|
7544
|
-
});
|
|
7545
|
-
}
|
|
6769
|
+
notifyListeners(event) {
|
|
6770
|
+
if (this._testCaptures) {
|
|
6771
|
+
this._testCaptures.push(event);
|
|
7546
6772
|
}
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
* For full-line expressions: returns `{ expressions: [trimmedText], inlineSolveCount: 0 }`.
|
|
7553
|
-
* For inline solve lines: returns `{ expressions: [...allSolves], inlineSolveCount: N }`.
|
|
7554
|
-
* For pre-extracted (cached) expressions: returns the cached array.
|
|
7555
|
-
*
|
|
7556
|
-
* Inline solves are extracted left-to-right via the sharedLexer, so variable
|
|
7557
|
-
* definitions in earlier solves (e.g., `s\`x = 5\` more text s\`x + 1\``)
|
|
7558
|
-
* correctly update the VM state before later solves are evaluated.
|
|
7559
|
-
*/
|
|
7560
|
-
extractExpressions(state) {
|
|
7561
|
-
if (state.expressions.length > 0) {
|
|
7562
|
-
return { expressions: state.expressions, inlineSolveCount: state.inlineSolveCount };
|
|
7563
|
-
}
|
|
7564
|
-
const trimmed = state.text.trim();
|
|
7565
|
-
if (trimmed.length === 0) return { expressions: [], inlineSolveCount: 0 };
|
|
7566
|
-
const inlineSpans = sharedLexer.findInlineSolves(state.text);
|
|
7567
|
-
if (inlineSpans.length > 0) {
|
|
7568
|
-
return {
|
|
7569
|
-
expressions: inlineSpans.map((s) => s.expression),
|
|
7570
|
-
inlineSolveCount: inlineSpans.length
|
|
7571
|
-
};
|
|
6773
|
+
if (this._streamController) {
|
|
6774
|
+
try {
|
|
6775
|
+
this._streamController.enqueue(event);
|
|
6776
|
+
} catch {
|
|
6777
|
+
}
|
|
7572
6778
|
}
|
|
7573
|
-
return { expressions: [trimmed], inlineSolveCount: 0 };
|
|
7574
6779
|
}
|
|
7575
6780
|
};
|
|
6781
|
+
// ── Web Streams API integration ──────────────────────────────────
|
|
6782
|
+
/**
|
|
6783
|
+
* Default high-water mark for the internal event stream.
|
|
6784
|
+
* Limits the internal buffer size before the stream signals to
|
|
6785
|
+
* consumers that they need to catch up.
|
|
6786
|
+
*/
|
|
6787
|
+
_AsyncResolutionBatcher.DEFAULT_HIGH_WATER_MARK = 64;
|
|
6788
|
+
var AsyncResolutionBatcher = _AsyncResolutionBatcher;
|
|
7576
6789
|
|
|
7577
|
-
export { AsyncResolutionBatcher,
|
|
7578
|
-
//# sourceMappingURL=chunk-
|
|
7579
|
-
//# sourceMappingURL=chunk-
|
|
6790
|
+
export { AsyncResolutionBatcher, ExpressionEngine, SNAPSHOT_FORMAT, SNAPSHOT_VERSION, SnapshotErrorCodes, assertEngineVersionCompatible, checkEngineVersionCompatibility, checkExpressionComplexity, checkExpressionLength, checkPackageCompatibility, engine_worker_default, extractReadsAndWrites, findInlineSolvesInLine, isEmptyLine };
|
|
6791
|
+
//# sourceMappingURL=chunk-JIPATHVY.js.map
|
|
6792
|
+
//# sourceMappingURL=chunk-JIPATHVY.js.map
|