solve-engine 1.0.1 → 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-D78E2yDW.d.cts → Lexer-BOs7euZe.d.cts} +58 -1
- package/dist/{Lexer-BN5mt30n.d.ts → Lexer-CSI_lwbW.d.ts} +58 -1
- package/dist/PackageCompatibility-B-7rK1TD.d.cts +76 -0
- package/dist/PackageCompatibility-Dh59eF-X.d.ts +76 -0
- package/dist/{PackageRegistry-B37mRvvz.d.cts → PackageRegistry-BHWJP83F.d.cts} +449 -11
- package/dist/{PackageRegistry-DCUEzbQt.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-BHTNFEGZ.cjs → chunk-C75AFGVD.cjs} +11 -11
- package/dist/{chunk-BHTNFEGZ.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-TBN7DEHO.js → chunk-G7Z4HJQA.js} +11 -5
- package/dist/chunk-G7Z4HJQA.js.map +1 -0
- package/dist/{chunk-QY25VWBF.js → chunk-GQMUHVE3.js} +142 -6
- 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-V5PYO44Y.js → chunk-JIPATHVY.js} +1853 -2638
- package/dist/chunk-JIPATHVY.js.map +1 -0
- package/dist/{chunk-YPPPYLSR.js → chunk-JJIXHXFQ.js} +5 -5
- package/dist/{chunk-YPPPYLSR.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-B7TLZABL.cjs → chunk-MG6Q3DUO.cjs} +1962 -2746
- 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-5F4C26RU.js → chunk-OADDUPT3.js} +4 -4
- package/dist/{chunk-5F4C26RU.js.map → chunk-OADDUPT3.js.map} +1 -1
- package/dist/{chunk-3AFRJYP4.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-HMOISHXR.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-KBSXGXPM.cjs → chunk-XJGJCF2R.cjs} +6 -6
- package/dist/{chunk-KBSXGXPM.cjs.map → chunk-XJGJCF2R.cjs.map} +1 -1
- package/dist/{chunk-YWQ6V4ZN.cjs → chunk-YTIYVHL7.cjs} +211 -75
- 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-2XLQPQKI.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-2XLQPQKI.cjs.map +0 -1
- package/dist/chunk-3AFRJYP4.cjs.map +0 -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-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-AJA6LUI7.js.map +0 -1
- package/dist/chunk-B7TLZABL.cjs.map +0 -1
- package/dist/chunk-GQCOSXMG.js.map +0 -1
- package/dist/chunk-HDP7VK3C.cjs.map +0 -1
- package/dist/chunk-HMOISHXR.js.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-O3ANBHSA.js.map +0 -1
- package/dist/chunk-PA4VC73I.cjs.map +0 -1
- package/dist/chunk-PFUESQTW.cjs.map +0 -1
- package/dist/chunk-QY25VWBF.js.map +0 -1
- package/dist/chunk-R3PY4G7J.js.map +0 -1
- package/dist/chunk-TBN7DEHO.js.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-V5PYO44Y.js.map +0 -1
- package/dist/chunk-VB37OC6I.js.map +0 -1
- package/dist/chunk-Y7FT4IQT.js.map +0 -1
- package/dist/chunk-YWQ6V4ZN.cjs.map +0 -1
|
@@ -0,0 +1,1768 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkMG6Q3DUO_cjs = require('./chunk-MG6Q3DUO.cjs');
|
|
4
|
+
var chunkL2TE7PMO_cjs = require('./chunk-L2TE7PMO.cjs');
|
|
5
|
+
var chunkADBA3XBE_cjs = require('./chunk-ADBA3XBE.cjs');
|
|
6
|
+
var chunk7ZX6B7SY_cjs = require('./chunk-7ZX6B7SY.cjs');
|
|
7
|
+
var chunk2TZKENDH_cjs = require('./chunk-2TZKENDH.cjs');
|
|
8
|
+
var chunkZXCESUJS_cjs = require('./chunk-ZXCESUJS.cjs');
|
|
9
|
+
var chunkXJGJCF2R_cjs = require('./chunk-XJGJCF2R.cjs');
|
|
10
|
+
var chunkB4HBEFTB_cjs = require('./chunk-B4HBEFTB.cjs');
|
|
11
|
+
|
|
12
|
+
// src/engine/SegmentTree.ts
|
|
13
|
+
var SegmentTree = class {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.root = null;
|
|
16
|
+
}
|
|
17
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
18
|
+
/** Total number of elements in the tree. */
|
|
19
|
+
get length() {
|
|
20
|
+
return this.root ? this.root.size : 0;
|
|
21
|
+
}
|
|
22
|
+
/** Whether the tree is empty. */
|
|
23
|
+
get isEmpty() {
|
|
24
|
+
return this.root === null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get the lineId at the given 0-based index.
|
|
28
|
+
* Returns undefined if index is out of bounds.
|
|
29
|
+
* O(log N) expected.
|
|
30
|
+
*/
|
|
31
|
+
getAt(index) {
|
|
32
|
+
if (index < 0 || index >= (this.root?.size ?? 0)) return void 0;
|
|
33
|
+
return this.nodeAt(this.root, index).lineId;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Insert a single lineId at the given 0-based index.
|
|
37
|
+
* O(log N) expected.
|
|
38
|
+
*/
|
|
39
|
+
insertAt(index, lineId) {
|
|
40
|
+
const [left, right] = split(this.root, index);
|
|
41
|
+
this.root = merge(merge(left, createNode(lineId)), right);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Delete a single element at the given 0-based index.
|
|
45
|
+
* Returns the removed lineId, or undefined if out of bounds.
|
|
46
|
+
* O(log N) expected.
|
|
47
|
+
*/
|
|
48
|
+
deleteAt(index) {
|
|
49
|
+
if (index < 0 || index >= (this.root?.size ?? 0)) return void 0;
|
|
50
|
+
const [left, midRight] = split(this.root, index);
|
|
51
|
+
const [mid, right] = split(midRight, 1);
|
|
52
|
+
const removed = mid ? mid.lineId : void 0;
|
|
53
|
+
this.root = merge(left, right);
|
|
54
|
+
return removed;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Bulk splice: delete `deleteCount` elements at `startIndex` (0-based),
|
|
58
|
+
* then insert `newIds` in their place.
|
|
59
|
+
*
|
|
60
|
+
* Returns the removed lineId array.
|
|
61
|
+
* O(log N + deleteCount + insertCount) expected.
|
|
62
|
+
*/
|
|
63
|
+
spliceAt(startIndex, deleteCount, newIds) {
|
|
64
|
+
const clampedStart = Math.max(0, startIndex);
|
|
65
|
+
const clampedCount = Math.min(
|
|
66
|
+
Math.max(0, deleteCount),
|
|
67
|
+
(this.root?.size ?? 0) - clampedStart
|
|
68
|
+
);
|
|
69
|
+
const [left, midRight] = split(this.root, clampedStart);
|
|
70
|
+
const [mid, right] = split(midRight, clampedCount);
|
|
71
|
+
const removed = collectInOrder(mid);
|
|
72
|
+
const newTree = buildTreap(newIds, 0, newIds.length);
|
|
73
|
+
this.root = merge(merge(left, newTree), right);
|
|
74
|
+
return removed;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Replace the entire tree with a new set of lineIds.
|
|
78
|
+
* O(N), builds a balanced treap from a flat array.
|
|
79
|
+
*/
|
|
80
|
+
replaceAll(lineIds) {
|
|
81
|
+
this.root = buildTreap(lineIds, 0, lineIds.length);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get a contiguous range of lineIds by index.
|
|
85
|
+
* O(rangeSize + log N), single in-order walk instead of N × O(log N).
|
|
86
|
+
*
|
|
87
|
+
* This is the hot path for viewport rendering in DocumentModel.getVisibleLines().
|
|
88
|
+
*/
|
|
89
|
+
getRange(startIndex, endIndex) {
|
|
90
|
+
const clampedStart = Math.max(0, startIndex);
|
|
91
|
+
const clampedEnd = Math.min((this.root?.size ?? 0) - 1, endIndex);
|
|
92
|
+
if (clampedStart > clampedEnd) return [];
|
|
93
|
+
const result = [];
|
|
94
|
+
this.collectRange(this.root, clampedStart, clampedEnd, 0, result);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Clear the tree.
|
|
99
|
+
*/
|
|
100
|
+
clear() {
|
|
101
|
+
this.root = null;
|
|
102
|
+
}
|
|
103
|
+
// ── Iteration ─────────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* In-order iterator yielding all lineIds.
|
|
106
|
+
*/
|
|
107
|
+
*[Symbol.iterator]() {
|
|
108
|
+
yield* this.inOrder(this.root);
|
|
109
|
+
}
|
|
110
|
+
*inOrder(node) {
|
|
111
|
+
if (!node) return;
|
|
112
|
+
yield* this.inOrder(node.left);
|
|
113
|
+
yield node.lineId;
|
|
114
|
+
yield* this.inOrder(node.right);
|
|
115
|
+
}
|
|
116
|
+
// ── Private helpers ───────────────────────────────────────────────────
|
|
117
|
+
nodeAt(node, index) {
|
|
118
|
+
const leftSize = node.left ? node.left.size : 0;
|
|
119
|
+
if (index < leftSize) {
|
|
120
|
+
return this.nodeAt(node.left, index);
|
|
121
|
+
} else if (index === leftSize) {
|
|
122
|
+
return node;
|
|
123
|
+
} else {
|
|
124
|
+
return this.nodeAt(node.right, index - leftSize - 1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Collect all node values in the range [targetStart, targetEnd] into result.
|
|
129
|
+
* `offset` is the 0-based start of the current subtree within the full tree.
|
|
130
|
+
* O(rangeSize + log N), skips entire subtrees outside the target range.
|
|
131
|
+
*/
|
|
132
|
+
collectRange(node, targetStart, targetEnd, offset, result) {
|
|
133
|
+
if (!node) return;
|
|
134
|
+
const leftSize = node.left ? node.left.size : 0;
|
|
135
|
+
const nodeIndex = offset + leftSize;
|
|
136
|
+
if (nodeIndex > targetStart && node.left) {
|
|
137
|
+
this.collectRange(node.left, targetStart, targetEnd, offset, result);
|
|
138
|
+
}
|
|
139
|
+
if (nodeIndex >= targetStart && nodeIndex <= targetEnd) {
|
|
140
|
+
result.push(node.lineId);
|
|
141
|
+
}
|
|
142
|
+
if (nodeIndex < targetEnd && node.right) {
|
|
143
|
+
this.collectRange(node.right, targetStart, targetEnd, nodeIndex + 1, result);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function createNode(lineId) {
|
|
148
|
+
return {
|
|
149
|
+
lineId,
|
|
150
|
+
size: 1,
|
|
151
|
+
priority: Math.random(),
|
|
152
|
+
left: null,
|
|
153
|
+
right: null
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function updateSize(node) {
|
|
157
|
+
node.size = 1 + (node.left ? node.left.size : 0) + (node.right ? node.right.size : 0);
|
|
158
|
+
}
|
|
159
|
+
function merge(left, right) {
|
|
160
|
+
if (!left) return right;
|
|
161
|
+
if (!right) return left;
|
|
162
|
+
if (left.priority > right.priority) {
|
|
163
|
+
left.right = merge(left.right, right);
|
|
164
|
+
updateSize(left);
|
|
165
|
+
return left;
|
|
166
|
+
} else {
|
|
167
|
+
right.left = merge(left, right.left);
|
|
168
|
+
updateSize(right);
|
|
169
|
+
return right;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function split(node, index) {
|
|
173
|
+
if (!node) return [null, null];
|
|
174
|
+
const leftSize = node.left ? node.left.size : 0;
|
|
175
|
+
if (index <= leftSize) {
|
|
176
|
+
const [left, right] = split(node.left, index);
|
|
177
|
+
node.left = right;
|
|
178
|
+
updateSize(node);
|
|
179
|
+
return [left, node];
|
|
180
|
+
} else {
|
|
181
|
+
const [left, right] = split(node.right, index - leftSize - 1);
|
|
182
|
+
node.right = left;
|
|
183
|
+
updateSize(node);
|
|
184
|
+
return [node, right];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function collectInOrder(node) {
|
|
188
|
+
const result = [];
|
|
189
|
+
collect(node, result);
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function collect(node, result) {
|
|
193
|
+
if (!node) return;
|
|
194
|
+
collect(node.left, result);
|
|
195
|
+
result.push(node.lineId);
|
|
196
|
+
collect(node.right, result);
|
|
197
|
+
}
|
|
198
|
+
function buildTreap(lineIds, start, end) {
|
|
199
|
+
if (start >= end) return null;
|
|
200
|
+
const mid = Math.floor((start + end) / 2);
|
|
201
|
+
const priority = pseudoRandom(mid);
|
|
202
|
+
const node = {
|
|
203
|
+
lineId: lineIds[mid],
|
|
204
|
+
size: end - start,
|
|
205
|
+
priority,
|
|
206
|
+
left: buildTreap(lineIds, start, mid),
|
|
207
|
+
right: buildTreap(lineIds, mid + 1, end)
|
|
208
|
+
};
|
|
209
|
+
return node;
|
|
210
|
+
}
|
|
211
|
+
function pseudoRandom(seed) {
|
|
212
|
+
let x = seed;
|
|
213
|
+
x = (x >> 16 ^ x) * 73244475;
|
|
214
|
+
x = (x >> 16 ^ x) * 73244475;
|
|
215
|
+
x = x >> 16 ^ x;
|
|
216
|
+
return x / 4294967295;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/engine/DocumentModel.ts
|
|
220
|
+
var DocumentModel = class {
|
|
221
|
+
/**
|
|
222
|
+
* @param maxLines - Ceiling on the line count, defaulting to the engine's
|
|
223
|
+
* configured one. Every line costs a LineState with six arrays in it
|
|
224
|
+
* whatever the line says, so the cost of a document is its line count and
|
|
225
|
+
* nothing else bounds it: two hundred thousand lines of `1 + 1` exhausted
|
|
226
|
+
* the heap here, before a single expression had been looked at.
|
|
227
|
+
*/
|
|
228
|
+
constructor(maxLines = chunkZXCESUJS_cjs.DEFAULT_CONFIG.performance.maxDocumentLines) {
|
|
229
|
+
/** Persistent line ID → LineState. */
|
|
230
|
+
this.lines = /* @__PURE__ */ new Map();
|
|
231
|
+
/** Order-statistic treap representing the current document line order. */
|
|
232
|
+
this.orderTree = new SegmentTree();
|
|
233
|
+
/** Monotonically increasing counter for new line IDs. */
|
|
234
|
+
this.nextLineId = 1;
|
|
235
|
+
/**
|
|
236
|
+
* Lazy position cache: lineId → 1-based position.
|
|
237
|
+
* Built on first `getLinePosition()` call, invalidated on structural edits.
|
|
238
|
+
*/
|
|
239
|
+
this._positionCache = null;
|
|
240
|
+
/**
|
|
241
|
+
* Line IDs currently marked dirty, maintained alongside every
|
|
242
|
+
* `state.dirty` mutation (in this class and in every other module that
|
|
243
|
+
* holds a direct `LineState` reference: ThreeTierEvaluator, PageManager).
|
|
244
|
+
* Lets {@link hasAnyDirtyLineBefore} answer "is anything before position
|
|
245
|
+
* X dirty" in O(d log N), d = current dirty count, typically tiny once
|
|
246
|
+
* a document has settled after its initial evaluation, instead of
|
|
247
|
+
* O(N log N), which used to mean every scroll event re-walked the WHOLE
|
|
248
|
+
* document via `getLineAt()` regardless of how little of it was actually
|
|
249
|
+
* dirty. Benchmarked: ~10.6ms per setViewport() call scrolled near the
|
|
250
|
+
* bottom of a 20k-line document before this fix.
|
|
251
|
+
*/
|
|
252
|
+
this.dirtyLineIds = /* @__PURE__ */ new Set();
|
|
253
|
+
this.maxLines = maxLines;
|
|
254
|
+
}
|
|
255
|
+
// ── Initialization ──────────────────────────────────────────────────
|
|
256
|
+
/**
|
|
257
|
+
* Initialize or replace the entire document from a text blob.
|
|
258
|
+
* Clears all existing state and assigns new persistent line IDs.
|
|
259
|
+
*
|
|
260
|
+
* @throws `DOCUMENT_TOO_LARGE` for a document past {@link maxLines}, before
|
|
261
|
+
* any of it is stored. Recoverable: nothing has been replaced yet, so the
|
|
262
|
+
* model still holds whatever it held.
|
|
263
|
+
*/
|
|
264
|
+
setDocument(text) {
|
|
265
|
+
const lineCount = chunkADBA3XBE_cjs.countLines(text, this.maxLines);
|
|
266
|
+
if (lineCount > this.maxLines) {
|
|
267
|
+
throw chunkB4HBEFTB_cjs.ErrorFactory.execution(
|
|
268
|
+
"DOCUMENT_TOO_LARGE",
|
|
269
|
+
`This document has more than ${this.maxLines.toLocaleString("en-US")} lines, which is the most the engine will hold at once`,
|
|
270
|
+
{ maxLines: this.maxLines }
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
this.lines.clear();
|
|
274
|
+
this.orderTree.clear();
|
|
275
|
+
this._positionCache = null;
|
|
276
|
+
this.dirtyLineIds.clear();
|
|
277
|
+
this.nextLineId = 1;
|
|
278
|
+
const rawLines = text.split("\n");
|
|
279
|
+
const lineIds = new Array(rawLines.length);
|
|
280
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
281
|
+
const lineId = this.nextLineId++;
|
|
282
|
+
lineIds[i] = lineId;
|
|
283
|
+
this.lines.set(lineId, {
|
|
284
|
+
lineId,
|
|
285
|
+
textHash: chunkL2TE7PMO_cjs.djb2Hash(rawLines[i]),
|
|
286
|
+
text: rawLines[i],
|
|
287
|
+
expressions: [],
|
|
288
|
+
bytecodes: [],
|
|
289
|
+
reads: [],
|
|
290
|
+
writes: [],
|
|
291
|
+
results: [],
|
|
292
|
+
result: null,
|
|
293
|
+
dirty: true,
|
|
294
|
+
isVariableDef: false,
|
|
295
|
+
isEmpty: rawLines[i].trim().length === 0,
|
|
296
|
+
inlineSolveCount: 0
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
this.orderTree.replaceAll(lineIds);
|
|
300
|
+
for (const id of lineIds) this.dirtyLineIds.add(id);
|
|
301
|
+
}
|
|
302
|
+
// ── Structural edits ────────────────────────────────────────────────
|
|
303
|
+
/**
|
|
304
|
+
* Apply one or more line-level changes to the document.
|
|
305
|
+
*
|
|
306
|
+
* **Precondition:** Changes must be **non-overlapping** in their line ranges.
|
|
307
|
+
* If two changes target the same or adjacent lines, the reverse-order
|
|
308
|
+
* processing may produce incorrect results because the first-applied
|
|
309
|
+
* change shifts the line numbers that the second change references.
|
|
310
|
+
*
|
|
311
|
+
* Changes are applied in **reverse order** (highest startLine first) so
|
|
312
|
+
* that earlier changes in the document don't shift the indices of later
|
|
313
|
+
* changes during processing.
|
|
314
|
+
*
|
|
315
|
+
* Returns both the newly inserted line IDs and the removed line IDs.
|
|
316
|
+
* Callers should use `removed` to clean up the dependency graph and
|
|
317
|
+
* other data structures keyed by lineId.
|
|
318
|
+
*/
|
|
319
|
+
applyChanges(changes) {
|
|
320
|
+
const inserted = [];
|
|
321
|
+
const removed = [];
|
|
322
|
+
const sorted = [...changes].sort((a, b) => b.startLine - a.startLine);
|
|
323
|
+
for (const change of sorted) {
|
|
324
|
+
const startIdx = change.startLine - 1;
|
|
325
|
+
const newIds = [];
|
|
326
|
+
for (const text of change.insertLines) {
|
|
327
|
+
const lineId = this.nextLineId++;
|
|
328
|
+
newIds.push(lineId);
|
|
329
|
+
inserted.push(lineId);
|
|
330
|
+
this.lines.set(lineId, {
|
|
331
|
+
lineId,
|
|
332
|
+
textHash: chunkL2TE7PMO_cjs.djb2Hash(text),
|
|
333
|
+
text,
|
|
334
|
+
expressions: [],
|
|
335
|
+
bytecodes: [],
|
|
336
|
+
reads: [],
|
|
337
|
+
writes: [],
|
|
338
|
+
results: [],
|
|
339
|
+
result: null,
|
|
340
|
+
dirty: true,
|
|
341
|
+
isVariableDef: false,
|
|
342
|
+
isEmpty: text.trim().length === 0,
|
|
343
|
+
inlineSolveCount: 0
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
for (const id of newIds) this.dirtyLineIds.add(id);
|
|
347
|
+
const removedIds = this.orderTree.spliceAt(
|
|
348
|
+
startIdx,
|
|
349
|
+
change.deleteCount,
|
|
350
|
+
newIds
|
|
351
|
+
);
|
|
352
|
+
for (const id of removedIds) {
|
|
353
|
+
removed.push(id);
|
|
354
|
+
this.lines.delete(id);
|
|
355
|
+
this.dirtyLineIds.delete(id);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
this._positionCache = null;
|
|
359
|
+
return { inserted, removed };
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Insert new lines at the given 1-based position.
|
|
363
|
+
* Convenience wrapper around applyChanges.
|
|
364
|
+
*/
|
|
365
|
+
insertLines(atLine, texts) {
|
|
366
|
+
const change = {
|
|
367
|
+
startLine: atLine,
|
|
368
|
+
deleteCount: 0,
|
|
369
|
+
insertLines: texts
|
|
370
|
+
};
|
|
371
|
+
const result = this.applyChanges([change]);
|
|
372
|
+
return result.inserted;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Delete lines in the given 1-based range [startLine, endLine] inclusive.
|
|
376
|
+
* Convenience wrapper around applyChanges.
|
|
377
|
+
*/
|
|
378
|
+
deleteLines(startLine, endLine) {
|
|
379
|
+
const change = {
|
|
380
|
+
startLine,
|
|
381
|
+
deleteCount: endLine - startLine + 1,
|
|
382
|
+
insertLines: []
|
|
383
|
+
};
|
|
384
|
+
const result = this.applyChanges([change]);
|
|
385
|
+
return result.removed;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Update the text of a single line in place.
|
|
389
|
+
* If the text hash differs, marks the line dirty and clears its
|
|
390
|
+
* bytecode/result so it gets re-evaluated.
|
|
391
|
+
*
|
|
392
|
+
* Returns true if the text actually changed (hash mismatch).
|
|
393
|
+
*/
|
|
394
|
+
editLine(lineNumber, newText) {
|
|
395
|
+
const state = this.getLineAt(lineNumber);
|
|
396
|
+
if (!state) return false;
|
|
397
|
+
const newHash = chunkL2TE7PMO_cjs.djb2Hash(newText);
|
|
398
|
+
if (newHash === state.textHash) return false;
|
|
399
|
+
state.text = newText;
|
|
400
|
+
state.textHash = newHash;
|
|
401
|
+
state.expressions = [];
|
|
402
|
+
state.bytecodes = [];
|
|
403
|
+
state.results = [];
|
|
404
|
+
state.result = null;
|
|
405
|
+
state.inlineSolveCount = 0;
|
|
406
|
+
state.dirty = true;
|
|
407
|
+
this.dirtyLineIds.add(state.lineId);
|
|
408
|
+
state.isEmpty = newText.trim().length === 0;
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
// ── Queries ─────────────────────────────────────────────────────────
|
|
412
|
+
/**
|
|
413
|
+
* Get the LineState at the given 1-based line position. O(1).
|
|
414
|
+
*/
|
|
415
|
+
getLineAt(position) {
|
|
416
|
+
const idx = position - 1;
|
|
417
|
+
const lineId = this.orderTree.getAt(idx);
|
|
418
|
+
if (lineId === void 0) return void 0;
|
|
419
|
+
return this.lines.get(lineId);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get the 1-based position of a line by its persistent ID.
|
|
423
|
+
* Returns -1 if the line ID is not in the document.
|
|
424
|
+
*
|
|
425
|
+
* Uses a lazy position cache: O(N) on first call after structural edit,
|
|
426
|
+
* O(1) on subsequent calls. The cache is invalidated by any structural edit.
|
|
427
|
+
*/
|
|
428
|
+
getLinePosition(lineId) {
|
|
429
|
+
if (this._positionCache) {
|
|
430
|
+
return this._positionCache.get(lineId) ?? -1;
|
|
431
|
+
}
|
|
432
|
+
this._positionCache = /* @__PURE__ */ new Map();
|
|
433
|
+
let pos = 1;
|
|
434
|
+
for (const id of this.orderTree) {
|
|
435
|
+
this._positionCache.set(id, pos++);
|
|
436
|
+
}
|
|
437
|
+
return this._positionCache.get(lineId) ?? -1;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get all LineState entries within the given viewport range (1-based, inclusive).
|
|
441
|
+
* Uses SegmentTree.getRange() for O(viewport + log N) collection instead of
|
|
442
|
+
* O(viewport × log N) per-line lookups.
|
|
443
|
+
*/
|
|
444
|
+
getVisibleLines(startLine, endLine) {
|
|
445
|
+
const lineIds = this.orderTree.getRange(startLine - 1, endLine - 1);
|
|
446
|
+
const result = [];
|
|
447
|
+
for (const lineId of lineIds) {
|
|
448
|
+
const state = this.lines.get(lineId);
|
|
449
|
+
if (state) result.push(state);
|
|
450
|
+
}
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Get all LineState entries in order. Useful for batch processing.
|
|
455
|
+
*/
|
|
456
|
+
getAllLines() {
|
|
457
|
+
return this.getVisibleLines(1, this.lineCount);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Get a LineState by its persistent line ID. O(1).
|
|
461
|
+
*/
|
|
462
|
+
getLineById(lineId) {
|
|
463
|
+
return this.lines.get(lineId);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Get all lines that are marked dirty.
|
|
467
|
+
*/
|
|
468
|
+
getDirtyLines() {
|
|
469
|
+
const result = [];
|
|
470
|
+
for (const state of this.lines.values()) {
|
|
471
|
+
if (state.dirty) result.push(state);
|
|
472
|
+
}
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Whether any line before `position` (1-based, exclusive) is dirty.
|
|
477
|
+
*
|
|
478
|
+
* Used by ThreeTierEvaluator.setViewport() to decide whether cached
|
|
479
|
+
* checkpoint state might be stale and a full evaluate() (from line 1) is
|
|
480
|
+
* needed instead of the cheap viewport-only path.
|
|
481
|
+
*
|
|
482
|
+
* O(d log N) where d = current dirty line count via {@link dirtyLineIds},
|
|
483
|
+
* not O(N log N), a document that's mostly clean (the steady state after
|
|
484
|
+
* initial load) answers this in the cost of resolving a handful of
|
|
485
|
+
* lineIds to positions, not walking every line up to `position`.
|
|
486
|
+
*/
|
|
487
|
+
hasAnyDirtyLineBefore(position) {
|
|
488
|
+
for (const lineId of this.dirtyLineIds) {
|
|
489
|
+
const pos = this.getLinePosition(lineId);
|
|
490
|
+
if (pos >= 1 && pos < position) return true;
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Whether any **variable-definition** line before `position` (1-based,
|
|
496
|
+
* exclusive) is dirty.
|
|
497
|
+
*
|
|
498
|
+
* Narrower than {@link hasAnyDirtyLineBefore}: `VMCheckpointer.snapshot()`
|
|
499
|
+
* only ever records state for lines with `writes.length > 0` (see
|
|
500
|
+
* VMCheckpoints.ts), so a dirty plain-expression line before the viewport
|
|
501
|
+
* cannot have invalidated any checkpoint, there's no checkpoint entry
|
|
502
|
+
* for it to invalidate. Only a dirty variable-def line can mean the VM
|
|
503
|
+
* state a checkpoint would restore is stale.
|
|
504
|
+
*
|
|
505
|
+
* This distinction matters because `PageManager.evictPageBytecode()`
|
|
506
|
+
* marks evicted non-variable-def lines dirty (so they get Tier 1 if
|
|
507
|
+
* scrolled back into view), and Tier 3's compile-only path never clears
|
|
508
|
+
* `dirty` for non-variable-def lines by design. Using the broader
|
|
509
|
+
* `hasAnyDirtyLineBefore` here meant scrolling far into a large,
|
|
510
|
+
* variable-def-free document would trip `setViewport()`'s fallback to
|
|
511
|
+
* `evaluate()` on every single call, evaluate() reprocesses the evicted
|
|
512
|
+
* lines via Tier 3, which recompiles their bytecode without clearing
|
|
513
|
+
* dirty, so the very next `maintainAfterEval()` re-evicts and re-dirties
|
|
514
|
+
* the same lines, forever re-triggering the fallback on an otherwise
|
|
515
|
+
* unchanged viewport.
|
|
516
|
+
*/
|
|
517
|
+
hasAnyDirtyVariableDefLineBefore(position) {
|
|
518
|
+
for (const lineId of this.dirtyLineIds) {
|
|
519
|
+
const state = this.lines.get(lineId);
|
|
520
|
+
if (!state || !state.isVariableDef) continue;
|
|
521
|
+
const pos = this.getLinePosition(lineId);
|
|
522
|
+
if (pos >= 1 && pos < position) return true;
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
/** Number of lines currently marked dirty. For diagnostics/tests. */
|
|
527
|
+
get dirtyCount() {
|
|
528
|
+
return this.dirtyLineIds.size;
|
|
529
|
+
}
|
|
530
|
+
// ── Thread-safety validation ────────────────────────────────────────
|
|
531
|
+
/**
|
|
532
|
+
* Verify that bytecode compiled by a worker is still valid for this line.
|
|
533
|
+
*
|
|
534
|
+
* When Phase 5.2h sends compilation to a worker, the worker posts back
|
|
535
|
+
* `{lineId, bytecode, reads, writes, compiledAgainstHash}`. Between dispatch
|
|
536
|
+
* and response, the user may have edited the line. This method lets the
|
|
537
|
+
* main thread check whether the bytecode is still applicable.
|
|
538
|
+
*
|
|
539
|
+
* @returns true if the line still exists and its text hash matches.
|
|
540
|
+
*/
|
|
541
|
+
isBytecodeValid(lineId, compiledAgainstHash) {
|
|
542
|
+
const state = this.lines.get(lineId);
|
|
543
|
+
return state !== void 0 && state.textHash === compiledAgainstHash;
|
|
544
|
+
}
|
|
545
|
+
// ── State mutations ─────────────────────────────────────────────────
|
|
546
|
+
/**
|
|
547
|
+
* Mark a line as clean (re-evaluated successfully).
|
|
548
|
+
*/
|
|
549
|
+
markClean(lineId) {
|
|
550
|
+
const state = this.lines.get(lineId);
|
|
551
|
+
if (state) {
|
|
552
|
+
state.dirty = false;
|
|
553
|
+
this.dirtyLineIds.delete(lineId);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Mark a line as dirty (needs re-evaluation) by its 1-based position.
|
|
558
|
+
* Convenience for callers that have line numbers instead of line IDs.
|
|
559
|
+
*/
|
|
560
|
+
markDirtyByLineNumber(lineNumber) {
|
|
561
|
+
const state = this.getLineAt(lineNumber);
|
|
562
|
+
if (state) {
|
|
563
|
+
state.dirty = true;
|
|
564
|
+
this.dirtyLineIds.add(state.lineId);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Mark a line as dirty (needs re-evaluation).
|
|
569
|
+
*/
|
|
570
|
+
markDirty(lineId) {
|
|
571
|
+
const state = this.lines.get(lineId);
|
|
572
|
+
if (state) {
|
|
573
|
+
state.dirty = true;
|
|
574
|
+
this.dirtyLineIds.add(lineId);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Mark all lines as dirty (e.g., after plugin register/unregister).
|
|
579
|
+
*/
|
|
580
|
+
invalidateAll() {
|
|
581
|
+
for (const state of this.lines.values()) {
|
|
582
|
+
state.dirty = true;
|
|
583
|
+
}
|
|
584
|
+
this.dirtyLineIds = new Set(this.lines.keys());
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Update a line's evaluation state after successful execution (Tier 1 / Tier 2).
|
|
588
|
+
*
|
|
589
|
+
* Sets results, bytecodes, reads, writes, and marks the line clean.
|
|
590
|
+
* Supports multi-expression lines (inline solves) via parallel arrays.
|
|
591
|
+
*
|
|
592
|
+
* @param lineId - Persistent line identifier.
|
|
593
|
+
* @param results - Evaluation result groups for each expression (in order). Each element is a Value[].
|
|
594
|
+
* @param bytecodes - Compiled bytecode for each expression (in order).
|
|
595
|
+
* @param expressions - Extracted expression strings (in order).
|
|
596
|
+
* @param reads - Aggregated read variables across all expressions.
|
|
597
|
+
* @param writes - Aggregated write variables across all expressions.
|
|
598
|
+
* @param isVariableDef - True if any expression defines a variable.
|
|
599
|
+
* @param inlineSolveCount - Number of inline solves (0 for full-line).
|
|
600
|
+
*/
|
|
601
|
+
updateLineResult(lineId, results, bytecodes, expressions, reads, writes, isVariableDef, inlineSolveCount = 0) {
|
|
602
|
+
const state = this.lines.get(lineId);
|
|
603
|
+
if (!state) return;
|
|
604
|
+
state.results = results;
|
|
605
|
+
state.result = results[0]?.[0] ?? null;
|
|
606
|
+
state.bytecodes = bytecodes;
|
|
607
|
+
state.expressions = expressions;
|
|
608
|
+
state.reads = reads;
|
|
609
|
+
state.writes = writes;
|
|
610
|
+
state.isVariableDef = isVariableDef;
|
|
611
|
+
state.inlineSolveCount = inlineSolveCount;
|
|
612
|
+
state.dirty = false;
|
|
613
|
+
this.dirtyLineIds.delete(lineId);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Update a line's compile-only state (Tier 3: background compilation).
|
|
617
|
+
*
|
|
618
|
+
* Stores expressions, bytecodes, reads, and writes. Does NOT set results
|
|
619
|
+
* and does NOT mark the line clean, it still needs execution (Tier 1 or
|
|
620
|
+
* Tier 2) to produce results. This distinction allows the three-tier
|
|
621
|
+
* evaluation strategy: compile invisible lines in the background without
|
|
622
|
+
* executing them, then execute from cached bytecode when scrolled into view.
|
|
623
|
+
*
|
|
624
|
+
* @param lineId - Persistent line identifier.
|
|
625
|
+
* @param expressions - Extracted expression strings (in order).
|
|
626
|
+
* @param bytecodes - Compiled bytecode for each expression (in order).
|
|
627
|
+
* @param reads - Aggregated read variables across all expressions.
|
|
628
|
+
* @param writes - Aggregated write variables across all expressions.
|
|
629
|
+
* @param isVariableDef - True if any expression defines a variable.
|
|
630
|
+
* @param inlineSolveCount - Number of inline solves (0 for full-line).
|
|
631
|
+
*/
|
|
632
|
+
updateLineCompiled(lineId, expressions, bytecodes, reads, writes, isVariableDef, inlineSolveCount = 0) {
|
|
633
|
+
const state = this.lines.get(lineId);
|
|
634
|
+
if (!state) return;
|
|
635
|
+
state.expressions = expressions;
|
|
636
|
+
state.bytecodes = bytecodes;
|
|
637
|
+
state.reads = reads;
|
|
638
|
+
state.writes = writes;
|
|
639
|
+
state.isVariableDef = isVariableDef;
|
|
640
|
+
state.inlineSolveCount = inlineSolveCount;
|
|
641
|
+
}
|
|
642
|
+
// ── Properties ──────────────────────────────────────────────────────
|
|
643
|
+
get lineCount() {
|
|
644
|
+
return this.orderTree.length;
|
|
645
|
+
}
|
|
646
|
+
get isEmpty() {
|
|
647
|
+
return this.orderTree.isEmpty;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Iterator over LineState in document order.
|
|
651
|
+
*/
|
|
652
|
+
*[Symbol.iterator]() {
|
|
653
|
+
for (const lineId of this.orderTree) {
|
|
654
|
+
const state = this.lines.get(lineId);
|
|
655
|
+
if (state) yield state;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
659
|
+
clear() {
|
|
660
|
+
this.lines.clear();
|
|
661
|
+
this.orderTree.clear();
|
|
662
|
+
this._positionCache = null;
|
|
663
|
+
this.nextLineId = 1;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Serialize the document model to a plain object for debugging.
|
|
667
|
+
*/
|
|
668
|
+
toJSON() {
|
|
669
|
+
return {
|
|
670
|
+
lineCount: this.lineCount,
|
|
671
|
+
lines: this.getAllLines().map((s) => ({
|
|
672
|
+
lineId: s.lineId,
|
|
673
|
+
text: s.text.substring(0, 80),
|
|
674
|
+
// truncate for readability
|
|
675
|
+
textHash: s.textHash,
|
|
676
|
+
dirty: s.dirty,
|
|
677
|
+
isVariableDef: s.isVariableDef,
|
|
678
|
+
isEmpty: s.isEmpty,
|
|
679
|
+
hasBytecode: s.bytecodes.length > 0,
|
|
680
|
+
hasResult: s.results.length > 0,
|
|
681
|
+
inlineSolveCount: s.inlineSolveCount,
|
|
682
|
+
reads: s.reads,
|
|
683
|
+
writes: s.writes
|
|
684
|
+
}))
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// src/engine/CompilationWorkerManager.ts
|
|
690
|
+
var CompilationWorkerManager = class {
|
|
691
|
+
constructor() {
|
|
692
|
+
this.worker = null;
|
|
693
|
+
this.nextId = 1;
|
|
694
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Ensure the worker is started (lazy initialization).
|
|
698
|
+
* Uses esbuild-plugin-inline-worker to inline the worker as a blob URL.
|
|
699
|
+
*/
|
|
700
|
+
ensureWorker() {
|
|
701
|
+
if (this.worker) return this.worker;
|
|
702
|
+
this.worker = chunkMG6Q3DUO_cjs.engine_worker_default();
|
|
703
|
+
this.worker.onmessage = (event) => {
|
|
704
|
+
const data = event.data;
|
|
705
|
+
if (!data || data.type !== "COMPILE_RESULT") return;
|
|
706
|
+
const pending = this.pending.get(data.id);
|
|
707
|
+
if (!pending) return;
|
|
708
|
+
this.pending.delete(data.id);
|
|
709
|
+
const results = [];
|
|
710
|
+
for (const raw of data.results) {
|
|
711
|
+
results.push(this.reconstructResult(raw));
|
|
712
|
+
}
|
|
713
|
+
pending.resolve(results);
|
|
714
|
+
};
|
|
715
|
+
this.worker.onerror = (err) => {
|
|
716
|
+
for (const [id, pending] of this.pending) {
|
|
717
|
+
pending.reject(
|
|
718
|
+
new Error(`Compilation worker error: ${err.message}`)
|
|
719
|
+
);
|
|
720
|
+
this.pending.delete(id);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
return this.worker;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Batch-compile expressions in the worker.
|
|
727
|
+
*
|
|
728
|
+
* Sends a batch of { lineId, expression, textHash } to the worker,
|
|
729
|
+
* which compiles each and transfers bytecode ArrayBuffers back.
|
|
730
|
+
* Bytecode is reconstructed into BytecodeProgram objects.
|
|
731
|
+
*
|
|
732
|
+
* Results are returned in the same order as the input items.
|
|
733
|
+
*
|
|
734
|
+
* @param items Expressions to compile.
|
|
735
|
+
* @returns Compiled bytecode for each item (or error).
|
|
736
|
+
*/
|
|
737
|
+
async compileBatch(items) {
|
|
738
|
+
if (items.length === 0) return [];
|
|
739
|
+
const worker = this.ensureWorker();
|
|
740
|
+
const id = this.nextId++;
|
|
741
|
+
return new Promise((resolve, reject) => {
|
|
742
|
+
this.pending.set(id, { resolve, reject });
|
|
743
|
+
worker.postMessage({
|
|
744
|
+
type: "COMPILE_BATCH",
|
|
745
|
+
id,
|
|
746
|
+
items
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Store worker-compiled bytecode into the DocumentModel, with safety
|
|
752
|
+
* validation. Only stores bytecode for lines whose text has not changed
|
|
753
|
+
* since the compilation request was dispatched.
|
|
754
|
+
*
|
|
755
|
+
* Results for the same lineId are batched, all bytecodes from
|
|
756
|
+
* successful compilations are passed to updateLineCompiled in a single
|
|
757
|
+
* call. This supports multi-expression lines (inline solves).
|
|
758
|
+
*
|
|
759
|
+
* @param results Compiled results from the worker.
|
|
760
|
+
* @param doc The target document model.
|
|
761
|
+
* @returns Number of results successfully stored (passed safety check).
|
|
762
|
+
*/
|
|
763
|
+
storeResults(results, doc) {
|
|
764
|
+
const byLineId = /* @__PURE__ */ new Map();
|
|
765
|
+
for (const result of results) {
|
|
766
|
+
if (result.error) continue;
|
|
767
|
+
const state = doc.getLineById(result.lineId);
|
|
768
|
+
if (!state) continue;
|
|
769
|
+
if (state.textHash !== result.compiledAgainstHash) continue;
|
|
770
|
+
let batch = byLineId.get(result.lineId);
|
|
771
|
+
if (!batch) {
|
|
772
|
+
batch = {
|
|
773
|
+
bytecodes: [],
|
|
774
|
+
reads: /* @__PURE__ */ new Set(),
|
|
775
|
+
writes: /* @__PURE__ */ new Set(),
|
|
776
|
+
isVariableDef: false,
|
|
777
|
+
expressions: state.expressions.length > 0 ? [...state.expressions] : [],
|
|
778
|
+
textHash: result.compiledAgainstHash
|
|
779
|
+
};
|
|
780
|
+
byLineId.set(result.lineId, batch);
|
|
781
|
+
}
|
|
782
|
+
batch.bytecodes.push(result.program);
|
|
783
|
+
for (const r of result.reads) batch.reads.add(r);
|
|
784
|
+
for (const w of result.writes) batch.writes.add(w);
|
|
785
|
+
if (result.isVariableDef) batch.isVariableDef = true;
|
|
786
|
+
}
|
|
787
|
+
let stored = 0;
|
|
788
|
+
for (const [lineId, batch] of byLineId) {
|
|
789
|
+
if (batch.bytecodes.length === 0) continue;
|
|
790
|
+
const state = doc.getLineById(lineId);
|
|
791
|
+
const inlineSolveCount = state?.inlineSolveCount ?? 0;
|
|
792
|
+
doc.updateLineCompiled(
|
|
793
|
+
lineId,
|
|
794
|
+
batch.expressions,
|
|
795
|
+
batch.bytecodes,
|
|
796
|
+
[...batch.reads],
|
|
797
|
+
[...batch.writes],
|
|
798
|
+
batch.isVariableDef,
|
|
799
|
+
inlineSolveCount
|
|
800
|
+
);
|
|
801
|
+
stored += batch.bytecodes.length;
|
|
802
|
+
}
|
|
803
|
+
return stored;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Terminate the worker and clean up.
|
|
807
|
+
*/
|
|
808
|
+
terminate() {
|
|
809
|
+
if (this.worker) {
|
|
810
|
+
this.worker.terminate();
|
|
811
|
+
this.worker = null;
|
|
812
|
+
this.pending.clear();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Whether the worker is currently active.
|
|
817
|
+
*/
|
|
818
|
+
get isActive() {
|
|
819
|
+
return this.worker !== null;
|
|
820
|
+
}
|
|
821
|
+
// ── Private helpers ────────────────────────────────────────────────
|
|
822
|
+
/**
|
|
823
|
+
* Reconstruct a BytecodeProgram from transferred ArrayBuffers.
|
|
824
|
+
*
|
|
825
|
+
* The transferred ArrayBuffers are zero-copy, we create TypedArray
|
|
826
|
+
* views directly over them. No data is copied or serialized.
|
|
827
|
+
*/
|
|
828
|
+
reconstructResult(raw) {
|
|
829
|
+
if (raw.error) {
|
|
830
|
+
return {
|
|
831
|
+
lineId: raw.lineId,
|
|
832
|
+
compiledAgainstHash: raw.compiledAgainstHash,
|
|
833
|
+
program: { opcodes: new Uint8Array(0), numbers: new Float64Array(0), strings: [], hasAsync: false },
|
|
834
|
+
reads: [],
|
|
835
|
+
writes: [],
|
|
836
|
+
isVariableDef: false,
|
|
837
|
+
error: raw.error
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const opcodes = raw.opcodesLength > 0 ? new Uint8Array(raw.opcodesBuffer, 0, raw.opcodesLength) : new Uint8Array(0);
|
|
841
|
+
const numbers = raw.numbersLength > 0 ? new Float64Array(raw.numbersBuffer, 0, raw.numbersLength) : new Float64Array(0);
|
|
842
|
+
const program = {
|
|
843
|
+
opcodes,
|
|
844
|
+
numbers,
|
|
845
|
+
strings: raw.strings,
|
|
846
|
+
hasAsync: false
|
|
847
|
+
};
|
|
848
|
+
return {
|
|
849
|
+
lineId: raw.lineId,
|
|
850
|
+
compiledAgainstHash: raw.compiledAgainstHash,
|
|
851
|
+
program,
|
|
852
|
+
reads: raw.reads,
|
|
853
|
+
writes: raw.writes,
|
|
854
|
+
isVariableDef: raw.isVariableDef,
|
|
855
|
+
error: null
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
// src/engine/PageManager.ts
|
|
861
|
+
var PAGE_SIZE = 128;
|
|
862
|
+
var HOT_PAGE_RADIUS = 3;
|
|
863
|
+
var WARM_PAGE_RADIUS = 6;
|
|
864
|
+
var PRELOAD_PAGE_COUNT = 2;
|
|
865
|
+
var COLD_EVICT_BUFFER = HOT_PAGE_RADIUS;
|
|
866
|
+
var PageManager = class _PageManager {
|
|
867
|
+
constructor() {
|
|
868
|
+
/** Previous viewport for scroll direction detection. null = no previous viewport. */
|
|
869
|
+
this.lastViewportStart = null;
|
|
870
|
+
/** Saved scroll direction from the most recent maintainAfterEval call. */
|
|
871
|
+
this.savedDirection = null;
|
|
872
|
+
/** Per-page access counter for LRU tracking. */
|
|
873
|
+
this.pageAccess = /* @__PURE__ */ new Map();
|
|
874
|
+
/** Monotonically increasing access sequence number. */
|
|
875
|
+
this.accessSeq = 0;
|
|
876
|
+
}
|
|
877
|
+
// ── Static helpers ─────────────────────────────────────────────────
|
|
878
|
+
/** Get the page number for a 1-based line number (0-based page index). */
|
|
879
|
+
static pageForLine(lineNumber) {
|
|
880
|
+
return Math.floor((lineNumber - 1) / PAGE_SIZE);
|
|
881
|
+
}
|
|
882
|
+
/** Get the 1-based inclusive line range for a page. */
|
|
883
|
+
static pageRange(pageNum, docLineCount) {
|
|
884
|
+
return {
|
|
885
|
+
startLine: pageNum * PAGE_SIZE + 1,
|
|
886
|
+
endLine: Math.min((pageNum + 1) * PAGE_SIZE, docLineCount)
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
890
|
+
/**
|
|
891
|
+
* Detect scroll direction from viewport movement.
|
|
892
|
+
* Returns null on first call (no previous viewport) or no movement.
|
|
893
|
+
*/
|
|
894
|
+
detectDirection(newViewport) {
|
|
895
|
+
if (this.lastViewportStart === null) return null;
|
|
896
|
+
if (newViewport.startLine > this.lastViewportStart) return "down";
|
|
897
|
+
if (newViewport.startLine < this.lastViewportStart) return "up";
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Maintain page tiers after evaluation.
|
|
902
|
+
*
|
|
903
|
+
* 1. Records the new viewport position
|
|
904
|
+
* 2. Touches hot pages (viewport ± HOT_PAGE_RADIUS) for LRU
|
|
905
|
+
* 3. Evicts cold/warm pages based on distance from viewport
|
|
906
|
+
*
|
|
907
|
+
* Call this after every `evaluate()`, `setViewport()`, or `evaluateAll()`.
|
|
908
|
+
*/
|
|
909
|
+
maintainAfterEval(viewport, doc) {
|
|
910
|
+
this.savedDirection = this.detectDirection(viewport);
|
|
911
|
+
this.lastViewportStart = viewport.startLine;
|
|
912
|
+
const docLineCount = doc.lineCount;
|
|
913
|
+
const viewportStartPage = _PageManager.pageForLine(viewport.startLine);
|
|
914
|
+
const viewportEndPage = _PageManager.pageForLine(viewport.endLine);
|
|
915
|
+
const lastPage = _PageManager.pageForLine(docLineCount);
|
|
916
|
+
const hotStart = Math.max(0, viewportStartPage - HOT_PAGE_RADIUS);
|
|
917
|
+
const hotEnd = Math.min(lastPage, viewportEndPage + HOT_PAGE_RADIUS);
|
|
918
|
+
const warmStart = Math.max(0, viewportStartPage - WARM_PAGE_RADIUS);
|
|
919
|
+
const warmEnd = Math.min(lastPage, viewportEndPage + WARM_PAGE_RADIUS);
|
|
920
|
+
for (let p = hotStart; p <= hotEnd; p++) {
|
|
921
|
+
this.touchPage(p);
|
|
922
|
+
}
|
|
923
|
+
for (let p = warmStart; p <= warmEnd; p++) {
|
|
924
|
+
if (p >= hotStart && p <= hotEnd) continue;
|
|
925
|
+
this.evictPageResults(p, doc, docLineCount);
|
|
926
|
+
}
|
|
927
|
+
const coldLeftEnd = warmStart - 1;
|
|
928
|
+
const coldLeftStart = Math.max(0, coldLeftEnd - COLD_EVICT_BUFFER + 1);
|
|
929
|
+
for (let p = coldLeftStart; p <= coldLeftEnd; p++) {
|
|
930
|
+
this.evictPageBytecode(p, doc, docLineCount);
|
|
931
|
+
}
|
|
932
|
+
const coldRightStart = warmEnd + 1;
|
|
933
|
+
const coldRightEnd = Math.min(lastPage, coldRightStart + COLD_EVICT_BUFFER - 1);
|
|
934
|
+
for (let p = coldRightStart; p <= coldRightEnd; p++) {
|
|
935
|
+
this.evictPageBytecode(p, doc, docLineCount);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Get lines ahead of the viewport that should be pre-compiled.
|
|
940
|
+
*
|
|
941
|
+
* Looks PRELOAD_PAGE_COUNT pages ahead of the current scroll direction.
|
|
942
|
+
* Only returns lines that are dirty, don't already have bytecode,
|
|
943
|
+
* and are not empty/markdown-only.
|
|
944
|
+
*
|
|
945
|
+
* @param viewport The current viewport.
|
|
946
|
+
* @param doc The document model.
|
|
947
|
+
* @returns Items ready to send to the compilation worker.
|
|
948
|
+
*/
|
|
949
|
+
getPreloadTargets(viewport, doc) {
|
|
950
|
+
const direction = this.savedDirection;
|
|
951
|
+
if (!direction) return [];
|
|
952
|
+
const items = [];
|
|
953
|
+
const viewportEndPage = _PageManager.pageForLine(viewport.endLine);
|
|
954
|
+
const viewportStartPage = _PageManager.pageForLine(viewport.startLine);
|
|
955
|
+
const lastPage = _PageManager.pageForLine(doc.lineCount);
|
|
956
|
+
const pages = [];
|
|
957
|
+
if (direction === "down") {
|
|
958
|
+
const startPage = viewportEndPage + HOT_PAGE_RADIUS + 1;
|
|
959
|
+
const endPage = Math.min(startPage + PRELOAD_PAGE_COUNT - 1, lastPage);
|
|
960
|
+
for (let p = startPage; p <= endPage && p <= lastPage; p++) {
|
|
961
|
+
pages.push(p);
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
const firstPreloadPage = viewportStartPage - HOT_PAGE_RADIUS - 1;
|
|
965
|
+
for (let p = firstPreloadPage; p > firstPreloadPage - PRELOAD_PAGE_COUNT && p >= 0; p--) {
|
|
966
|
+
pages.push(p);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
for (const pageNum of pages) {
|
|
970
|
+
const range = _PageManager.pageRange(pageNum, doc.lineCount);
|
|
971
|
+
for (let pos = range.startLine; pos <= range.endLine; pos++) {
|
|
972
|
+
const state = doc.getLineAt(pos);
|
|
973
|
+
if (!state) continue;
|
|
974
|
+
if (!state.dirty) continue;
|
|
975
|
+
if (state.bytecodes.length > 0 && !state.isVariableDef) continue;
|
|
976
|
+
if (state.isEmpty) continue;
|
|
977
|
+
if (state.expressions.length > 0) {
|
|
978
|
+
for (const expression of state.expressions) {
|
|
979
|
+
if (!expression.trim()) continue;
|
|
980
|
+
items.push({
|
|
981
|
+
lineId: state.lineId,
|
|
982
|
+
expression,
|
|
983
|
+
textHash: state.textHash
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
} else {
|
|
987
|
+
const inlineSpans = chunkXJGJCF2R_cjs.sharedLexer.findInlineSolves(state.text);
|
|
988
|
+
if (inlineSpans.length > 0) {
|
|
989
|
+
for (const span of inlineSpans) {
|
|
990
|
+
if (!span.expression.trim()) continue;
|
|
991
|
+
items.push({
|
|
992
|
+
lineId: state.lineId,
|
|
993
|
+
expression: span.expression,
|
|
994
|
+
textHash: state.textHash
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
const expression = state.text.trim();
|
|
999
|
+
if (expression) {
|
|
1000
|
+
items.push({
|
|
1001
|
+
lineId: state.lineId,
|
|
1002
|
+
expression,
|
|
1003
|
+
textHash: state.textHash
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return items;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Reset internal state (e.g., after document switch).
|
|
1014
|
+
*/
|
|
1015
|
+
clear() {
|
|
1016
|
+
this.pageAccess.clear();
|
|
1017
|
+
this.accessSeq = 0;
|
|
1018
|
+
this.lastViewportStart = null;
|
|
1019
|
+
this.savedDirection = null;
|
|
1020
|
+
}
|
|
1021
|
+
// ── Private helpers ─────────────────────────────────────────────────
|
|
1022
|
+
/** Record a page access with a monotonic sequence number. */
|
|
1023
|
+
touchPage(pageNum) {
|
|
1024
|
+
this.pageAccess.set(pageNum, ++this.accessSeq);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Evict results from all non-variable-def lines in a page.
|
|
1028
|
+
* Bytecode is preserved so Tier 2 execution works on scroll-back.
|
|
1029
|
+
*/
|
|
1030
|
+
evictPageResults(pageNum, doc, docLineCount) {
|
|
1031
|
+
const range = _PageManager.pageRange(pageNum, docLineCount);
|
|
1032
|
+
for (let pos = range.startLine; pos <= range.endLine; pos++) {
|
|
1033
|
+
const state = doc.getLineAt(pos);
|
|
1034
|
+
if (state && !state.isVariableDef && state.results.length > 0) {
|
|
1035
|
+
state.results = [];
|
|
1036
|
+
state.result = null;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Evict bytecode + results from all non-variable-def lines in a page.
|
|
1042
|
+
* Variable definition bytecode is **never** evicted, it forms the
|
|
1043
|
+
* backbone of the DAG and VM checkpoints.
|
|
1044
|
+
*
|
|
1045
|
+
* Evicted lines are marked dirty so they get Tier 1 re-evaluation
|
|
1046
|
+
* when scrolled back into view.
|
|
1047
|
+
*/
|
|
1048
|
+
evictPageBytecode(pageNum, doc, docLineCount) {
|
|
1049
|
+
const range = _PageManager.pageRange(pageNum, docLineCount);
|
|
1050
|
+
for (let pos = range.startLine; pos <= range.endLine; pos++) {
|
|
1051
|
+
const state = doc.getLineAt(pos);
|
|
1052
|
+
if (!state || state.isVariableDef) continue;
|
|
1053
|
+
if (state.bytecodes.length > 0 || state.results.length > 0) {
|
|
1054
|
+
state.bytecodes = [];
|
|
1055
|
+
state.results = [];
|
|
1056
|
+
state.result = null;
|
|
1057
|
+
doc.markDirty(state.lineId);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
// src/engine/ThreeTierEvaluator.ts
|
|
1064
|
+
var EvalTier = /* @__PURE__ */ ((EvalTier2) => {
|
|
1065
|
+
EvalTier2[EvalTier2["Tier1"] = 1] = "Tier1";
|
|
1066
|
+
EvalTier2[EvalTier2["Tier2"] = 2] = "Tier2";
|
|
1067
|
+
EvalTier2[EvalTier2["Tier3"] = 3] = "Tier3";
|
|
1068
|
+
EvalTier2[EvalTier2["Skipped"] = 0] = "Skipped";
|
|
1069
|
+
return EvalTier2;
|
|
1070
|
+
})(EvalTier || {});
|
|
1071
|
+
var ThreeTierEvaluator = class {
|
|
1072
|
+
/**
|
|
1073
|
+
* @param doc The persistent document model.
|
|
1074
|
+
* @param engine The expression engine (shared VM is accessed via engine.getVM()).
|
|
1075
|
+
* @param checkpointer Optional VM state checkpointer. If provided, the evaluator
|
|
1076
|
+
* will create checkpoints after variable-definition lines and support fast VM
|
|
1077
|
+
* restoration via `restoreTo()`. If omitted, checkpointing is disabled.
|
|
1078
|
+
*/
|
|
1079
|
+
constructor(doc, engine, checkpointer) {
|
|
1080
|
+
this.compilationWorker = null;
|
|
1081
|
+
/**
|
|
1082
|
+
* Unsubscribe from sharedGlobalVariableStore, set in the constructor
|
|
1083
|
+
* called from terminateWorker(). See the subscription itself below for
|
|
1084
|
+
* why this only marks lines dirty and never re-evaluates synchronously.
|
|
1085
|
+
*/
|
|
1086
|
+
this.globalUnsubscribe = null;
|
|
1087
|
+
this.doc = doc;
|
|
1088
|
+
this.engine = engine;
|
|
1089
|
+
this.dag = engine.getDag();
|
|
1090
|
+
this.checkpointer = checkpointer ?? null;
|
|
1091
|
+
this.pageManager = new PageManager();
|
|
1092
|
+
this.engine.setDocumentModel(this.doc);
|
|
1093
|
+
this.globalUnsubscribe = chunk7ZX6B7SY_cjs.sharedGlobalVariableStore.subscribe((name) => {
|
|
1094
|
+
for (const lineNumber of this.dag.getAffectedLines(chunk7ZX6B7SY_cjs.globalDagKey(name))) {
|
|
1095
|
+
this.doc.markDirtyByLineNumber(lineNumber);
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Evaluate all lines needed to render the given viewport.
|
|
1101
|
+
*
|
|
1102
|
+
* Processes lines from 1 to `viewport.endLine` in document order.
|
|
1103
|
+
* Dirty lines in the viewport get Tier-1 full pipeline; clean cached
|
|
1104
|
+
* lines get Tier-2 bytecode execution. Lines after the viewport
|
|
1105
|
+
* get Tier-3 compile-only (with variable-def execution).
|
|
1106
|
+
*
|
|
1107
|
+
* @returns Results for all processed lines, including tier metadata.
|
|
1108
|
+
*/
|
|
1109
|
+
evaluate(viewport, signal) {
|
|
1110
|
+
this.engine.setKeystrokeSignal(signal ?? null);
|
|
1111
|
+
chunk2TZKENDH_cjs.enableValueArena();
|
|
1112
|
+
try {
|
|
1113
|
+
const lines = [];
|
|
1114
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1115
|
+
const tierCounts = { tier1: 0, tier2: 0, tier3: 0, skipped: 0 };
|
|
1116
|
+
const docEnd = this.doc.lineCount;
|
|
1117
|
+
const evalEnd = Math.min(viewport.endLine, docEnd);
|
|
1118
|
+
for (let pos = 1; pos <= evalEnd; pos++) {
|
|
1119
|
+
const state = this.doc.getLineAt(pos);
|
|
1120
|
+
if (!state) {
|
|
1121
|
+
tierCounts.skipped++;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const inViewport = pos >= viewport.startLine && pos <= viewport.endLine;
|
|
1125
|
+
const lineResult = this.evaluateSingleLine(state, pos, inViewport);
|
|
1126
|
+
lines.push(lineResult);
|
|
1127
|
+
if (lineResult.tier === 1 /* Tier1 */) tierCounts.tier1++;
|
|
1128
|
+
else if (lineResult.tier === 2 /* Tier2 */) tierCounts.tier2++;
|
|
1129
|
+
else if (lineResult.tier === 3 /* Tier3 */) tierCounts.tier3++;
|
|
1130
|
+
else tierCounts.skipped++;
|
|
1131
|
+
if (lineResult.results && inViewport) {
|
|
1132
|
+
resultMap.set(pos, lineResult.results.flat());
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
this.pageManager.maintainAfterEval(viewport, this.doc);
|
|
1136
|
+
return { lines, resultMap, tierCounts };
|
|
1137
|
+
} finally {
|
|
1138
|
+
this.engine.setKeystrokeSignal(null);
|
|
1139
|
+
chunk2TZKENDH_cjs.disableValueArena();
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Background-compile invisible dirty lines beyond the viewport (Tier 3 only).
|
|
1144
|
+
*
|
|
1145
|
+
* Compiles expressions to discover reads/writes for the dependency graph
|
|
1146
|
+
* without executing display-only expressions. Variable definitions are
|
|
1147
|
+
* executed to maintain VM state for future Tier-2 executions.
|
|
1148
|
+
*
|
|
1149
|
+
* This is intended to be called after evaluate() so visible lines are
|
|
1150
|
+
* rendered first, then background work fills in the dependency graph.
|
|
1151
|
+
*
|
|
1152
|
+
* **Phase 5.2h:** This synchronous method is retained for environments
|
|
1153
|
+
* without Worker support. Prefer `dispatchBackgroundCompiles()` which
|
|
1154
|
+
* offloads compilation to a Web Worker with Transferable bytecode.
|
|
1155
|
+
*/
|
|
1156
|
+
backgroundCompile(viewport) {
|
|
1157
|
+
const results = [];
|
|
1158
|
+
const docEnd = this.doc.lineCount;
|
|
1159
|
+
const startPos = viewport.endLine + 1;
|
|
1160
|
+
for (let pos = startPos; pos <= docEnd; pos++) {
|
|
1161
|
+
const state = this.doc.getLineAt(pos);
|
|
1162
|
+
if (!state) continue;
|
|
1163
|
+
if (!state.dirty) continue;
|
|
1164
|
+
if (state.bytecodes.length > 0 && !state.isVariableDef) continue;
|
|
1165
|
+
const lineResult = this.evaluateSingleLine(state, pos, false);
|
|
1166
|
+
results.push(lineResult);
|
|
1167
|
+
}
|
|
1168
|
+
return results;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Dispatch background compilation to a Web Worker (Phase 5.2h).
|
|
1172
|
+
*
|
|
1173
|
+
* Collects invisible dirty lines beyond the viewport that need compilation,
|
|
1174
|
+
* sends them to the compilation worker, and asynchronously stores the
|
|
1175
|
+
* transferred bytecode in the DocumentModel when the worker responds.
|
|
1176
|
+
*
|
|
1177
|
+
* This is the non-blocking alternative to `backgroundCompile()`. The worker
|
|
1178
|
+
* compiles expressions with Transferable ArrayBuffers (zero-copy postMessage),
|
|
1179
|
+
* so bytecode appears on the main thread without serialization overhead.
|
|
1180
|
+
*
|
|
1181
|
+
* Lines that already have cached bytecode (from a previous worker pass or
|
|
1182
|
+
* synchronous compile) are skipped, only truly uncompiled dirty lines are
|
|
1183
|
+
* sent to the worker.
|
|
1184
|
+
*
|
|
1185
|
+
* **Usage:** Call after `evaluate()` so visible lines render first, then
|
|
1186
|
+
* this fills the bytecode cache for future Tier-2 scrolls.
|
|
1187
|
+
*
|
|
1188
|
+
* @param viewport The current visible range. Lines beyond viewport.endLine
|
|
1189
|
+
* that are dirty and don't have bytecode are dispatched.
|
|
1190
|
+
*/
|
|
1191
|
+
dispatchBackgroundCompiles(viewport) {
|
|
1192
|
+
const items = this.collectInvisibleCompileTargets(viewport);
|
|
1193
|
+
if (items.length === 0) return;
|
|
1194
|
+
if (!this.compilationWorker) {
|
|
1195
|
+
this.compilationWorker = new CompilationWorkerManager();
|
|
1196
|
+
}
|
|
1197
|
+
this.compilationWorker.compileBatch(items).then((results) => {
|
|
1198
|
+
this.compilationWorker.storeResults(results, this.doc);
|
|
1199
|
+
}).catch((_err) => {
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Terminate the compilation worker if active, and unsubscribe from
|
|
1204
|
+
* sharedGlobalVariableStore. Call this when the evaluator is no longer
|
|
1205
|
+
* needed to clean up resources, every call site that retires a
|
|
1206
|
+
* ThreeTierEvaluator (document switch, pane destroy()) already calls
|
|
1207
|
+
* this unconditionally, so folding the global-store unsubscribe in here
|
|
1208
|
+
* needs no new call sites anywhere.
|
|
1209
|
+
*/
|
|
1210
|
+
terminateWorker() {
|
|
1211
|
+
if (this.compilationWorker) {
|
|
1212
|
+
this.compilationWorker.terminate();
|
|
1213
|
+
this.compilationWorker = null;
|
|
1214
|
+
}
|
|
1215
|
+
if (this.globalUnsubscribe) {
|
|
1216
|
+
this.globalUnsubscribe();
|
|
1217
|
+
this.globalUnsubscribe = null;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Get the DocumentModel (read-only access for decoration building).
|
|
1222
|
+
*/
|
|
1223
|
+
getDoc() {
|
|
1224
|
+
return this.doc;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Evaluate all dirty lines in the document, regardless of viewport.
|
|
1228
|
+
* Used for full re-evaluation after plugin register/unregister.
|
|
1229
|
+
*/
|
|
1230
|
+
evaluateAll(signal) {
|
|
1231
|
+
const viewport = { startLine: 1, endLine: this.doc.lineCount };
|
|
1232
|
+
const result = this.evaluate(viewport, signal);
|
|
1233
|
+
return result;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Zero-allocation viewport evaluation, the Phase 5.2e "holy grail."
|
|
1237
|
+
*
|
|
1238
|
+
* **Key insight:** When the user scrolls (viewport-only change, no edits),
|
|
1239
|
+
* we don't need to re-evaluate from line 1. Instead:
|
|
1240
|
+
*
|
|
1241
|
+
* 1. Restore the VM to just before the viewport via the nearest checkpoint.
|
|
1242
|
+
* 2. Evaluate ONLY the visible lines (Tier 2 for clean cached, Tier 1 for dirty).
|
|
1243
|
+
* 3. Lines before the viewport are completely skipped, their state lives in
|
|
1244
|
+
* the VM checkpointer's prototypal chain.
|
|
1245
|
+
*
|
|
1246
|
+
* **Correctness guard:** If any variable-definition line before the viewport
|
|
1247
|
+
* is dirty (e.g., the user edited a variable def that hasn't been
|
|
1248
|
+
* re-evaluated yet), we clear stale checkpoints and fall back to `evaluate()`
|
|
1249
|
+
* which processes from line 1 and rebuilds fresh checkpoints. This
|
|
1250
|
+
* guarantees that stale checkpoints are never used as restoration targets.
|
|
1251
|
+
* Only variable-def lines matter here, `VMCheckpointer.snapshot()` only
|
|
1252
|
+
* records state for lines that write a variable, so a dirty plain-expression
|
|
1253
|
+
* line before the viewport has no checkpoint to invalidate (see
|
|
1254
|
+
* `DocumentModel.hasAnyDirtyVariableDefLineBefore()`).
|
|
1255
|
+
*
|
|
1256
|
+
* **Performance:** O(visible lines) instead of O(document length). Target:
|
|
1257
|
+
* < 1ms for a typical ~30-line viewport, independent of document size.
|
|
1258
|
+
*
|
|
1259
|
+
* @param viewport The visible line range.
|
|
1260
|
+
* @returns Results for visible lines only. Lines before the viewport are
|
|
1261
|
+
* not included in `lines[]` or `resultMap`.
|
|
1262
|
+
*/
|
|
1263
|
+
setViewport(viewport, signal) {
|
|
1264
|
+
this.engine.setKeystrokeSignal(signal ?? null);
|
|
1265
|
+
if (viewport.startLine > 1 && this.hasDirtyLinesBefore(viewport.startLine)) {
|
|
1266
|
+
this.checkpointer?.clear();
|
|
1267
|
+
return this.evaluate(viewport, signal);
|
|
1268
|
+
}
|
|
1269
|
+
this.pageManager.maintainAfterEval(viewport, this.doc);
|
|
1270
|
+
this.preloadNextPages(viewport);
|
|
1271
|
+
this.restoreTo(viewport.startLine - 1);
|
|
1272
|
+
chunk2TZKENDH_cjs.enableValueArena();
|
|
1273
|
+
try {
|
|
1274
|
+
const result = this.collectEvalResults(viewport.startLine, viewport.endLine);
|
|
1275
|
+
return result;
|
|
1276
|
+
} finally {
|
|
1277
|
+
this.engine.setKeystrokeSignal(null);
|
|
1278
|
+
chunk2TZKENDH_cjs.disableValueArena();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Apply incremental line-level changes to the document model.
|
|
1283
|
+
*
|
|
1284
|
+
* **Phase 5.2f:** Replaces the O(N) `setDocument()` + full re-evaluation
|
|
1285
|
+
* with O(changed) incremental updates. Key benefits:
|
|
1286
|
+
*
|
|
1287
|
+
* 1. Unchanged lines retain their persistent lineIds → bytecode survives
|
|
1288
|
+
* 2. Only changed + DAG-downstream lines are marked dirty → Tier 1 re-evaluation
|
|
1289
|
+
* 3. Clean lines in viewport use Tier 2 (cached bytecode execution)
|
|
1290
|
+
* 4. Clean lines outside viewport are skipped entirely
|
|
1291
|
+
*
|
|
1292
|
+
* The DAG is fully cleared after propagation: shifted lines would have
|
|
1293
|
+
* stale entries keyed by old line numbers, so the DAG is rebuilt from
|
|
1294
|
+
* scratch during the subsequent `evaluate()` call.
|
|
1295
|
+
*
|
|
1296
|
+
* **Caller should follow up with `evaluate(viewport)`** to re-evaluate
|
|
1297
|
+
* dirty lines from line 1 and rebuild the DAG + checkpoints.
|
|
1298
|
+
*
|
|
1299
|
+
* @param changes Line-level changes to apply. Must be non-overlapping.
|
|
1300
|
+
* @returns Metadata about the applied changes.
|
|
1301
|
+
*/
|
|
1302
|
+
applyTransaction(changes) {
|
|
1303
|
+
const allWrites = /* @__PURE__ */ new Set();
|
|
1304
|
+
for (const change of changes) {
|
|
1305
|
+
for (let i = 0; i < change.deleteCount; i++) {
|
|
1306
|
+
const lineNum = change.startLine + i;
|
|
1307
|
+
const writes = this.dag.getWrites(lineNum);
|
|
1308
|
+
for (const w of writes) {
|
|
1309
|
+
allWrites.add(w);
|
|
1310
|
+
}
|
|
1311
|
+
this.dag.removeLine(lineNum);
|
|
1312
|
+
this.engine.getLineCache().removeAllForLine(lineNum);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const downstreamLineIds = /* @__PURE__ */ new Set();
|
|
1316
|
+
for (const writeVar of allWrites) {
|
|
1317
|
+
const affected = this.dag.getAffectedLines(writeVar);
|
|
1318
|
+
for (const lineNum of affected) {
|
|
1319
|
+
const state = this.doc.getLineAt(lineNum);
|
|
1320
|
+
if (state) {
|
|
1321
|
+
downstreamLineIds.add(state.lineId);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const result = this.doc.applyChanges(changes);
|
|
1326
|
+
this.checkpointer?.clear();
|
|
1327
|
+
for (const lineId of downstreamLineIds) {
|
|
1328
|
+
this.doc.markDirty(lineId);
|
|
1329
|
+
}
|
|
1330
|
+
this.dag.clear();
|
|
1331
|
+
return {
|
|
1332
|
+
inserted: result.inserted,
|
|
1333
|
+
removed: result.removed
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
// ── Private helpers ─────────────────────────────────────────────────
|
|
1337
|
+
/**
|
|
1338
|
+
* Collect evaluation results for a contiguous range of lines.
|
|
1339
|
+
*
|
|
1340
|
+
* Used by both `evaluate()` (startLine=1) and `setViewport()` (any start).
|
|
1341
|
+
* All lines in the range are treated as in-viewport (visible), callers that
|
|
1342
|
+
* need the invisible/dirty → Tier 3 handling should use `evaluate()` instead.
|
|
1343
|
+
*
|
|
1344
|
+
* @param startLine First line to evaluate (1-based, inclusive).
|
|
1345
|
+
* @param endLine Last line to evaluate (1-based, inclusive). Clamped to docEnd.
|
|
1346
|
+
*/
|
|
1347
|
+
collectEvalResults(startLine, endLine) {
|
|
1348
|
+
const lines = [];
|
|
1349
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1350
|
+
const tierCounts = { tier1: 0, tier2: 0, tier3: 0, skipped: 0 };
|
|
1351
|
+
const docEnd = this.doc.lineCount;
|
|
1352
|
+
const evalEnd = Math.min(endLine, docEnd);
|
|
1353
|
+
for (let pos = startLine; pos <= evalEnd; pos++) {
|
|
1354
|
+
const state = this.doc.getLineAt(pos);
|
|
1355
|
+
if (!state) continue;
|
|
1356
|
+
const lineResult = this.evaluateSingleLine(state, pos, true);
|
|
1357
|
+
lines.push(lineResult);
|
|
1358
|
+
if (lineResult.tier === 1 /* Tier1 */) tierCounts.tier1++;
|
|
1359
|
+
else if (lineResult.tier === 2 /* Tier2 */) tierCounts.tier2++;
|
|
1360
|
+
else if (lineResult.tier === 3 /* Tier3 */) tierCounts.tier3++;
|
|
1361
|
+
else tierCounts.skipped++;
|
|
1362
|
+
if (lineResult.results) {
|
|
1363
|
+
resultMap.set(pos, lineResult.results.flat());
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return { lines, resultMap, tierCounts };
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Check whether any **variable-definition** line before `position`
|
|
1370
|
+
* (1-based, exclusive) is dirty.
|
|
1371
|
+
*
|
|
1372
|
+
* Used by `setViewport()` to decide whether to fall back to `evaluate()`:
|
|
1373
|
+
* if a variable-def before the viewport is dirty, the checkpoint state
|
|
1374
|
+
* `restoreTo()` would use may be stale and we need to reprocess from
|
|
1375
|
+
* line 1 to rebuild checkpoints correctly.
|
|
1376
|
+
*
|
|
1377
|
+
* Deliberately narrower than `DocumentModel.hasAnyDirtyLineBefore()`:
|
|
1378
|
+
* checkpoints only snapshot variable-def lines (see VMCheckpointer), so a
|
|
1379
|
+
* dirty plain-expression line before the viewport can't have invalidated
|
|
1380
|
+
* one, there's nothing checkpointed for it to invalidate. Using the
|
|
1381
|
+
* broader check here previously caused a real perf bug: `PageManager`'s
|
|
1382
|
+
* cold-page eviction marks evicted non-variable-def lines dirty, so
|
|
1383
|
+
* scrolling far into a large, variable-def-free document would trip this
|
|
1384
|
+
* guard, fall back to `evaluate()`, which recompiles those lines via
|
|
1385
|
+
* Tier 3 (never clearing their dirty flag by design), causing the very
|
|
1386
|
+
* next `maintainAfterEval()` to re-evict and re-dirty them, a
|
|
1387
|
+
* self-sustaining loop that pinned every subsequent `setViewport()` call
|
|
1388
|
+
* to the cost of a full re-evaluation instead of O(visible lines).
|
|
1389
|
+
*
|
|
1390
|
+
* Delegates to DocumentModel.hasAnyDirtyVariableDefLineBefore(), which
|
|
1391
|
+
* tracks dirty lineIds incrementally instead of scanning every line up to
|
|
1392
|
+
* `position` on every call. This used to be a real per-scroll cost
|
|
1393
|
+
* (benchmarked at ~10ms scrolled near the bottom of a 20k-line document)
|
|
1394
|
+
* since it fired on every viewport change, not just edits.
|
|
1395
|
+
*/
|
|
1396
|
+
hasDirtyLinesBefore(position) {
|
|
1397
|
+
return this.doc.hasAnyDirtyVariableDefLineBefore(position);
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Evaluate a single line using the appropriate tier.
|
|
1401
|
+
*
|
|
1402
|
+
* Tier assignment logic:
|
|
1403
|
+
* - Empty/markdown-only lines → skipped
|
|
1404
|
+
* - Dirty + in-viewport → Tier 1 (full pipeline)
|
|
1405
|
+
* - Dirty + not in viewport → Tier 3 (compile-only, execute variable defs)
|
|
1406
|
+
* - Clean + has bytecode + in viewport → Tier 2 (execute from cache)
|
|
1407
|
+
* - Clean + no bytecode → skipped (non-evaluable)
|
|
1408
|
+
*/
|
|
1409
|
+
evaluateSingleLine(state, lineNumber, inViewport) {
|
|
1410
|
+
const baseResult = {
|
|
1411
|
+
lineId: state.lineId,
|
|
1412
|
+
lineNumber
|
|
1413
|
+
};
|
|
1414
|
+
if (state.isEmpty || chunkMG6Q3DUO_cjs.isEmptyLine(state.text)) {
|
|
1415
|
+
state.isEmpty = true;
|
|
1416
|
+
this.doc.markClean(state.lineId);
|
|
1417
|
+
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
1418
|
+
}
|
|
1419
|
+
const { expressions, inlineSolveCount } = this.extractExpressions(state);
|
|
1420
|
+
if (expressions.length === 0) {
|
|
1421
|
+
state.isEmpty = true;
|
|
1422
|
+
this.doc.markClean(state.lineId);
|
|
1423
|
+
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
1424
|
+
}
|
|
1425
|
+
if (state.dirty) {
|
|
1426
|
+
if (inViewport) {
|
|
1427
|
+
return this.evaluateTier1(state, lineNumber, expressions, inlineSolveCount, baseResult);
|
|
1428
|
+
} else {
|
|
1429
|
+
if (state.bytecodes.length > 0 && state.bytecodes.length === expressions.length && !state.isVariableDef) {
|
|
1430
|
+
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
1431
|
+
}
|
|
1432
|
+
return this.evaluateTier3(state, lineNumber, expressions, inlineSolveCount, baseResult);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (inViewport && state.bytecodes.length > 0) {
|
|
1436
|
+
return this.evaluateTier2(state, lineNumber, baseResult);
|
|
1437
|
+
}
|
|
1438
|
+
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Tier 1: Full pipeline, lex, parse, compile, execute.
|
|
1442
|
+
* Uses the engine's existing evaluateLine() which handles all pipeline
|
|
1443
|
+
* stages including DAG updates and LineCache population.
|
|
1444
|
+
*
|
|
1445
|
+
* Supports multiple expressions per line (inline solves). Evaluates each
|
|
1446
|
+
* expression left-to-right through the engine so variable definitions in
|
|
1447
|
+
* earlier solves update the VM state before later solves are evaluated.
|
|
1448
|
+
* Reads/writes are aggregated across all expressions for the DAG.
|
|
1449
|
+
*/
|
|
1450
|
+
evaluateTier1(state, lineNumber, expressions, inlineSolveCount, baseResult) {
|
|
1451
|
+
const allResults = [];
|
|
1452
|
+
const allBytecodes = [];
|
|
1453
|
+
const allReads = /* @__PURE__ */ new Set();
|
|
1454
|
+
const allWrites = /* @__PURE__ */ new Set();
|
|
1455
|
+
let hasVariableDef = false;
|
|
1456
|
+
let lastValue = null;
|
|
1457
|
+
let firstError = null;
|
|
1458
|
+
let anyFailed = false;
|
|
1459
|
+
let anyPending = false;
|
|
1460
|
+
for (const expression of expressions) {
|
|
1461
|
+
if (!expression.trim()) continue;
|
|
1462
|
+
let value = null;
|
|
1463
|
+
let entry;
|
|
1464
|
+
try {
|
|
1465
|
+
const evaluation = this.engine.evaluateLineDetailed(lineNumber, expression);
|
|
1466
|
+
value = evaluation.values;
|
|
1467
|
+
lastValue = value[0];
|
|
1468
|
+
entry = this.engine.getLineCache().get(lineNumber, expression);
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1471
|
+
if (!firstError) firstError = errorMessage;
|
|
1472
|
+
anyFailed = true;
|
|
1473
|
+
value = null;
|
|
1474
|
+
}
|
|
1475
|
+
if (value && value.some((v) => v.type === 12 /* Pending */)) {
|
|
1476
|
+
anyPending = true;
|
|
1477
|
+
}
|
|
1478
|
+
if (value) {
|
|
1479
|
+
allResults.push(value);
|
|
1480
|
+
} else {
|
|
1481
|
+
allResults.push([chunk2TZKENDH_cjs.errorValue("eval_failed", firstError ?? "unknown error")]);
|
|
1482
|
+
}
|
|
1483
|
+
if (entry) {
|
|
1484
|
+
allBytecodes.push(entry.bytecode);
|
|
1485
|
+
for (const r of entry.readVariables) allReads.add(r);
|
|
1486
|
+
if (entry.writeVariable) {
|
|
1487
|
+
allWrites.add(entry.writeVariable);
|
|
1488
|
+
hasVariableDef = true;
|
|
1489
|
+
}
|
|
1490
|
+
} else {
|
|
1491
|
+
try {
|
|
1492
|
+
const { program, reads: reads2, writes: writes2 } = this.engine.compileExpression(expression);
|
|
1493
|
+
allBytecodes.push(program);
|
|
1494
|
+
for (const r of reads2) allReads.add(r);
|
|
1495
|
+
for (const w of writes2) allWrites.add(w);
|
|
1496
|
+
if (writes2.length > 0) hasVariableDef = true;
|
|
1497
|
+
} catch (compileErr) {
|
|
1498
|
+
allBytecodes.push({ opcodes: new Uint8Array(0), numbers: new Float64Array(0), strings: [], hasAsync: false });
|
|
1499
|
+
if (compileErr instanceof chunkB4HBEFTB_cjs.EngineError && compileErr.context) {
|
|
1500
|
+
const errReads = compileErr.context.reads;
|
|
1501
|
+
const errWrites = compileErr.context.writes;
|
|
1502
|
+
if (Array.isArray(errReads)) for (const r of errReads) allReads.add(r);
|
|
1503
|
+
if (Array.isArray(errWrites)) for (const w of errWrites) allWrites.add(w);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
const reads = [...allReads];
|
|
1509
|
+
const writes = [...allWrites];
|
|
1510
|
+
if (anyFailed || anyPending) {
|
|
1511
|
+
this.doc.updateLineCompiled(
|
|
1512
|
+
state.lineId,
|
|
1513
|
+
expressions,
|
|
1514
|
+
allBytecodes,
|
|
1515
|
+
reads,
|
|
1516
|
+
writes,
|
|
1517
|
+
hasVariableDef,
|
|
1518
|
+
inlineSolveCount
|
|
1519
|
+
);
|
|
1520
|
+
state.results = allResults;
|
|
1521
|
+
state.result = allResults[0]?.[0] ?? null;
|
|
1522
|
+
state.inlineSolveCount = inlineSolveCount;
|
|
1523
|
+
state.expressions = expressions;
|
|
1524
|
+
} else {
|
|
1525
|
+
this.doc.updateLineResult(
|
|
1526
|
+
state.lineId,
|
|
1527
|
+
allResults,
|
|
1528
|
+
allBytecodes,
|
|
1529
|
+
expressions,
|
|
1530
|
+
reads,
|
|
1531
|
+
writes,
|
|
1532
|
+
hasVariableDef,
|
|
1533
|
+
inlineSolveCount
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
this.dag.registerLine(lineNumber, reads, writes);
|
|
1537
|
+
if (this.checkpointer && writes.length > 0 && !anyFailed && !anyPending) {
|
|
1538
|
+
this.checkpointer.snapshot(lineNumber, state.lineId, writes);
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
...baseResult,
|
|
1542
|
+
tier: 1 /* Tier1 */,
|
|
1543
|
+
result: lastValue,
|
|
1544
|
+
results: allResults,
|
|
1545
|
+
error: firstError
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Tier 2: Execute from cached bytecode only.
|
|
1550
|
+
* Skips lexing, parsing, and compiling, runs the pre-compiled bytecode
|
|
1551
|
+
* against the engine's shared VM. Supports multiple bytecodes per line
|
|
1552
|
+
* (inline solves), each is executed left-to-right so variable definitions
|
|
1553
|
+
* in earlier bytecodes update the VM before later ones run.
|
|
1554
|
+
* Assumes the VM already has correct variable state from preceding
|
|
1555
|
+
* Tier-1 evaluations.
|
|
1556
|
+
*/
|
|
1557
|
+
evaluateTier2(state, lineNumber, baseResult) {
|
|
1558
|
+
if (state.bytecodes.length === 0) {
|
|
1559
|
+
return { ...baseResult, tier: 0 /* Skipped */, result: null, error: null };
|
|
1560
|
+
}
|
|
1561
|
+
const results = [];
|
|
1562
|
+
let lastValue = null;
|
|
1563
|
+
let firstError = null;
|
|
1564
|
+
let anyFailed = false;
|
|
1565
|
+
for (const bytecode of state.bytecodes) {
|
|
1566
|
+
if (bytecode.opcodes.length === 0) continue;
|
|
1567
|
+
try {
|
|
1568
|
+
const value = this.engine.executeCached(bytecode, lineNumber);
|
|
1569
|
+
lastValue = value;
|
|
1570
|
+
results.push([value]);
|
|
1571
|
+
} catch (e) {
|
|
1572
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1573
|
+
if (!firstError) firstError = errorMessage;
|
|
1574
|
+
anyFailed = true;
|
|
1575
|
+
results.push([chunk2TZKENDH_cjs.errorValue("exec_failed", errorMessage)]);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
this.dag.registerLine(lineNumber, state.reads, state.writes);
|
|
1579
|
+
state.results = results;
|
|
1580
|
+
state.result = results[0]?.[0] ?? null;
|
|
1581
|
+
if (anyFailed) {
|
|
1582
|
+
this.doc.markDirty(state.lineId);
|
|
1583
|
+
}
|
|
1584
|
+
return { ...baseResult, tier: 2 /* Tier2 */, result: lastValue, results, error: firstError };
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Tier 3: Compile-only for invisible lines.
|
|
1588
|
+
* Lex → Parse → Compile to discover reads/writes for the dependency graph.
|
|
1589
|
+
* Executes the bytecode ONLY if the line defines a variable (isVariableDef
|
|
1590
|
+
* or writes.length > 0), because variable assignments affect VM state
|
|
1591
|
+
* that other lines depend on. Pure expression lines are compiled but NOT
|
|
1592
|
+
* executed, saving CPU for large documents.
|
|
1593
|
+
*
|
|
1594
|
+
* Supports multiple expressions per line (inline solves). Each is compiled
|
|
1595
|
+
* separately; variable-def expressions are also executed.
|
|
1596
|
+
*/
|
|
1597
|
+
evaluateTier3(state, lineNumber, expressions, inlineSolveCount, baseResult) {
|
|
1598
|
+
const allBytecodes = [];
|
|
1599
|
+
const allReads = /* @__PURE__ */ new Set();
|
|
1600
|
+
const allWrites = /* @__PURE__ */ new Set();
|
|
1601
|
+
let hasVariableDef = false;
|
|
1602
|
+
let lastResult = null;
|
|
1603
|
+
let firstError = null;
|
|
1604
|
+
let anyFailed = false;
|
|
1605
|
+
let anyPending = false;
|
|
1606
|
+
for (const expression of expressions) {
|
|
1607
|
+
if (!expression.trim()) continue;
|
|
1608
|
+
try {
|
|
1609
|
+
const { program, reads: reads2, writes: writes2 } = this.engine.compileExpression(expression);
|
|
1610
|
+
allBytecodes.push(program);
|
|
1611
|
+
for (const r of reads2) allReads.add(r);
|
|
1612
|
+
for (const w of writes2) allWrites.add(w);
|
|
1613
|
+
if (writes2.length > 0) hasVariableDef = true;
|
|
1614
|
+
if (writes2.length > 0 && program.opcodes.length > 0) {
|
|
1615
|
+
lastResult = this.engine.executeCached(program, lineNumber);
|
|
1616
|
+
if (lastResult && lastResult.type === 12 /* Pending */) {
|
|
1617
|
+
anyPending = true;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
} catch (e) {
|
|
1621
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1622
|
+
if (!firstError) firstError = errorMessage;
|
|
1623
|
+
anyFailed = true;
|
|
1624
|
+
allBytecodes.push({ opcodes: new Uint8Array(0), numbers: new Float64Array(0), strings: [], hasAsync: false });
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const reads = [...allReads];
|
|
1628
|
+
const writes = [...allWrites];
|
|
1629
|
+
this.doc.updateLineCompiled(
|
|
1630
|
+
state.lineId,
|
|
1631
|
+
expressions,
|
|
1632
|
+
allBytecodes,
|
|
1633
|
+
reads,
|
|
1634
|
+
writes,
|
|
1635
|
+
hasVariableDef,
|
|
1636
|
+
inlineSolveCount
|
|
1637
|
+
);
|
|
1638
|
+
this.dag.registerLine(lineNumber, reads, writes);
|
|
1639
|
+
if (hasVariableDef && lastResult && !anyFailed && !anyPending) {
|
|
1640
|
+
state.results = [[lastResult]];
|
|
1641
|
+
state.result = lastResult;
|
|
1642
|
+
this.doc.markClean(state.lineId);
|
|
1643
|
+
if (this.checkpointer) {
|
|
1644
|
+
this.checkpointer.snapshot(lineNumber, state.lineId, writes);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return { ...baseResult, tier: 3 /* Tier3 */, result: lastResult, results: hasVariableDef && lastResult && !anyFailed && !anyPending ? [[lastResult]] : void 0, error: firstError };
|
|
1648
|
+
}
|
|
1649
|
+
// ── Public checkpoint API (used by Phase 5.2e setViewport) ──────
|
|
1650
|
+
/**
|
|
1651
|
+
* Restore the VM to the state at or just after the given line number.
|
|
1652
|
+
*
|
|
1653
|
+
* Finds the nearest checkpoint at or before `lineNumber` and replays
|
|
1654
|
+
* all variable definitions from the checkpoint chain into the VM.
|
|
1655
|
+
* After calling this, the VM is ready to evaluate lines starting at
|
|
1656
|
+
* `lineNumber + 1` without re-evaluating all preceding lines.
|
|
1657
|
+
*
|
|
1658
|
+
* **Usage:** Phase 5.2e's `setViewport()` calls `restoreTo(viewport.startLine - 1)`
|
|
1659
|
+
* before evaluating only the newly visible lines. This is the key to
|
|
1660
|
+
* O(visible lines) scrolling.
|
|
1661
|
+
*
|
|
1662
|
+
* @param lineNumber The line number to restore to. Variables defined
|
|
1663
|
+
* at lines ≤ this number will be available in the VM.
|
|
1664
|
+
*/
|
|
1665
|
+
restoreTo(lineNumber) {
|
|
1666
|
+
if (this.checkpointer) {
|
|
1667
|
+
this.checkpointer.restoreTo(lineNumber);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Get the VM checkpointer, or null if checkpointing is disabled.
|
|
1672
|
+
*/
|
|
1673
|
+
getCheckpointer() {
|
|
1674
|
+
return this.checkpointer;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Get the PageManager (Phase 5.2g).
|
|
1678
|
+
* Exposed for testing.
|
|
1679
|
+
*/
|
|
1680
|
+
getPageManager() {
|
|
1681
|
+
return this.pageManager;
|
|
1682
|
+
}
|
|
1683
|
+
// ── Phase 5.2g: Directional preloading ──────────────────────────
|
|
1684
|
+
/**
|
|
1685
|
+
* Preload the next 1–2 pages in the current scroll direction.
|
|
1686
|
+
*
|
|
1687
|
+
* Called during `setViewport()` (scroll-only path). Collects dirty
|
|
1688
|
+
* uncompiled lines in pages just beyond the viewport and dispatches
|
|
1689
|
+
* them to the background compilation worker so bytecode is ready
|
|
1690
|
+
* before the user scrolls those lines into view.
|
|
1691
|
+
*/
|
|
1692
|
+
preloadNextPages(viewport) {
|
|
1693
|
+
const targets = this.pageManager.getPreloadTargets(viewport, this.doc);
|
|
1694
|
+
if (targets.length === 0) return;
|
|
1695
|
+
if (!this.compilationWorker) {
|
|
1696
|
+
this.compilationWorker = new CompilationWorkerManager();
|
|
1697
|
+
}
|
|
1698
|
+
this.compilationWorker.compileBatch(targets).then((results) => {
|
|
1699
|
+
this.compilationWorker.storeResults(results, this.doc);
|
|
1700
|
+
}).catch((_err) => {
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Collect invisible dirty lines that need background compilation.
|
|
1705
|
+
*
|
|
1706
|
+
* Iterates lines beyond `viewport.endLine`, filtering for:
|
|
1707
|
+
* - Dirty lines (need re-compilation)
|
|
1708
|
+
* - Non-empty, non-markdown lines
|
|
1709
|
+
* - No existing bytecode (skip already-compiled Tier 3 lines)
|
|
1710
|
+
*
|
|
1711
|
+
* Returns CompileRequestItem[] suitable for CompilationWorkerManager.
|
|
1712
|
+
*/
|
|
1713
|
+
collectInvisibleCompileTargets(viewport) {
|
|
1714
|
+
const items = [];
|
|
1715
|
+
const docEnd = this.doc.lineCount;
|
|
1716
|
+
const startPos = viewport.endLine + 1;
|
|
1717
|
+
for (let pos = startPos; pos <= docEnd; pos++) {
|
|
1718
|
+
const state = this.doc.getLineAt(pos);
|
|
1719
|
+
if (!state) continue;
|
|
1720
|
+
if (!state.dirty) continue;
|
|
1721
|
+
if (state.bytecodes.length > 0 && !state.isVariableDef) continue;
|
|
1722
|
+
if (state.isEmpty || chunkMG6Q3DUO_cjs.isEmptyLine(state.text)) continue;
|
|
1723
|
+
const { expressions } = this.extractExpressions(state);
|
|
1724
|
+
if (expressions.length === 0) continue;
|
|
1725
|
+
for (const expression of expressions) {
|
|
1726
|
+
if (!expression.trim()) continue;
|
|
1727
|
+
items.push({
|
|
1728
|
+
lineId: state.lineId,
|
|
1729
|
+
expression,
|
|
1730
|
+
textHash: state.textHash
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return items;
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Extract all evaluable expressions from a LineState.
|
|
1738
|
+
*
|
|
1739
|
+
* For full-line expressions: returns `{ expressions: [trimmedText], inlineSolveCount: 0 }`.
|
|
1740
|
+
* For inline solve lines: returns `{ expressions: [...allSolves], inlineSolveCount: N }`.
|
|
1741
|
+
* For pre-extracted (cached) expressions: returns the cached array.
|
|
1742
|
+
*
|
|
1743
|
+
* Inline solves are extracted left-to-right via the sharedLexer, so variable
|
|
1744
|
+
* definitions in earlier solves (e.g., `s\`x = 5\` more text s\`x + 1\``)
|
|
1745
|
+
* correctly update the VM state before later solves are evaluated.
|
|
1746
|
+
*/
|
|
1747
|
+
extractExpressions(state) {
|
|
1748
|
+
if (state.expressions.length > 0) {
|
|
1749
|
+
return { expressions: state.expressions, inlineSolveCount: state.inlineSolveCount };
|
|
1750
|
+
}
|
|
1751
|
+
const trimmed = state.text.trim();
|
|
1752
|
+
if (trimmed.length === 0) return { expressions: [], inlineSolveCount: 0 };
|
|
1753
|
+
const inlineSpans = chunkXJGJCF2R_cjs.sharedLexer.findInlineSolves(state.text);
|
|
1754
|
+
if (inlineSpans.length > 0) {
|
|
1755
|
+
return {
|
|
1756
|
+
expressions: inlineSpans.map((s) => s.expression),
|
|
1757
|
+
inlineSolveCount: inlineSpans.length
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
return { expressions: [trimmed], inlineSolveCount: 0 };
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
exports.DocumentModel = DocumentModel;
|
|
1765
|
+
exports.EvalTier = EvalTier;
|
|
1766
|
+
exports.ThreeTierEvaluator = ThreeTierEvaluator;
|
|
1767
|
+
//# sourceMappingURL=chunk-SNHVVOJK.cjs.map
|
|
1768
|
+
//# sourceMappingURL=chunk-SNHVVOJK.cjs.map
|