@holoscript/core 7.0.0 → 8.0.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/README.md +2 -2
- package/bin/holoscript.cjs +3 -0
- package/dist/GLTFPipeline-276GE64O.js +16 -0
- package/dist/GLTFPipeline-M7WSSJED.cjs +37 -0
- package/dist/HoloScriptPlusParser-7FBIFUFC.js +693 -0
- package/dist/HoloScriptPlusParser-A2W7OCN2.cjs +698 -0
- package/dist/HoloScriptPlusParser-F2Z56FJR.js +10 -0
- package/dist/HoloScriptPlusParser-WKHRIH6T.cjs +27 -0
- package/dist/HoloScriptRuntime-3URVTKPJ.js +10 -0
- package/dist/HoloScriptRuntime-SXEAEU2N.cjs +19 -0
- package/dist/USDZExporter-3FWJ25JI.cjs +1037 -0
- package/dist/USDZExporter-D2HQLQYK.js +1035 -0
- package/dist/agents/index.cjs +19 -0
- package/dist/agents/index.d.ts +100 -0
- package/dist/agents/index.js +17 -0
- package/dist/chunk-23J755AR.js +1046 -0
- package/dist/chunk-253GBQZY.cjs +627 -0
- package/dist/chunk-25IQCHTO.cjs +781 -0
- package/dist/chunk-2BSMSUCE.js +1941 -0
- package/dist/chunk-2M2DPHKB.js +762 -0
- package/dist/chunk-2YSNA2F2.cjs +1274 -0
- package/dist/chunk-3AO425MZ.cjs +886 -0
- package/dist/chunk-3FOKASKO.cjs +348 -0
- package/dist/chunk-3HYYMEXI.cjs +56578 -0
- package/dist/chunk-3NFFOWGG.js +4287 -0
- package/dist/chunk-3WZEJHUQ.cjs +116 -0
- package/dist/chunk-4C57PR42.js +468 -0
- package/dist/chunk-4N4SXXWF.js +769 -0
- package/dist/chunk-4PDEPEQW.cjs +41 -0
- package/dist/chunk-4R6SBXDB.js +45 -0
- package/dist/chunk-4V2E7U7Q.cjs +2015 -0
- package/dist/chunk-5FSKKMR7.cjs +2092 -0
- package/dist/chunk-5GLCTO44.js +161 -0
- package/dist/chunk-5JEU52DW.js +382 -0
- package/dist/chunk-5S7RKFGR.cjs +446 -0
- package/dist/chunk-5WUNN6ZN.cjs +428 -0
- package/dist/chunk-62FBK3B7.js +70 -0
- package/dist/chunk-6CTRH7HT.cjs +1985 -0
- package/dist/chunk-6DPSFNJB.cjs +847 -0
- package/dist/chunk-6EX3I5P4.cjs +130 -0
- package/dist/chunk-73FELZVJ.cjs +611 -0
- package/dist/chunk-7CUEMLDZ.cjs +6445 -0
- package/dist/chunk-7HIASF7S.cjs +814 -0
- package/dist/chunk-A6KINLZJ.js +236 -0
- package/dist/chunk-AIA52I7T.cjs +543 -0
- package/dist/chunk-ANXZFVY2.js +113 -0
- package/dist/chunk-AYBXB26F.js +2176 -0
- package/dist/chunk-B4TFTDG4.js +449 -0
- package/dist/chunk-BE4UJCQT.js +623 -0
- package/dist/chunk-BRQJKJGT.cjs +20 -0
- package/dist/chunk-C2A5DUMF.cjs +689 -0
- package/dist/chunk-C37BHB7N.cjs +636 -0
- package/dist/chunk-CBJY23VL.js +126 -0
- package/dist/chunk-CIWWXYHZ.cjs +50 -0
- package/dist/chunk-CXFDRBQU.cjs +672 -0
- package/dist/chunk-D54CNIAJ.cjs +413 -0
- package/dist/chunk-DAJXW4KX.js +725 -0
- package/dist/chunk-DTGBOSEM.cjs +319 -0
- package/dist/chunk-E7JP3BS3.cjs +61 -0
- package/dist/chunk-EKBKLK7P.js +1169 -0
- package/dist/chunk-EKYL4PE3.cjs +404 -0
- package/dist/chunk-EUDQTK7N.cjs +4310 -0
- package/dist/chunk-EXKSFGSX.cjs +274 -0
- package/dist/chunk-EXUP2BUM.cjs +1173 -0
- package/dist/chunk-F43YAA34.js +623 -0
- package/dist/chunk-FDSGHPAK.js +130 -0
- package/dist/chunk-FEWUHQNM.cjs +144 -0
- package/dist/chunk-FGGJQRLZ.js +270 -0
- package/dist/chunk-FHP6LXJS.js +415 -0
- package/dist/chunk-FJPADH77.cjs +666 -0
- package/dist/chunk-FYSPZT4V.cjs +169 -0
- package/dist/chunk-GIOYPNTH.js +5575 -0
- package/dist/chunk-GJNCFRJU.cjs +125 -0
- package/dist/chunk-GMP623FG.cjs +1508 -0
- package/dist/chunk-GQHQOPXZ.cjs +5580 -0
- package/dist/chunk-GQT4TYKY.cjs +4381 -0
- package/dist/chunk-GTU2Z2WW.cjs +262 -0
- package/dist/chunk-GYZE55MO.js +2007 -0
- package/dist/chunk-GZG4KA4E.js +529 -0
- package/dist/chunk-H652NHX5.js +600 -0
- package/dist/chunk-HCCQLAA4.cjs +74 -0
- package/dist/chunk-HINXWAOD.cjs +1049 -0
- package/dist/chunk-HYNTS5CP.js +422 -0
- package/dist/chunk-HZSPDCXV.cjs +457 -0
- package/dist/chunk-HZTDET3N.js +1399 -0
- package/dist/chunk-I6XZ5DZF.cjs +488 -0
- package/dist/chunk-ILRBGFVD.cjs +72 -0
- package/dist/chunk-IPWSXCCN.js +1183 -0
- package/dist/chunk-IUTY5WQ2.cjs +424 -0
- package/dist/chunk-IVSZDECV.js +882 -0
- package/dist/chunk-IVY74O6C.js +247 -0
- package/dist/chunk-IZM5VQZD.js +714 -0
- package/dist/chunk-J32ZJZJH.js +554 -0
- package/dist/chunk-J6M4FPK5.js +421 -0
- package/dist/chunk-JC5YBPSF.cjs +984 -0
- package/dist/chunk-JCLBKPMX.js +3813 -0
- package/dist/chunk-JD7CIZ33.js +1949 -0
- package/dist/chunk-JIYN45TV.cjs +2525 -0
- package/dist/chunk-JJBE3TIH.cjs +1189 -0
- package/dist/chunk-JX7YL6ZE.js +2252 -0
- package/dist/chunk-K4VVR7CN.cjs +2433 -0
- package/dist/chunk-KEFNV2WM.cjs +2257 -0
- package/dist/chunk-KH4CNH66.cjs +424 -0
- package/dist/chunk-KJLB4OPV.js +5731 -0
- package/dist/chunk-KKFZ3B6J.cjs +557 -0
- package/dist/chunk-KKP7JKN4.cjs +6002 -0
- package/dist/chunk-KPJL34Y7.cjs +186 -0
- package/dist/chunk-KQCN6AKJ.cjs +3042 -0
- package/dist/chunk-KVRSTFNH.cjs +72 -0
- package/dist/chunk-LICPB5HZ.cjs +1409 -0
- package/dist/chunk-LM2VU56C.cjs +37 -0
- package/dist/chunk-M56JSP4A.js +400 -0
- package/dist/chunk-MBOOR7X4.js +18 -0
- package/dist/chunk-MGE5Z4GS.js +608 -0
- package/dist/chunk-MXFUTKCO.js +56 -0
- package/dist/chunk-MYY4PMWX.cjs +264 -0
- package/dist/chunk-N75IH2HY.js +258 -0
- package/dist/chunk-NBMHU6SW.js +669 -0
- package/dist/chunk-NG77ATEW.cjs +778 -0
- package/dist/chunk-NGKREZ36.js +6384 -0
- package/dist/chunk-NHWSN2SD.cjs +718 -0
- package/dist/chunk-NV4VMVXU.js +421 -0
- package/dist/chunk-NW5G5FBQ.js +215 -0
- package/dist/chunk-NXZFOPVD.cjs +5737 -0
- package/dist/chunk-OBIYPYYS.cjs +1993 -0
- package/dist/chunk-OIQYIIO3.cjs +473 -0
- package/dist/chunk-OL46QLBJ.js +61 -0
- package/dist/chunk-OMXII44L.js +5386 -0
- package/dist/chunk-OR4Q2BGL.cjs +759 -0
- package/dist/chunk-OSYNRVOV.cjs +243 -0
- package/dist/chunk-PGANYCAG.cjs +220 -0
- package/dist/chunk-PHENW4MQ.js +65 -0
- package/dist/chunk-PHVRG4I2.js +844 -0
- package/dist/chunk-Q56TT7IL.js +483 -0
- package/dist/chunk-QPOQ7VPI.js +1491 -0
- package/dist/chunk-QPSKHBJP.js +5963 -0
- package/dist/chunk-QXH7QDSZ.js +1989 -0
- package/dist/chunk-R75BSDH7.cjs +50 -0
- package/dist/chunk-RARB3MBR.js +207 -0
- package/dist/chunk-RVBHAXFG.js +56160 -0
- package/dist/chunk-S3MDHNKB.cjs +490 -0
- package/dist/chunk-S4W6Z2MN.js +2426 -0
- package/dist/chunk-S6YWHCKV.cjs +410 -0
- package/dist/chunk-SIEXCILW.js +229 -0
- package/dist/chunk-SJBGLS25.cjs +2196 -0
- package/dist/chunk-SLU6UPJI.cjs +410 -0
- package/dist/chunk-SMIWS6DE.cjs +212 -0
- package/dist/chunk-T2QCTZMO.js +811 -0
- package/dist/chunk-T5J7ZKP3.cjs +4885 -0
- package/dist/chunk-TDCNJHWO.js +341 -0
- package/dist/chunk-TG7O4WBC.js +406 -0
- package/dist/chunk-THCK6ECW.js +2085 -0
- package/dist/chunk-TJHVSBP2.js +439 -0
- package/dist/chunk-TVHSPJ4N.cjs +606 -0
- package/dist/chunk-U44IKGG7.cjs +5390 -0
- package/dist/chunk-UJXISJBN.js +487 -0
- package/dist/chunk-UM6C3CGK.js +4375 -0
- package/dist/chunk-UMABK6SI.js +686 -0
- package/dist/chunk-UOIGFQXC.cjs +9750 -0
- package/dist/chunk-URG2SJLK.js +2521 -0
- package/dist/chunk-V6MKC2FZ.js +979 -0
- package/dist/chunk-VECQYRXL.js +474 -0
- package/dist/chunk-VJ3L7CHT.cjs +169 -0
- package/dist/chunk-VMLKSV26.js +1216 -0
- package/dist/chunk-VVKVQZI6.js +55 -0
- package/dist/chunk-W6RW64BB.cjs +1219 -0
- package/dist/chunk-W7VDOASB.cjs +639 -0
- package/dist/chunk-WDT3JTGM.cjs +483 -0
- package/dist/chunk-WJIHFLMJ.js +404 -0
- package/dist/chunk-WKX5AMOY.js +309 -0
- package/dist/chunk-WNNE3PO3.js +34 -0
- package/dist/chunk-WRKUDAZJ.js +3038 -0
- package/dist/chunk-WRLUBRKS.cjs +1943 -0
- package/dist/chunk-WRNEH5MO.js +3169 -0
- package/dist/chunk-WRWEX5ZF.js +658 -0
- package/dist/chunk-X3GJPRVK.cjs +3246 -0
- package/dist/chunk-XHIV6326.cjs +793 -0
- package/dist/chunk-XIVIE43A.cjs +729 -0
- package/dist/chunk-XJAN7CZG.cjs +432 -0
- package/dist/chunk-XKTPFXGA.js +164 -0
- package/dist/chunk-XL6INWKT.js +121 -0
- package/dist/chunk-XSFNHP2E.js +632 -0
- package/dist/chunk-XT6JY5W5.js +9684 -0
- package/dist/chunk-XUILI4KB.cjs +430 -0
- package/dist/chunk-XUUVDHIF.cjs +3962 -0
- package/dist/chunk-YF2I3MV3.js +46 -0
- package/dist/chunk-YIMN575O.js +4847 -0
- package/dist/chunk-YIVFJNYV.js +176 -0
- package/dist/chunk-YKGPCZEE.cjs +237 -0
- package/dist/chunk-YN6V7R3L.cjs +327 -0
- package/dist/chunk-YQELFNJV.js +39 -0
- package/dist/chunk-YYXH45E2.js +426 -0
- package/dist/chunk-ZBQXLH2L.js +756 -0
- package/dist/chunk-ZDSYWBIE.cjs +69 -0
- package/dist/chunk-ZU7NIRT6.js +325 -0
- package/dist/chunk-ZZ7RO2UQ.js +1243 -0
- package/dist/chunk-ZZEI7MV4.js +778 -0
- package/dist/cli/holoscript-runner.cjs +2255 -0
- package/dist/cli/holoscript-runner.js +2230 -0
- package/dist/codebase/index.cjs +336 -0
- package/dist/codebase/index.js +331 -0
- package/dist/compiler/agent-inference.cjs +31 -0
- package/dist/compiler/agent-inference.js +8 -0
- package/dist/compiler/android-xr.cjs +24 -0
- package/dist/compiler/android-xr.js +14 -0
- package/dist/compiler/android.cjs +24 -0
- package/dist/compiler/android.js +14 -0
- package/dist/compiler/ar.cjs +23 -0
- package/dist/compiler/ar.js +11 -0
- package/dist/compiler/babylon.cjs +21 -0
- package/dist/compiler/babylon.js +15 -0
- package/dist/compiler/business-quest.cjs +43 -0
- package/dist/compiler/business-quest.js +4 -0
- package/dist/compiler/coco.cjs +27 -0
- package/dist/compiler/coco.js +4 -0
- package/dist/compiler/context.cjs +25 -0
- package/dist/compiler/context.d.ts +28 -0
- package/dist/compiler/context.js +8 -0
- package/dist/compiler/domain-block-utils.cjs +605 -0
- package/dist/compiler/domain-block-utils.js +11 -0
- package/dist/compiler/dtdl.cjs +27 -0
- package/dist/compiler/dtdl.js +11 -0
- package/dist/compiler/gltf-pipeline.cjs +39 -0
- package/dist/compiler/gltf-pipeline.js +4 -0
- package/dist/compiler/godot.cjs +18 -0
- package/dist/compiler/godot.js +12 -0
- package/dist/compiler/incremental.cjs +34 -0
- package/dist/compiler/incremental.js +9 -0
- package/dist/compiler/index.cjs +5860 -0
- package/dist/compiler/index.d.ts +322 -21
- package/dist/compiler/index.js +5525 -0
- package/dist/compiler/ios.cjs +23 -0
- package/dist/compiler/ios.js +13 -0
- package/dist/compiler/llm-provider-capabilities.cjs +25 -0
- package/dist/compiler/llm-provider-capabilities.d.ts +43 -0
- package/dist/compiler/llm-provider-capabilities.js +8 -0
- package/dist/compiler/multi-layer.cjs +24 -0
- package/dist/compiler/multi-layer.js +15 -0
- package/dist/compiler/nodetoy.cjs +23 -0
- package/dist/compiler/nodetoy.js +4 -0
- package/dist/compiler/openxr.cjs +19 -0
- package/dist/compiler/openxr.js +13 -0
- package/dist/compiler/playcanvas.cjs +19 -0
- package/dist/compiler/playcanvas.js +13 -0
- package/dist/compiler/r3f.cjs +38 -0
- package/dist/compiler/r3f.d.ts +22 -0
- package/dist/compiler/r3f.js +16 -0
- package/dist/compiler/remotion.cjs +23 -0
- package/dist/compiler/remotion.js +4 -0
- package/dist/compiler/reproducibility.cjs +39 -0
- package/dist/compiler/reproducibility.js +4 -0
- package/dist/compiler/sdf.cjs +24 -0
- package/dist/compiler/sdf.js +12 -0
- package/dist/compiler/semantic-scene.cjs +31 -0
- package/dist/compiler/semantic-scene.js +4 -0
- package/dist/compiler/state.cjs +21 -0
- package/dist/compiler/state.js +11 -0
- package/dist/compiler/trait-composition.cjs +30 -0
- package/dist/compiler/trait-composition.js +9 -0
- package/dist/compiler/unity.cjs +19 -0
- package/dist/compiler/unity.js +13 -0
- package/dist/compiler/unreal.cjs +23 -0
- package/dist/compiler/unreal.js +13 -0
- package/dist/compiler/urdf.cjs +52 -0
- package/dist/compiler/urdf.js +12 -0
- package/dist/compiler/usd-physics.cjs +26 -0
- package/dist/compiler/usd-physics.js +9 -0
- package/dist/compiler/visionos.cjs +19 -0
- package/dist/compiler/visionos.js +13 -0
- package/dist/compiler/vrchat.cjs +22 -0
- package/dist/compiler/vrchat.js +12 -0
- package/dist/compiler/vrr.cjs +24 -0
- package/dist/compiler/vrr.js +12 -0
- package/dist/compiler/wasm.cjs +36 -0
- package/dist/compiler/wasm.js +12 -0
- package/dist/compiler/webgpu.cjs +18 -0
- package/dist/compiler/webgpu.js +12 -0
- package/dist/coordinators/index.cjs +1266 -0
- package/dist/coordinators/index.d.ts +72 -1
- package/dist/coordinators/index.js +1259 -0
- package/dist/debugger.cjs +24 -0
- package/dist/debugger.js +11 -0
- package/dist/entries/interop.cjs +64 -0
- package/dist/entries/interop.js +7 -0
- package/dist/entries/scripting.cjs +55 -0
- package/dist/entries/scripting.js +10 -0
- package/dist/hololand/index.cjs +146 -0
- package/dist/hololand/index.d.ts +70 -0
- package/dist/hololand/index.js +5 -0
- package/dist/index.cjs +43233 -0
- package/dist/index.d.ts +976 -21
- package/dist/index.js +39995 -0
- package/dist/math/tropical-spmv.cjs +45 -0
- package/dist/math/tropical-spmv.js +4 -0
- package/dist/math/vec3.cjs +57 -0
- package/dist/math/vec3.js +4 -0
- package/dist/ml-dsa-FSKBTE3W.cjs +1265 -0
- package/dist/ml-dsa-Z7JDKW4L.js +1265 -0
- package/dist/paper-0c-spike/index.cjs +150 -0
- package/dist/paper-0c-spike/index.js +141 -0
- package/dist/parser.cjs +61 -0
- package/dist/parser.d.ts +11 -0
- package/dist/parser.js +12 -0
- package/dist/playwright-3AT3AKTV.cjs +98673 -0
- package/dist/playwright-VZPDVUEQ.js +98660 -0
- package/dist/reconstruction/index.cjs +153 -0
- package/dist/reconstruction/index.d.ts +29 -2
- package/dist/reconstruction/index.js +8 -0
- package/dist/runtime.cjs +316 -0
- package/dist/runtime.d.ts +61 -0
- package/dist/runtime.js +31 -0
- package/dist/self-improvement/index.cjs +563 -0
- package/dist/self-improvement/index.js +545 -0
- package/dist/storage/index.cjs +38 -0
- package/dist/storage/index.js +5 -0
- package/dist/testing.cjs +25 -0
- package/dist/testing.d.ts +70 -0
- package/dist/testing.js +4 -0
- package/dist/traitDocs/traitDocs.cjs +33 -0
- package/dist/traitDocs/traitDocs.js +7 -0
- package/dist/traits/botanical-lotus.cjs +41 -0
- package/dist/traits/botanical-lotus.d.ts +172 -0
- package/dist/traits/botanical-lotus.js +4 -0
- package/dist/traits/engines/index.cjs +520 -0
- package/dist/traits/engines/index.js +380 -0
- package/dist/traits/index.cjs +24409 -0
- package/dist/traits/index.d.ts +171 -35
- package/dist/traits/index.js +21634 -0
- package/dist/traits/simulation-solver-factory.cjs +13 -0
- package/dist/traits/simulation-solver-factory.d.ts +23 -0
- package/dist/traits/simulation-solver-factory.js +4 -0
- package/dist/traits/webcam-gaze.cjs +35 -0
- package/dist/traits/webcam-gaze.d.ts +17 -0
- package/dist/traits/webcam-gaze.js +4 -0
- package/dist/type-checker.cjs +19 -0
- package/dist/type-checker.js +6 -0
- package/dist/world/index.cjs +947 -0
- package/dist/world/index.d.ts +147 -0
- package/dist/world/index.js +939 -0
- package/dist/world-model/index.cjs +1446 -0
- package/dist/world-model/index.d.ts +119 -0
- package/dist/world-model/index.js +1414 -0
- package/dist/wot/index.cjs +29 -0
- package/dist/wot/index.js +4 -0
- package/package.json +93 -49
|
@@ -0,0 +1,4847 @@
|
|
|
1
|
+
import { ErrorRecovery, generateQuickFixes, enrichErrorWithSuggestions, LIFECYCLE_HOOKS, STRUCTURAL_DIRECTIVES } from './chunk-4N4SXXWF.js';
|
|
2
|
+
import { VR_TRAITS } from './chunk-2BSMSUCE.js';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
// src/parser/HybridChunker.ts
|
|
6
|
+
function countTokens(text) {
|
|
7
|
+
return Math.ceil(text.length / 4);
|
|
8
|
+
}
|
|
9
|
+
function detectFileType(filePath) {
|
|
10
|
+
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
11
|
+
if (["ts", "js", "jsx", "tsx", "hs", "hsplus"].includes(ext)) {
|
|
12
|
+
return "code";
|
|
13
|
+
}
|
|
14
|
+
if (["log", "txt", "csv"].includes(ext)) {
|
|
15
|
+
return "log";
|
|
16
|
+
}
|
|
17
|
+
if (["md", "mdx", "markdown"].includes(ext)) {
|
|
18
|
+
return "markdown";
|
|
19
|
+
}
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
var StructureBasedChunker = class {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.options = {
|
|
25
|
+
maxTokens: options.maxTokens || 1024,
|
|
26
|
+
overlapTokens: options.overlapTokens || 100,
|
|
27
|
+
semanticThreshold: options.semanticThreshold || 0.85,
|
|
28
|
+
debug: options.debug || false
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Chunk code files by detecting structural boundaries
|
|
33
|
+
*/
|
|
34
|
+
chunk(content, filePath) {
|
|
35
|
+
const lines = content.split("\n");
|
|
36
|
+
const boundaries = this.detectStructuralBoundaries(lines);
|
|
37
|
+
if (boundaries.length === 0) {
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
id: `structure:${filePath}:0`,
|
|
41
|
+
type: "code-block",
|
|
42
|
+
startLine: 1,
|
|
43
|
+
endLine: lines.length,
|
|
44
|
+
content,
|
|
45
|
+
tokens: countTokens(content),
|
|
46
|
+
strategy: "structure"
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let currentChunk = [];
|
|
52
|
+
let currentStart = 1;
|
|
53
|
+
let chunkIndex = 0;
|
|
54
|
+
for (let i = 0; i < boundaries.length; i++) {
|
|
55
|
+
const boundary = boundaries[i];
|
|
56
|
+
const segmentContent = lines.slice(boundary.startLine - 1, boundary.endLine).join("\n");
|
|
57
|
+
const segmentTokens = countTokens(segmentContent);
|
|
58
|
+
if (currentChunk.length > 0 && countTokens(currentChunk.join("\n") + "\n" + segmentContent) > this.options.maxTokens) {
|
|
59
|
+
const chunkContent = currentChunk.join("\n");
|
|
60
|
+
chunks.push({
|
|
61
|
+
id: `structure:${filePath}:${chunkIndex++}`,
|
|
62
|
+
type: "code-block",
|
|
63
|
+
startLine: currentStart,
|
|
64
|
+
endLine: currentStart + currentChunk.length - 1,
|
|
65
|
+
content: chunkContent,
|
|
66
|
+
tokens: countTokens(chunkContent),
|
|
67
|
+
strategy: "structure"
|
|
68
|
+
});
|
|
69
|
+
currentChunk = [];
|
|
70
|
+
currentStart = boundary.startLine;
|
|
71
|
+
}
|
|
72
|
+
if (segmentTokens > this.options.maxTokens) {
|
|
73
|
+
const subChunks = this.recursiveSplit(
|
|
74
|
+
segmentContent,
|
|
75
|
+
boundary.startLine,
|
|
76
|
+
filePath,
|
|
77
|
+
chunkIndex
|
|
78
|
+
);
|
|
79
|
+
chunks.push(...subChunks);
|
|
80
|
+
chunkIndex += subChunks.length;
|
|
81
|
+
} else {
|
|
82
|
+
currentChunk.push(segmentContent);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (currentChunk.length > 0) {
|
|
86
|
+
const chunkContent = currentChunk.join("\n");
|
|
87
|
+
chunks.push({
|
|
88
|
+
id: `structure:${filePath}:${chunkIndex}`,
|
|
89
|
+
type: "code-block",
|
|
90
|
+
startLine: currentStart,
|
|
91
|
+
endLine: currentStart + currentChunk.length - 1,
|
|
92
|
+
content: chunkContent,
|
|
93
|
+
tokens: countTokens(chunkContent),
|
|
94
|
+
strategy: "structure"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return chunks;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Detect structural boundaries (functions, classes, interfaces)
|
|
101
|
+
*/
|
|
102
|
+
detectStructuralBoundaries(lines) {
|
|
103
|
+
const boundaries = [];
|
|
104
|
+
let braceDepth = 0;
|
|
105
|
+
let currentBlock = null;
|
|
106
|
+
for (let i = 0; i < lines.length; i++) {
|
|
107
|
+
const line = lines[i];
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (!currentBlock) {
|
|
110
|
+
const functionMatch = trimmed.match(
|
|
111
|
+
/^(export\s+)?(async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/
|
|
112
|
+
);
|
|
113
|
+
const classMatch = trimmed.match(
|
|
114
|
+
/^(export\s+)?(abstract\s+)?class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/
|
|
115
|
+
);
|
|
116
|
+
const interfaceMatch = trimmed.match(/^(export\s+)?interface\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
117
|
+
const methodMatch = trimmed.match(
|
|
118
|
+
/^(public|private|protected|static)?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*[:{]/
|
|
119
|
+
);
|
|
120
|
+
if (functionMatch) {
|
|
121
|
+
currentBlock = {
|
|
122
|
+
type: "function",
|
|
123
|
+
name: functionMatch[3],
|
|
124
|
+
startLine: i + 1
|
|
125
|
+
};
|
|
126
|
+
} else if (classMatch) {
|
|
127
|
+
currentBlock = {
|
|
128
|
+
type: "class",
|
|
129
|
+
name: classMatch[3],
|
|
130
|
+
startLine: i + 1
|
|
131
|
+
};
|
|
132
|
+
} else if (interfaceMatch) {
|
|
133
|
+
currentBlock = {
|
|
134
|
+
type: "interface",
|
|
135
|
+
name: interfaceMatch[2],
|
|
136
|
+
startLine: i + 1
|
|
137
|
+
};
|
|
138
|
+
} else if (methodMatch) {
|
|
139
|
+
currentBlock = {
|
|
140
|
+
type: "method",
|
|
141
|
+
name: methodMatch[2],
|
|
142
|
+
startLine: i + 1
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (currentBlock) {
|
|
147
|
+
for (const char of line) {
|
|
148
|
+
if (char === "{") braceDepth++;
|
|
149
|
+
if (char === "}") braceDepth--;
|
|
150
|
+
}
|
|
151
|
+
if (braceDepth === 0 && trimmed.includes("}")) {
|
|
152
|
+
boundaries.push({
|
|
153
|
+
type: currentBlock.type,
|
|
154
|
+
name: currentBlock.name,
|
|
155
|
+
startLine: currentBlock.startLine,
|
|
156
|
+
endLine: i + 1
|
|
157
|
+
});
|
|
158
|
+
currentBlock = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (currentBlock) {
|
|
163
|
+
boundaries.push({
|
|
164
|
+
type: currentBlock.type,
|
|
165
|
+
name: currentBlock.name,
|
|
166
|
+
startLine: currentBlock.startLine,
|
|
167
|
+
endLine: lines.length
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return boundaries;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Recursively split large segments
|
|
174
|
+
*/
|
|
175
|
+
recursiveSplit(content, startLine, filePath, baseIndex) {
|
|
176
|
+
const lines = content.split("\n");
|
|
177
|
+
const chunks = [];
|
|
178
|
+
let currentChunk = [];
|
|
179
|
+
let currentStart = startLine;
|
|
180
|
+
for (let i = 0; i < lines.length; i++) {
|
|
181
|
+
currentChunk.push(lines[i]);
|
|
182
|
+
if (countTokens(currentChunk.join("\n")) >= this.options.maxTokens) {
|
|
183
|
+
const chunkContent = currentChunk.join("\n");
|
|
184
|
+
chunks.push({
|
|
185
|
+
id: `structure:${filePath}:${baseIndex + chunks.length}`,
|
|
186
|
+
type: "code-fragment",
|
|
187
|
+
startLine: currentStart,
|
|
188
|
+
endLine: currentStart + currentChunk.length - 1,
|
|
189
|
+
content: chunkContent,
|
|
190
|
+
tokens: countTokens(chunkContent),
|
|
191
|
+
strategy: "structure",
|
|
192
|
+
metadata: { splitReason: "recursive-oversized" }
|
|
193
|
+
});
|
|
194
|
+
currentChunk = [];
|
|
195
|
+
currentStart = startLine + i + 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (currentChunk.length > 0) {
|
|
199
|
+
const chunkContent = currentChunk.join("\n");
|
|
200
|
+
chunks.push({
|
|
201
|
+
id: `structure:${filePath}:${baseIndex + chunks.length}`,
|
|
202
|
+
type: "code-fragment",
|
|
203
|
+
startLine: currentStart,
|
|
204
|
+
endLine: currentStart + currentChunk.length - 1,
|
|
205
|
+
content: chunkContent,
|
|
206
|
+
tokens: countTokens(chunkContent),
|
|
207
|
+
strategy: "structure"
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return chunks;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var FixedSizeChunker = class {
|
|
214
|
+
constructor(options) {
|
|
215
|
+
this.options = {
|
|
216
|
+
maxTokens: options.maxTokens || 1024,
|
|
217
|
+
overlapTokens: options.overlapTokens || 100,
|
|
218
|
+
semanticThreshold: options.semanticThreshold || 0.85,
|
|
219
|
+
debug: options.debug || false
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Chunk text files into fixed-size segments with overlap
|
|
224
|
+
*/
|
|
225
|
+
chunk(content, filePath) {
|
|
226
|
+
const lines = content.split("\n");
|
|
227
|
+
const chunks = [];
|
|
228
|
+
let currentChunk = [];
|
|
229
|
+
let currentStart = 1;
|
|
230
|
+
let chunkIndex = 0;
|
|
231
|
+
for (let i = 0; i < lines.length; i++) {
|
|
232
|
+
currentChunk.push(lines[i]);
|
|
233
|
+
const currentTokens = countTokens(currentChunk.join("\n"));
|
|
234
|
+
if (currentTokens >= this.options.maxTokens) {
|
|
235
|
+
const chunkContent = currentChunk.join("\n");
|
|
236
|
+
chunks.push({
|
|
237
|
+
id: `fixed:${filePath}:${chunkIndex++}`,
|
|
238
|
+
type: "text-block",
|
|
239
|
+
startLine: currentStart,
|
|
240
|
+
endLine: currentStart + currentChunk.length - 1,
|
|
241
|
+
content: chunkContent,
|
|
242
|
+
tokens: currentTokens,
|
|
243
|
+
strategy: "fixed"
|
|
244
|
+
});
|
|
245
|
+
const overlapLines = this.calculateOverlapLines(currentChunk);
|
|
246
|
+
currentChunk = overlapLines;
|
|
247
|
+
currentStart = currentStart + currentChunk.length - overlapLines.length;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (currentChunk.length > 0) {
|
|
251
|
+
const chunkContent = currentChunk.join("\n");
|
|
252
|
+
chunks.push({
|
|
253
|
+
id: `fixed:${filePath}:${chunkIndex}`,
|
|
254
|
+
type: "text-block",
|
|
255
|
+
startLine: currentStart,
|
|
256
|
+
endLine: currentStart + currentChunk.length - 1,
|
|
257
|
+
content: chunkContent,
|
|
258
|
+
tokens: countTokens(chunkContent),
|
|
259
|
+
strategy: "fixed"
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return chunks;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Calculate overlap lines based on token count
|
|
266
|
+
*/
|
|
267
|
+
calculateOverlapLines(lines) {
|
|
268
|
+
const overlapLines = [];
|
|
269
|
+
let overlapTokens = 0;
|
|
270
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
271
|
+
const lineTokens = countTokens(lines[i]);
|
|
272
|
+
if (overlapTokens + lineTokens > this.options.overlapTokens) {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
overlapLines.unshift(lines[i]);
|
|
276
|
+
overlapTokens += lineTokens;
|
|
277
|
+
}
|
|
278
|
+
return overlapLines;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var SemanticChunker = class {
|
|
282
|
+
constructor(options) {
|
|
283
|
+
this.options = {
|
|
284
|
+
maxTokens: options.maxTokens || 1024,
|
|
285
|
+
overlapTokens: options.overlapTokens || 100,
|
|
286
|
+
semanticThreshold: options.semanticThreshold || 0.85,
|
|
287
|
+
debug: options.debug || false
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Chunk markdown/docs by semantic similarity
|
|
292
|
+
*/
|
|
293
|
+
chunk(content, filePath) {
|
|
294
|
+
const paragraphs = this.detectParagraphs(content);
|
|
295
|
+
if (paragraphs.length === 0) {
|
|
296
|
+
return [
|
|
297
|
+
{
|
|
298
|
+
id: `semantic:${filePath}:0`,
|
|
299
|
+
type: "document",
|
|
300
|
+
startLine: 1,
|
|
301
|
+
endLine: content.split("\n").length,
|
|
302
|
+
content,
|
|
303
|
+
tokens: countTokens(content),
|
|
304
|
+
strategy: "semantic"
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
const chunks = [];
|
|
309
|
+
let currentGroup = [];
|
|
310
|
+
let chunkIndex = 0;
|
|
311
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
312
|
+
const para = paragraphs[i];
|
|
313
|
+
if (currentGroup.length === 0) {
|
|
314
|
+
currentGroup.push(para);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const lastPara = currentGroup[currentGroup.length - 1];
|
|
318
|
+
const similarity2 = this.computeSimilarity(lastPara.content, para.content);
|
|
319
|
+
const groupContent = currentGroup.map((p) => p.content).join("\n\n");
|
|
320
|
+
const groupTokens = countTokens(groupContent + "\n\n" + para.content);
|
|
321
|
+
if (similarity2 >= this.options.semanticThreshold && groupTokens <= this.options.maxTokens) {
|
|
322
|
+
currentGroup.push(para);
|
|
323
|
+
} else {
|
|
324
|
+
const chunkContent = currentGroup.map((p) => p.content).join("\n\n");
|
|
325
|
+
chunks.push({
|
|
326
|
+
id: `semantic:${filePath}:${chunkIndex++}`,
|
|
327
|
+
type: "semantic-group",
|
|
328
|
+
startLine: currentGroup[0].startLine,
|
|
329
|
+
endLine: currentGroup[currentGroup.length - 1].endLine,
|
|
330
|
+
content: chunkContent,
|
|
331
|
+
tokens: countTokens(chunkContent),
|
|
332
|
+
strategy: "semantic",
|
|
333
|
+
metadata: {
|
|
334
|
+
paragraphCount: currentGroup.length
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
currentGroup = [para];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (currentGroup.length > 0) {
|
|
341
|
+
const chunkContent = currentGroup.map((p) => p.content).join("\n\n");
|
|
342
|
+
chunks.push({
|
|
343
|
+
id: `semantic:${filePath}:${chunkIndex}`,
|
|
344
|
+
type: "semantic-group",
|
|
345
|
+
startLine: currentGroup[0].startLine,
|
|
346
|
+
endLine: currentGroup[currentGroup.length - 1].endLine,
|
|
347
|
+
content: chunkContent,
|
|
348
|
+
tokens: countTokens(chunkContent),
|
|
349
|
+
strategy: "semantic",
|
|
350
|
+
metadata: {
|
|
351
|
+
paragraphCount: currentGroup.length
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return chunks;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Detect paragraph boundaries in markdown
|
|
359
|
+
*/
|
|
360
|
+
detectParagraphs(content) {
|
|
361
|
+
const lines = content.split("\n");
|
|
362
|
+
const paragraphs = [];
|
|
363
|
+
let currentPara = [];
|
|
364
|
+
let currentStart = 1;
|
|
365
|
+
for (let i = 0; i < lines.length; i++) {
|
|
366
|
+
const line = lines[i];
|
|
367
|
+
const trimmed = line.trim();
|
|
368
|
+
if (trimmed === "" || trimmed.startsWith("#") || /^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) {
|
|
369
|
+
if (currentPara.length > 0) {
|
|
370
|
+
paragraphs.push({
|
|
371
|
+
content: currentPara.join("\n"),
|
|
372
|
+
startLine: currentStart,
|
|
373
|
+
endLine: i
|
|
374
|
+
});
|
|
375
|
+
currentPara = [];
|
|
376
|
+
}
|
|
377
|
+
if (trimmed !== "") {
|
|
378
|
+
currentPara.push(line);
|
|
379
|
+
currentStart = i + 1;
|
|
380
|
+
} else {
|
|
381
|
+
currentStart = i + 2;
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
currentPara.push(line);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (currentPara.length > 0) {
|
|
388
|
+
paragraphs.push({
|
|
389
|
+
content: currentPara.join("\n"),
|
|
390
|
+
startLine: currentStart,
|
|
391
|
+
endLine: lines.length
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return paragraphs;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Compute similarity between two text segments
|
|
398
|
+
*
|
|
399
|
+
* Simple heuristic: Jaccard similarity of word sets
|
|
400
|
+
* For production: Use sentence-transformers embeddings + cosine similarity
|
|
401
|
+
*/
|
|
402
|
+
computeSimilarity(text1, text2) {
|
|
403
|
+
const words1 = new Set(
|
|
404
|
+
text1.toLowerCase().split(/\s+/).filter((w) => w.length > 3)
|
|
405
|
+
);
|
|
406
|
+
const words2 = new Set(
|
|
407
|
+
text2.toLowerCase().split(/\s+/).filter((w) => w.length > 3)
|
|
408
|
+
);
|
|
409
|
+
const intersection = new Set([...words1].filter((w) => words2.has(w)));
|
|
410
|
+
const union = /* @__PURE__ */ new Set([...words1, ...words2]);
|
|
411
|
+
return union.size > 0 ? intersection.size / union.size : 0;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
var HybridChunker = class {
|
|
415
|
+
constructor(options = {}) {
|
|
416
|
+
this.options = {
|
|
417
|
+
maxTokens: options.maxTokens || 1024,
|
|
418
|
+
overlapTokens: options.overlapTokens || 100,
|
|
419
|
+
semanticThreshold: options.semanticThreshold || 0.85,
|
|
420
|
+
debug: options.debug || false
|
|
421
|
+
};
|
|
422
|
+
this.structureChunker = new StructureBasedChunker(this.options);
|
|
423
|
+
this.fixedChunker = new FixedSizeChunker(this.options);
|
|
424
|
+
this.semanticChunker = new SemanticChunker(this.options);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Chunk a file using the optimal strategy
|
|
428
|
+
*/
|
|
429
|
+
chunk(content, filePath) {
|
|
430
|
+
const fileType = detectFileType(filePath);
|
|
431
|
+
this.log(`Chunking ${filePath} (type: ${fileType})`);
|
|
432
|
+
let chunks;
|
|
433
|
+
switch (fileType) {
|
|
434
|
+
case "code":
|
|
435
|
+
chunks = this.structureChunker.chunk(content, filePath);
|
|
436
|
+
break;
|
|
437
|
+
case "log":
|
|
438
|
+
chunks = this.fixedChunker.chunk(content, filePath);
|
|
439
|
+
break;
|
|
440
|
+
case "markdown":
|
|
441
|
+
chunks = this.semanticChunker.chunk(content, filePath);
|
|
442
|
+
break;
|
|
443
|
+
default:
|
|
444
|
+
this.log(`Unknown file type for ${filePath}, using fixed-size chunking`);
|
|
445
|
+
chunks = this.fixedChunker.chunk(content, filePath);
|
|
446
|
+
}
|
|
447
|
+
this.log(
|
|
448
|
+
`Generated ${chunks.length} chunks (${chunks.reduce((sum, c) => sum + c.tokens, 0)} total tokens)`
|
|
449
|
+
);
|
|
450
|
+
return chunks;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Get chunking statistics
|
|
454
|
+
*/
|
|
455
|
+
getStats(chunks) {
|
|
456
|
+
const totalTokens = chunks.reduce((sum, c) => sum + c.tokens, 0);
|
|
457
|
+
const strategyDistribution = {};
|
|
458
|
+
for (const chunk of chunks) {
|
|
459
|
+
strategyDistribution[chunk.strategy] = (strategyDistribution[chunk.strategy] || 0) + 1;
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
totalChunks: chunks.length,
|
|
463
|
+
totalTokens,
|
|
464
|
+
avgTokensPerChunk: chunks.length > 0 ? totalTokens / chunks.length : 0,
|
|
465
|
+
strategyDistribution
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Debug logging
|
|
470
|
+
*/
|
|
471
|
+
log(message) {
|
|
472
|
+
if (this.options.debug) {
|
|
473
|
+
console.log(`[HybridChunker] ${message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
function createHybridChunker(options) {
|
|
478
|
+
return new HybridChunker(options);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/parser/ChunkDetector.ts
|
|
482
|
+
var ChunkDetector = class {
|
|
483
|
+
/**
|
|
484
|
+
* Detects chunks in the source code based on top-level keywords
|
|
485
|
+
*
|
|
486
|
+
* @deprecated Use detectHybrid() for better performance with multi-strategy chunking
|
|
487
|
+
*/
|
|
488
|
+
static detect(source) {
|
|
489
|
+
const lines = source.split(/\r?\n/);
|
|
490
|
+
const chunks = [];
|
|
491
|
+
let currentChunk = null;
|
|
492
|
+
let braceDepth = 0;
|
|
493
|
+
for (let i = 0; i < lines.length; i++) {
|
|
494
|
+
const line = lines[i];
|
|
495
|
+
const trimmed = line.trim();
|
|
496
|
+
if (!currentChunk && (!trimmed || trimmed.startsWith("//"))) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (!currentChunk) {
|
|
500
|
+
const orbMatch = trimmed.match(/^orb\s+([a-zA-Z0-9_#"]+)/);
|
|
501
|
+
const templateMatch = trimmed.match(/^template\s+"([^"]+)"/);
|
|
502
|
+
const envMatch = trimmed.startsWith("environment");
|
|
503
|
+
const logicMatch = trimmed.startsWith("logic");
|
|
504
|
+
const directiveMatch = trimmed.startsWith("@");
|
|
505
|
+
if (orbMatch || templateMatch || envMatch || logicMatch || directiveMatch) {
|
|
506
|
+
currentChunk = {
|
|
507
|
+
startLine: i + 1,
|
|
508
|
+
content: line
|
|
509
|
+
};
|
|
510
|
+
if (orbMatch) {
|
|
511
|
+
currentChunk.type = "orb";
|
|
512
|
+
currentChunk.name = orbMatch[1].replace(/[#"]/g, "");
|
|
513
|
+
currentChunk.id = `orb:${currentChunk.name}`;
|
|
514
|
+
} else if (templateMatch) {
|
|
515
|
+
currentChunk.type = "template";
|
|
516
|
+
currentChunk.name = templateMatch[1];
|
|
517
|
+
currentChunk.id = `template:${currentChunk.name}`;
|
|
518
|
+
} else if (envMatch) {
|
|
519
|
+
currentChunk.type = "environment";
|
|
520
|
+
currentChunk.id = `environment:${i + 1}`;
|
|
521
|
+
} else if (logicMatch) {
|
|
522
|
+
currentChunk.type = "logic";
|
|
523
|
+
currentChunk.id = `logic:${i + 1}`;
|
|
524
|
+
} else if (directiveMatch) {
|
|
525
|
+
currentChunk.type = "directive";
|
|
526
|
+
if (!trimmed.includes("{")) {
|
|
527
|
+
currentChunk.endLine = i + 1;
|
|
528
|
+
currentChunk.id = `directive:${i + 1}`;
|
|
529
|
+
chunks.push(currentChunk);
|
|
530
|
+
currentChunk = null;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
currentChunk.id = `directive:${i + 1}`;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
currentChunk.content += "\n" + line;
|
|
538
|
+
}
|
|
539
|
+
if (currentChunk) {
|
|
540
|
+
for (const char of line) {
|
|
541
|
+
if (char === "{") braceDepth++;
|
|
542
|
+
if (char === "}") braceDepth--;
|
|
543
|
+
}
|
|
544
|
+
if (braceDepth === 0) {
|
|
545
|
+
currentChunk.endLine = i + 1;
|
|
546
|
+
chunks.push(currentChunk);
|
|
547
|
+
currentChunk = null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (currentChunk) {
|
|
552
|
+
currentChunk.endLine = lines.length;
|
|
553
|
+
chunks.push(currentChunk);
|
|
554
|
+
}
|
|
555
|
+
return chunks;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Detect chunks using HybridChunker (structure-based + semantic + fixed-size)
|
|
559
|
+
*
|
|
560
|
+
* Routes .hs/.hsplus files to structure-based chunking for better performance.
|
|
561
|
+
* This method provides 20-30% better parsing speed by using AST-aware boundaries.
|
|
562
|
+
*
|
|
563
|
+
* @param source - Source code content
|
|
564
|
+
* @param filePath - File path for type detection (defaults to .hsplus)
|
|
565
|
+
* @param options - Chunking options
|
|
566
|
+
*/
|
|
567
|
+
static detectHybrid(source, filePath = "file.hsplus", options) {
|
|
568
|
+
const hybridChunker = createHybridChunker(options);
|
|
569
|
+
const chunks = hybridChunker.chunk(source, filePath);
|
|
570
|
+
return chunks.map((chunk) => ({
|
|
571
|
+
id: chunk.id,
|
|
572
|
+
type: this.mapChunkType(chunk.type),
|
|
573
|
+
name: chunk.name,
|
|
574
|
+
startLine: chunk.startLine,
|
|
575
|
+
endLine: chunk.endLine,
|
|
576
|
+
content: chunk.content,
|
|
577
|
+
tokens: chunk.tokens,
|
|
578
|
+
strategy: chunk.strategy,
|
|
579
|
+
metadata: chunk.metadata
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Map HybridChunker types to ChunkDetector types
|
|
584
|
+
*/
|
|
585
|
+
static mapChunkType(hybridType) {
|
|
586
|
+
if (hybridType.includes("class")) return "template";
|
|
587
|
+
if (hybridType.includes("function")) return "logic";
|
|
588
|
+
return "unknown";
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
var ParseCache = class {
|
|
592
|
+
constructor(maxEntries = 500) {
|
|
593
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
594
|
+
this.evictionCount = 0;
|
|
595
|
+
/** Monotonically increasing counter for reliable LRU ordering */
|
|
596
|
+
this.clock = 0;
|
|
597
|
+
this.maxEntries = maxEntries;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Generates a hash for the chunk content
|
|
601
|
+
*/
|
|
602
|
+
static hash(content) {
|
|
603
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Retrieves a cached node if the hash matches
|
|
607
|
+
*/
|
|
608
|
+
get(id, currentHash) {
|
|
609
|
+
const cached = this.cache.get(id);
|
|
610
|
+
if (cached && cached.hash === currentHash) {
|
|
611
|
+
cached.lastUsed = ++this.clock;
|
|
612
|
+
return cached.node;
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Stores a node in the cache
|
|
618
|
+
*/
|
|
619
|
+
set(id, hash, node) {
|
|
620
|
+
if (this.cache.size >= this.maxEntries) {
|
|
621
|
+
this.evictOldest();
|
|
622
|
+
}
|
|
623
|
+
this.cache.set(id, {
|
|
624
|
+
id,
|
|
625
|
+
hash,
|
|
626
|
+
node,
|
|
627
|
+
lastUsed: ++this.clock
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Clears the entire cache
|
|
632
|
+
*/
|
|
633
|
+
clear() {
|
|
634
|
+
this.cache.clear();
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Returns cache statistics: current size, total evictions, and capacity.
|
|
638
|
+
*/
|
|
639
|
+
getStats() {
|
|
640
|
+
return {
|
|
641
|
+
size: this.cache.size,
|
|
642
|
+
evictions: this.evictionCount,
|
|
643
|
+
maxEntries: this.maxEntries
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
evictOldest() {
|
|
647
|
+
let oldestId = null;
|
|
648
|
+
let oldestTime = Infinity;
|
|
649
|
+
for (const [id, entry] of this.cache.entries()) {
|
|
650
|
+
if (entry.lastUsed < oldestTime) {
|
|
651
|
+
oldestTime = entry.lastUsed;
|
|
652
|
+
oldestId = id;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (oldestId) {
|
|
656
|
+
this.cache.delete(oldestId);
|
|
657
|
+
this.evictionCount++;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
var globalParseCache = new ParseCache();
|
|
662
|
+
|
|
663
|
+
// src/parser/IncrementalParser.ts
|
|
664
|
+
var ChunkBasedIncrementalParser = class {
|
|
665
|
+
constructor(cache = globalParseCache) {
|
|
666
|
+
this.lastSource = "";
|
|
667
|
+
this.lastChunks = /* @__PURE__ */ new Map();
|
|
668
|
+
this.cache = cache;
|
|
669
|
+
this.parser = new HoloScriptPlusParser({
|
|
670
|
+
enableVRTraits: true,
|
|
671
|
+
enableTypeScriptImports: true,
|
|
672
|
+
strict: false
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Parse source with incremental caching
|
|
677
|
+
*/
|
|
678
|
+
parse(source) {
|
|
679
|
+
const startTime = Date.now();
|
|
680
|
+
let cached = 0;
|
|
681
|
+
let parsed = 0;
|
|
682
|
+
const changedChunks = [];
|
|
683
|
+
const currentChunks = this.detectChunks(source);
|
|
684
|
+
const chunkMap = new Map(currentChunks.map((c) => [c.id, c]));
|
|
685
|
+
const changedIds = this.identifyChangedChunks(currentChunks);
|
|
686
|
+
changedChunks.push(...changedIds);
|
|
687
|
+
const dependentIds = this.findDependents(changedIds, currentChunks);
|
|
688
|
+
const toParseIds = /* @__PURE__ */ new Set([...changedIds, ...dependentIds]);
|
|
689
|
+
const chunkNodes = /* @__PURE__ */ new Map();
|
|
690
|
+
for (const chunk of currentChunks) {
|
|
691
|
+
const hash = ParseCache.hash(chunk.content);
|
|
692
|
+
if (toParseIds.has(chunk.id)) {
|
|
693
|
+
const chunkNode = this.parseChunk(chunk);
|
|
694
|
+
if (chunkNode) {
|
|
695
|
+
chunkNodes.set(chunk.id, chunkNode);
|
|
696
|
+
this.cache.set(chunk.id, hash, chunkNode);
|
|
697
|
+
parsed++;
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
const cached_node = this.cache.get(chunk.id, hash);
|
|
701
|
+
if (cached_node) {
|
|
702
|
+
chunkNodes.set(chunk.id, cached_node);
|
|
703
|
+
cached++;
|
|
704
|
+
} else {
|
|
705
|
+
const chunkNode = this.parseChunk(chunk);
|
|
706
|
+
if (chunkNode) {
|
|
707
|
+
chunkNodes.set(chunk.id, chunkNode);
|
|
708
|
+
this.cache.set(chunk.id, hash, chunkNode);
|
|
709
|
+
parsed++;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const ast = this.assembleAST(chunkNodes, currentChunks);
|
|
715
|
+
this.lastSource = source;
|
|
716
|
+
this.lastChunks = chunkMap;
|
|
717
|
+
const duration = Date.now() - startTime;
|
|
718
|
+
return {
|
|
719
|
+
ast,
|
|
720
|
+
cached,
|
|
721
|
+
parsed,
|
|
722
|
+
duration,
|
|
723
|
+
changedChunks
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Detects top-level chunks in the source
|
|
728
|
+
*/
|
|
729
|
+
detectChunks(source) {
|
|
730
|
+
return ChunkDetector.detect(source);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Identifies which chunks have changed by comparing hashes
|
|
734
|
+
*/
|
|
735
|
+
identifyChangedChunks(currentChunks) {
|
|
736
|
+
const changed = [];
|
|
737
|
+
for (const chunk of currentChunks) {
|
|
738
|
+
const hash = ParseCache.hash(chunk.content);
|
|
739
|
+
const lastChunk = this.lastChunks.get(chunk.id);
|
|
740
|
+
if (!lastChunk) {
|
|
741
|
+
changed.push(chunk.id);
|
|
742
|
+
} else {
|
|
743
|
+
const lastHash = ParseCache.hash(lastChunk.content);
|
|
744
|
+
if (hash !== lastHash) {
|
|
745
|
+
changed.push(chunk.id);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
for (const [id, _lastChunk] of this.lastChunks.entries()) {
|
|
750
|
+
const currentChunk = this.detectChunks(this.lastSource).find((c) => c.id === id);
|
|
751
|
+
if (!currentChunk) {
|
|
752
|
+
changed.push(id);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return changed;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Finds chunks that depend on the changed ones
|
|
759
|
+
*/
|
|
760
|
+
findDependents(changedIds, chunks) {
|
|
761
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
762
|
+
const changeSet = new Set(changedIds);
|
|
763
|
+
const references = /* @__PURE__ */ new Map();
|
|
764
|
+
for (const chunk of chunks) {
|
|
765
|
+
const refs = this.extractReferences(chunk.content);
|
|
766
|
+
if (refs.size > 0) {
|
|
767
|
+
references.set(chunk.id, refs);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
for (const [chunkId, refs] of references.entries()) {
|
|
771
|
+
if (!changeSet.has(chunkId)) {
|
|
772
|
+
for (const ref of refs) {
|
|
773
|
+
if (changeSet.has(ref) || dependents.has(ref)) {
|
|
774
|
+
dependents.add(chunkId);
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return Array.from(dependents);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Extracts referenced identifiers from a chunk (templates, orbs, logic)
|
|
784
|
+
*/
|
|
785
|
+
extractReferences(content) {
|
|
786
|
+
const refs = /* @__PURE__ */ new Set();
|
|
787
|
+
const templateRefs = content.match(/using\s+"([^"]+)"/g);
|
|
788
|
+
if (templateRefs) {
|
|
789
|
+
templateRefs.forEach((ref) => {
|
|
790
|
+
const name = ref.match(/"([^"]+)"/)[1];
|
|
791
|
+
refs.add(`template:${name}`);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
const spreadRefs = content.match(/\.\.\.\s*([a-zA-Z0-9_]+)/g);
|
|
795
|
+
if (spreadRefs) {
|
|
796
|
+
spreadRefs.forEach((ref) => {
|
|
797
|
+
const name = ref.replace(/\.\.\./, "").trim();
|
|
798
|
+
refs.add(`identifier:${name}`);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
const compRefs = content.match(/@composition\s+"([^"]+)"/g);
|
|
802
|
+
if (compRefs) {
|
|
803
|
+
compRefs.forEach((ref) => {
|
|
804
|
+
const name = ref.match(/"([^"]+)"/)[1];
|
|
805
|
+
refs.add(`composition:${name}`);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return refs;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Parses a single chunk
|
|
812
|
+
*/
|
|
813
|
+
parseChunk(chunk) {
|
|
814
|
+
try {
|
|
815
|
+
const content = this.wrapChunkForParsing(chunk);
|
|
816
|
+
const result = this.parser.parse(content);
|
|
817
|
+
if (result.success && result.ast) {
|
|
818
|
+
return result.ast;
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
} catch (error) {
|
|
822
|
+
console.warn(`Failed to parse chunk ${chunk.id}:`, error);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Wraps chunk content to make it parseable in isolation
|
|
828
|
+
*/
|
|
829
|
+
wrapChunkForParsing(chunk) {
|
|
830
|
+
return chunk.content;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Assembles final AST from parsed chunks
|
|
834
|
+
*/
|
|
835
|
+
assembleAST(chunkNodes, chunks) {
|
|
836
|
+
const children = [];
|
|
837
|
+
for (const chunk of chunks) {
|
|
838
|
+
const node = chunkNodes.get(chunk.id);
|
|
839
|
+
if (node) {
|
|
840
|
+
children.push(node);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (children.length === 1) {
|
|
844
|
+
return children[0];
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
type: "fragment",
|
|
848
|
+
children,
|
|
849
|
+
properties: {},
|
|
850
|
+
directives: [],
|
|
851
|
+
traits: /* @__PURE__ */ new Map(),
|
|
852
|
+
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Clears the internal cache
|
|
857
|
+
*/
|
|
858
|
+
clearCache() {
|
|
859
|
+
this.cache.clear();
|
|
860
|
+
this.lastChunks.clear();
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Gets cache statistics
|
|
864
|
+
*/
|
|
865
|
+
getCacheStats() {
|
|
866
|
+
return {
|
|
867
|
+
size: this.lastChunks.size,
|
|
868
|
+
maxSize: 500
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
function parseIncrementalChunks(source, cache = globalParseCache) {
|
|
873
|
+
const parser = new ChunkBasedIncrementalParser(cache);
|
|
874
|
+
return parser.parse(source);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/parser/RichErrors.ts
|
|
878
|
+
var HSPLUS_ERROR_CODES = {
|
|
879
|
+
// Syntax Errors (HSP001-HSP099)
|
|
880
|
+
HSP001: "Unexpected token",
|
|
881
|
+
HSP002: "Expected identifier",
|
|
882
|
+
HSP003: "Expected keyword",
|
|
883
|
+
HSP004: "Unclosed brace - missing }",
|
|
884
|
+
HSP005: "Unclosed bracket - missing ]",
|
|
885
|
+
HSP006: "Unclosed parenthesis - missing )",
|
|
886
|
+
HSP007: "Unclosed string literal",
|
|
887
|
+
HSP008: "Invalid number format",
|
|
888
|
+
HSP009: "Missing colon after property name",
|
|
889
|
+
HSP010: "Missing value after colon",
|
|
890
|
+
// Structure Errors (HSP100-HSP199)
|
|
891
|
+
HSP100: "Invalid composition structure",
|
|
892
|
+
HSP101: "Invalid object definition",
|
|
893
|
+
HSP102: "Invalid template definition",
|
|
894
|
+
HSP103: "Invalid spatial_group definition",
|
|
895
|
+
HSP104: "Invalid environment block",
|
|
896
|
+
HSP105: "Invalid state block",
|
|
897
|
+
HSP106: "Invalid logic block",
|
|
898
|
+
HSP107: "Duplicate identifier",
|
|
899
|
+
HSP108: "Missing required property",
|
|
900
|
+
// Trait Errors (HSP200-HSP299)
|
|
901
|
+
HSP200: "Unknown trait",
|
|
902
|
+
HSP201: "Invalid trait syntax",
|
|
903
|
+
HSP202: "Missing trait argument",
|
|
904
|
+
HSP203: "Invalid trait argument type",
|
|
905
|
+
HSP204: "Trait not allowed in this context",
|
|
906
|
+
HSP205: "Duplicate trait",
|
|
907
|
+
// Expression Errors (HSP300-HSP399)
|
|
908
|
+
HSP300: "Invalid expression",
|
|
909
|
+
HSP301: "Undefined variable",
|
|
910
|
+
HSP302: "Invalid operator",
|
|
911
|
+
HSP303: "Type mismatch",
|
|
912
|
+
HSP304: "Invalid function call",
|
|
913
|
+
HSP305: "Missing function argument",
|
|
914
|
+
// Import/Module Errors (HSP400-HSP499)
|
|
915
|
+
HSP400: "Invalid import statement",
|
|
916
|
+
HSP401: "Module not found",
|
|
917
|
+
HSP402: "Circular import detected",
|
|
918
|
+
// Limit Errors (HSP900-HSP999)
|
|
919
|
+
HSP900: "Maximum nesting depth exceeded",
|
|
920
|
+
HSP901: "Maximum array length exceeded",
|
|
921
|
+
HSP902: "Maximum string length exceeded"
|
|
922
|
+
};
|
|
923
|
+
function levenshteinDistance(a, b) {
|
|
924
|
+
const matrix = [];
|
|
925
|
+
for (let i = 0; i <= b.length; i++) {
|
|
926
|
+
matrix[i] = [i];
|
|
927
|
+
}
|
|
928
|
+
for (let j = 0; j <= a.length; j++) {
|
|
929
|
+
matrix[0][j] = j;
|
|
930
|
+
}
|
|
931
|
+
for (let i = 1; i <= b.length; i++) {
|
|
932
|
+
for (let j = 1; j <= a.length; j++) {
|
|
933
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
934
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
935
|
+
} else {
|
|
936
|
+
matrix[i][j] = Math.min(
|
|
937
|
+
matrix[i - 1][j - 1] + 1,
|
|
938
|
+
// substitution
|
|
939
|
+
matrix[i][j - 1] + 1,
|
|
940
|
+
// insertion
|
|
941
|
+
matrix[i - 1][j] + 1
|
|
942
|
+
// deletion
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return matrix[b.length][a.length];
|
|
948
|
+
}
|
|
949
|
+
function similarity(a, b) {
|
|
950
|
+
const aLower = a.toLowerCase();
|
|
951
|
+
const bLower = b.toLowerCase();
|
|
952
|
+
if (aLower === bLower) return 1;
|
|
953
|
+
if (bLower.startsWith(aLower) || aLower.startsWith(bLower)) {
|
|
954
|
+
return 0.9;
|
|
955
|
+
}
|
|
956
|
+
const maxLen = Math.max(a.length, b.length);
|
|
957
|
+
if (maxLen === 0) return 1;
|
|
958
|
+
const distance = levenshteinDistance(aLower, bLower);
|
|
959
|
+
return 1 - distance / maxLen;
|
|
960
|
+
}
|
|
961
|
+
function findSimilarTrait(input, threshold = 0.5) {
|
|
962
|
+
let bestMatch;
|
|
963
|
+
let bestScore = threshold;
|
|
964
|
+
for (const trait of VR_TRAITS) {
|
|
965
|
+
const score = similarity(input, trait);
|
|
966
|
+
if (score > bestScore) {
|
|
967
|
+
bestScore = score;
|
|
968
|
+
bestMatch = trait;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return bestMatch;
|
|
972
|
+
}
|
|
973
|
+
function findSimilarKeyword(input, threshold = 0.5) {
|
|
974
|
+
const keywords = [
|
|
975
|
+
"composition",
|
|
976
|
+
"object",
|
|
977
|
+
"template",
|
|
978
|
+
"spatial_group",
|
|
979
|
+
"environment",
|
|
980
|
+
"state",
|
|
981
|
+
"logic",
|
|
982
|
+
"on_click",
|
|
983
|
+
"on_hover",
|
|
984
|
+
"on_enter",
|
|
985
|
+
"on_exit",
|
|
986
|
+
"position",
|
|
987
|
+
"rotation",
|
|
988
|
+
"scale",
|
|
989
|
+
"color",
|
|
990
|
+
"model",
|
|
991
|
+
"geometry",
|
|
992
|
+
"visible",
|
|
993
|
+
"opacity",
|
|
994
|
+
"material",
|
|
995
|
+
"physics",
|
|
996
|
+
"audio",
|
|
997
|
+
"animation",
|
|
998
|
+
"true",
|
|
999
|
+
"false",
|
|
1000
|
+
"null",
|
|
1001
|
+
"if",
|
|
1002
|
+
"else",
|
|
1003
|
+
"for",
|
|
1004
|
+
"while",
|
|
1005
|
+
"function",
|
|
1006
|
+
"let",
|
|
1007
|
+
"const",
|
|
1008
|
+
"return",
|
|
1009
|
+
"import",
|
|
1010
|
+
"from",
|
|
1011
|
+
"using",
|
|
1012
|
+
"spawn",
|
|
1013
|
+
"emit"
|
|
1014
|
+
];
|
|
1015
|
+
let bestMatch;
|
|
1016
|
+
let bestScore = threshold;
|
|
1017
|
+
for (const keyword of keywords) {
|
|
1018
|
+
const score = similarity(input, keyword);
|
|
1019
|
+
if (score > bestScore) {
|
|
1020
|
+
bestScore = score;
|
|
1021
|
+
bestMatch = keyword;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return bestMatch;
|
|
1025
|
+
}
|
|
1026
|
+
function getSourceContext(source, line, column, contextLines = 2) {
|
|
1027
|
+
const lines = source.split("\n");
|
|
1028
|
+
const result = [];
|
|
1029
|
+
const startLine = Math.max(1, line - contextLines);
|
|
1030
|
+
const endLine = Math.min(lines.length, line + contextLines);
|
|
1031
|
+
const maxLineNum = endLine;
|
|
1032
|
+
const padding = String(maxLineNum).length;
|
|
1033
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
1034
|
+
const lineContent = lines[i - 1] || "";
|
|
1035
|
+
const lineNum = String(i).padStart(padding, " ");
|
|
1036
|
+
const prefix = i === line ? ">" : " ";
|
|
1037
|
+
result.push(`${prefix} ${lineNum} | ${lineContent}`);
|
|
1038
|
+
if (i === line) {
|
|
1039
|
+
const indicator = " ".repeat(padding + 4 + Math.max(0, column - 1)) + "^";
|
|
1040
|
+
result.push(indicator);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return result.join("\n");
|
|
1044
|
+
}
|
|
1045
|
+
function createRichError(code, message, line, column, options = {}) {
|
|
1046
|
+
const { source, suggestion, severity = "error", endLine, endColumn } = options;
|
|
1047
|
+
return {
|
|
1048
|
+
code,
|
|
1049
|
+
message: `${code}: ${message}`,
|
|
1050
|
+
line,
|
|
1051
|
+
column,
|
|
1052
|
+
endLine,
|
|
1053
|
+
endColumn,
|
|
1054
|
+
context: source ? getSourceContext(source, line, column) : void 0,
|
|
1055
|
+
suggestion,
|
|
1056
|
+
severity
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function createTraitError(traitName, line, column, source) {
|
|
1060
|
+
const similar = findSimilarTrait(traitName);
|
|
1061
|
+
const suggestion = similar ? `Did you mean '@${similar}'?` : void 0;
|
|
1062
|
+
return createRichError("HSP200", `Unknown trait '@${traitName}'`, line, column, {
|
|
1063
|
+
source,
|
|
1064
|
+
suggestion
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
function createKeywordError(found, expected, line, column, source) {
|
|
1068
|
+
const similar = findSimilarKeyword(found);
|
|
1069
|
+
const suggestion = similar ? `Did you mean '${similar}'?` : `Expected '${expected}'`;
|
|
1070
|
+
return createRichError("HSP003", `Expected '${expected}' but found '${found}'`, line, column, {
|
|
1071
|
+
source,
|
|
1072
|
+
suggestion
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
function formatRichError(error) {
|
|
1076
|
+
const lines = [];
|
|
1077
|
+
const icon = error.severity === "error" ? "\u274C" : error.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
|
|
1078
|
+
lines.push(`${icon} ${error.message}`);
|
|
1079
|
+
lines.push(` at line ${error.line}, column ${error.column}`);
|
|
1080
|
+
if (error.context) {
|
|
1081
|
+
lines.push("");
|
|
1082
|
+
lines.push(error.context);
|
|
1083
|
+
}
|
|
1084
|
+
if (error.suggestion) {
|
|
1085
|
+
lines.push("");
|
|
1086
|
+
lines.push(`\u{1F4A1} ${error.suggestion}`);
|
|
1087
|
+
}
|
|
1088
|
+
lines.push("");
|
|
1089
|
+
lines.push(`\u{1F4D6} See: https://holoscript.net/errors/${error.code}`);
|
|
1090
|
+
return lines.join("\n");
|
|
1091
|
+
}
|
|
1092
|
+
function formatRichErrors(errors) {
|
|
1093
|
+
if (errors.length === 0) {
|
|
1094
|
+
return "\u2705 No errors found";
|
|
1095
|
+
}
|
|
1096
|
+
const lines = [];
|
|
1097
|
+
const errorCount = errors.filter((e) => e.severity === "error").length;
|
|
1098
|
+
const warningCount = errors.filter((e) => e.severity === "warning").length;
|
|
1099
|
+
lines.push(`Found ${errorCount} error(s) and ${warningCount} warning(s):`);
|
|
1100
|
+
lines.push("");
|
|
1101
|
+
const displayErrors = errors.slice(0, 10);
|
|
1102
|
+
for (let i = 0; i < displayErrors.length; i++) {
|
|
1103
|
+
lines.push(`[${i + 1}/${errors.length}] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
1104
|
+
lines.push(formatRichError(displayErrors[i]));
|
|
1105
|
+
lines.push("");
|
|
1106
|
+
}
|
|
1107
|
+
if (errors.length > 10) {
|
|
1108
|
+
lines.push(`... and ${errors.length - 10} more errors`);
|
|
1109
|
+
}
|
|
1110
|
+
return lines.join("\n");
|
|
1111
|
+
}
|
|
1112
|
+
function getErrorCodeDocumentation() {
|
|
1113
|
+
const docs = [];
|
|
1114
|
+
for (const [code, description] of Object.entries(HSPLUS_ERROR_CODES)) {
|
|
1115
|
+
let category = "General";
|
|
1116
|
+
const num = parseInt(code.replace("HSP", ""), 10);
|
|
1117
|
+
if (num < 100) category = "Syntax";
|
|
1118
|
+
else if (num < 200) category = "Structure";
|
|
1119
|
+
else if (num < 300) category = "Traits";
|
|
1120
|
+
else if (num < 400) category = "Expressions";
|
|
1121
|
+
else if (num < 500) category = "Imports";
|
|
1122
|
+
else if (num >= 900) category = "Limits";
|
|
1123
|
+
docs.push({ code, description, category });
|
|
1124
|
+
}
|
|
1125
|
+
return docs;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/parser/HoloScriptPlusParser.ts
|
|
1129
|
+
var Lexer = class {
|
|
1130
|
+
constructor(source) {
|
|
1131
|
+
this.pos = 0;
|
|
1132
|
+
this.line = 1;
|
|
1133
|
+
this.column = 1;
|
|
1134
|
+
this.indentStack = [0];
|
|
1135
|
+
this.tokens = [];
|
|
1136
|
+
this.pendingDedents = 0;
|
|
1137
|
+
this.source = source;
|
|
1138
|
+
}
|
|
1139
|
+
tokenize() {
|
|
1140
|
+
while (this.pos < this.source.length) {
|
|
1141
|
+
while (this.pendingDedents > 0) {
|
|
1142
|
+
this.tokens.push(this.createToken("DEDENT", ""));
|
|
1143
|
+
this.pendingDedents--;
|
|
1144
|
+
}
|
|
1145
|
+
const char = this.source[this.pos];
|
|
1146
|
+
if (char === " " || char === " ") {
|
|
1147
|
+
if (this.column === 1) {
|
|
1148
|
+
this.handleIndentation();
|
|
1149
|
+
} else {
|
|
1150
|
+
this.advance();
|
|
1151
|
+
}
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (char === "/" && this.peek(1) === "/") {
|
|
1155
|
+
this.skipLineComment();
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (char === "\n") {
|
|
1159
|
+
this.advance();
|
|
1160
|
+
this.tokens.push(this.createToken("NEWLINE", "\n"));
|
|
1161
|
+
this.line++;
|
|
1162
|
+
this.column = 1;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (char === "\r") {
|
|
1166
|
+
const startOff = this.pos;
|
|
1167
|
+
this.advance();
|
|
1168
|
+
if (this.peek() === "\n") {
|
|
1169
|
+
this.advance();
|
|
1170
|
+
}
|
|
1171
|
+
const token = this.createToken("NEWLINE", "\n");
|
|
1172
|
+
token.offset = startOff;
|
|
1173
|
+
this.tokens.push(token);
|
|
1174
|
+
this.line++;
|
|
1175
|
+
this.column = 1;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (char === "{") {
|
|
1179
|
+
this.advance();
|
|
1180
|
+
this.tokens.push(this.createToken("LBRACE", "{"));
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (char === "}") {
|
|
1184
|
+
this.advance();
|
|
1185
|
+
this.tokens.push(this.createToken("RBRACE", "}"));
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
if (char === "[") {
|
|
1189
|
+
this.advance();
|
|
1190
|
+
this.tokens.push(this.createToken("LBRACKET", "["));
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (char === "]") {
|
|
1194
|
+
this.advance();
|
|
1195
|
+
this.tokens.push(this.createToken("RBRACKET", "]"));
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (char === "(") {
|
|
1199
|
+
this.advance();
|
|
1200
|
+
this.tokens.push(this.createToken("LPAREN", "("));
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
if (char === ")") {
|
|
1204
|
+
this.advance();
|
|
1205
|
+
this.tokens.push(this.createToken("RPAREN", ")"));
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
if (char === ":") {
|
|
1209
|
+
this.advance();
|
|
1210
|
+
this.tokens.push(this.createToken("COLON", ":"));
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (char === ",") {
|
|
1214
|
+
this.advance();
|
|
1215
|
+
this.tokens.push(this.createToken("COMMA", ","));
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (char === "@") {
|
|
1219
|
+
this.advance();
|
|
1220
|
+
this.tokens.push(this.createToken("AT", "@"));
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (char === "#") {
|
|
1224
|
+
this.advance();
|
|
1225
|
+
this.tokens.push(this.createToken("HASH", "#"));
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (char === ".") {
|
|
1229
|
+
if (this.peek(1) === "." && this.peek(2) === ".") {
|
|
1230
|
+
const startCol = this.column;
|
|
1231
|
+
this.advance();
|
|
1232
|
+
this.advance();
|
|
1233
|
+
this.advance();
|
|
1234
|
+
this.tokens.push(this.createToken("SPREAD", "..."));
|
|
1235
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
this.advance();
|
|
1239
|
+
this.tokens.push(this.createToken("DOT", "."));
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
if (char === "+") {
|
|
1243
|
+
this.advance();
|
|
1244
|
+
this.tokens.push(this.createToken("PLUS", "+"));
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (char === "-") {
|
|
1248
|
+
if (this.peek(1) === ">") {
|
|
1249
|
+
const startCol = this.column;
|
|
1250
|
+
this.advance();
|
|
1251
|
+
this.advance();
|
|
1252
|
+
this.tokens.push(this.createToken("ARROW", "->"));
|
|
1253
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
this.advance();
|
|
1257
|
+
this.tokens.push(this.createToken("MINUS", "-"));
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (char === "*") {
|
|
1261
|
+
this.advance();
|
|
1262
|
+
this.tokens.push(this.createToken("ASTERISK", "*"));
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
if (char === "/") {
|
|
1266
|
+
if (this.peek(1) === "*") {
|
|
1267
|
+
this.skipBlockComment();
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
if (this.peek(1) === "/") {
|
|
1271
|
+
this.skipLineComment();
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
this.advance();
|
|
1275
|
+
this.tokens.push(this.createToken("SLASH", "/"));
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (char === "%") {
|
|
1279
|
+
this.advance();
|
|
1280
|
+
this.tokens.push(this.createToken("PERCENT", "%"));
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
if (char === "!") {
|
|
1284
|
+
if (this.peek(1) === "=") {
|
|
1285
|
+
const startCol = this.column;
|
|
1286
|
+
const startOff = this.pos;
|
|
1287
|
+
this.advance();
|
|
1288
|
+
this.advance();
|
|
1289
|
+
this.tokens.push(this.createToken("NOT_EQUALS", "!="));
|
|
1290
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1291
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
this.advance();
|
|
1295
|
+
this.tokens.push(this.createToken("EXCLAMATION", "!"));
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
if (char === "=") {
|
|
1299
|
+
if (this.peek(1) === ">") {
|
|
1300
|
+
const startCol = this.column;
|
|
1301
|
+
const startOff = this.pos;
|
|
1302
|
+
this.advance();
|
|
1303
|
+
this.advance();
|
|
1304
|
+
this.tokens.push(this.createToken("ARROW", "=>"));
|
|
1305
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1306
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (this.peek(1) === "=") {
|
|
1310
|
+
const startCol = this.column;
|
|
1311
|
+
const startOff = this.pos;
|
|
1312
|
+
this.advance();
|
|
1313
|
+
this.advance();
|
|
1314
|
+
this.tokens.push(this.createToken("DOUBLE_EQUALS", "=="));
|
|
1315
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1316
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
this.advance();
|
|
1320
|
+
this.tokens.push(this.createToken("EQUALS", "="));
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
if (char === "<") {
|
|
1324
|
+
if (this.peek(1) === "=") {
|
|
1325
|
+
const startCol = this.column;
|
|
1326
|
+
const startOff = this.pos;
|
|
1327
|
+
this.advance();
|
|
1328
|
+
this.advance();
|
|
1329
|
+
this.tokens.push(this.createToken("LESS_EQUAL", "<="));
|
|
1330
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1331
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
this.advance();
|
|
1335
|
+
this.tokens.push(this.createToken("LESS_THAN", "<"));
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (char === ">") {
|
|
1339
|
+
if (this.peek(1) === "=") {
|
|
1340
|
+
const startCol = this.column;
|
|
1341
|
+
const startOff = this.pos;
|
|
1342
|
+
this.advance();
|
|
1343
|
+
this.advance();
|
|
1344
|
+
this.tokens.push(this.createToken("GREATER_EQUAL", ">="));
|
|
1345
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1346
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
this.advance();
|
|
1350
|
+
this.tokens.push(this.createToken("GREATER_THAN", ">"));
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
if (char === "-") {
|
|
1354
|
+
if (this.peek(1) === ">") {
|
|
1355
|
+
const startCol = this.column;
|
|
1356
|
+
this.advance();
|
|
1357
|
+
this.advance();
|
|
1358
|
+
this.tokens.push(this.createToken("ARROW", "->"));
|
|
1359
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (char === "&") {
|
|
1364
|
+
if (this.peek(1) === "&") {
|
|
1365
|
+
const startCol = this.column;
|
|
1366
|
+
const startOff = this.pos;
|
|
1367
|
+
this.advance();
|
|
1368
|
+
this.advance();
|
|
1369
|
+
this.tokens.push(this.createToken("AND", "&&"));
|
|
1370
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1371
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
this.advance();
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
if (char === "|") {
|
|
1378
|
+
if (this.peek(1) === "|") {
|
|
1379
|
+
const startCol = this.column;
|
|
1380
|
+
const startOff = this.pos;
|
|
1381
|
+
this.advance();
|
|
1382
|
+
this.advance();
|
|
1383
|
+
this.tokens.push(this.createToken("OR", "||"));
|
|
1384
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1385
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
this.advance();
|
|
1389
|
+
this.tokens.push(this.createToken("PIPE", "|"));
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (char === '"' || char === "'") {
|
|
1393
|
+
this.tokens.push(this.readString(char));
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (this.isDigit(char) || char === "-" && this.isDigit(this.peek(1))) {
|
|
1397
|
+
this.tokens.push(this.readNumber());
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
if (char === "?") {
|
|
1401
|
+
if (this.peek(1) === "?") {
|
|
1402
|
+
if (this.peek(2) === "=") {
|
|
1403
|
+
const startCol2 = this.column;
|
|
1404
|
+
const startOff2 = this.pos;
|
|
1405
|
+
this.advance();
|
|
1406
|
+
this.advance();
|
|
1407
|
+
this.advance();
|
|
1408
|
+
this.tokens.push(this.createToken("NULL_COALESCE_ASSIGN", "??="));
|
|
1409
|
+
this.tokens[this.tokens.length - 1].column = startCol2;
|
|
1410
|
+
this.tokens[this.tokens.length - 1].offset = startOff2;
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
const startCol = this.column;
|
|
1414
|
+
const startOff = this.pos;
|
|
1415
|
+
this.advance();
|
|
1416
|
+
this.advance();
|
|
1417
|
+
this.tokens.push(this.createToken("NULL_COALESCE", "??"));
|
|
1418
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1419
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (this.peek(1) === ".") {
|
|
1423
|
+
const startCol = this.column;
|
|
1424
|
+
const startOff = this.pos;
|
|
1425
|
+
this.advance();
|
|
1426
|
+
this.advance();
|
|
1427
|
+
this.tokens.push(this.createToken("OPTIONAL_DOT", "?."));
|
|
1428
|
+
this.tokens[this.tokens.length - 1].column = startCol;
|
|
1429
|
+
this.tokens[this.tokens.length - 1].offset = startOff;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
this.advance();
|
|
1433
|
+
this.tokens.push(this.createToken("QUESTION", "?"));
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (char === "`") {
|
|
1437
|
+
this.tokens.push(this.readTemplateString());
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (this.isIdentifierStart(char)) {
|
|
1441
|
+
this.tokens.push(this.readIdentifier());
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
this.advance();
|
|
1445
|
+
}
|
|
1446
|
+
while (this.indentStack.length > 1) {
|
|
1447
|
+
this.tokens.push(this.createToken("DEDENT", ""));
|
|
1448
|
+
this.indentStack.pop();
|
|
1449
|
+
}
|
|
1450
|
+
this.tokens.push(this.createToken("EOF", ""));
|
|
1451
|
+
return this.tokens;
|
|
1452
|
+
}
|
|
1453
|
+
advance() {
|
|
1454
|
+
const char = this.source[this.pos];
|
|
1455
|
+
this.pos++;
|
|
1456
|
+
this.column++;
|
|
1457
|
+
return char;
|
|
1458
|
+
}
|
|
1459
|
+
peek(offset = 0) {
|
|
1460
|
+
const pos = this.pos + offset;
|
|
1461
|
+
return pos < this.source.length ? this.source[pos] : "";
|
|
1462
|
+
}
|
|
1463
|
+
createToken(type, value) {
|
|
1464
|
+
const token = {
|
|
1465
|
+
type,
|
|
1466
|
+
value,
|
|
1467
|
+
line: this.line,
|
|
1468
|
+
column: this.column - (value.length || 0),
|
|
1469
|
+
offset: this.pos - (value.length || 0)
|
|
1470
|
+
};
|
|
1471
|
+
return token;
|
|
1472
|
+
}
|
|
1473
|
+
handleIndentation() {
|
|
1474
|
+
let indent = 0;
|
|
1475
|
+
while (this.peek() === " " || this.peek() === " ") {
|
|
1476
|
+
indent += this.peek() === " " ? 4 : 1;
|
|
1477
|
+
this.advance();
|
|
1478
|
+
}
|
|
1479
|
+
if (this.peek() === "\n" || this.peek() === "\r") {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const currentIndent = this.indentStack[this.indentStack.length - 1];
|
|
1483
|
+
if (indent > currentIndent) {
|
|
1484
|
+
this.indentStack.push(indent);
|
|
1485
|
+
this.tokens.push(this.createToken("INDENT", ""));
|
|
1486
|
+
} else if (indent < currentIndent) {
|
|
1487
|
+
while (this.indentStack.length > 1 && indent < this.indentStack[this.indentStack.length - 1]) {
|
|
1488
|
+
this.indentStack.pop();
|
|
1489
|
+
this.pendingDedents++;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
skipLineComment() {
|
|
1494
|
+
while (this.peek() !== "\n" && this.pos < this.source.length) {
|
|
1495
|
+
this.advance();
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
skipBlockComment() {
|
|
1499
|
+
this.advance();
|
|
1500
|
+
this.advance();
|
|
1501
|
+
while (this.pos < this.source.length) {
|
|
1502
|
+
if (this.peek() === "*" && this.peek(1) === "/") {
|
|
1503
|
+
this.advance();
|
|
1504
|
+
this.advance();
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
if (this.peek() === "\n") {
|
|
1508
|
+
this.line++;
|
|
1509
|
+
this.column = 0;
|
|
1510
|
+
}
|
|
1511
|
+
this.advance();
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
readString(quote) {
|
|
1515
|
+
const startLine = this.line;
|
|
1516
|
+
const startColumn = this.column;
|
|
1517
|
+
this.advance();
|
|
1518
|
+
let value = "";
|
|
1519
|
+
while (this.peek() !== quote && this.pos < this.source.length) {
|
|
1520
|
+
if (this.peek() === "\\") {
|
|
1521
|
+
this.advance();
|
|
1522
|
+
const escaped = this.advance();
|
|
1523
|
+
switch (escaped) {
|
|
1524
|
+
case "n":
|
|
1525
|
+
value += "\n";
|
|
1526
|
+
break;
|
|
1527
|
+
case "t":
|
|
1528
|
+
value += " ";
|
|
1529
|
+
break;
|
|
1530
|
+
case "r":
|
|
1531
|
+
value += "\r";
|
|
1532
|
+
break;
|
|
1533
|
+
case "\\":
|
|
1534
|
+
value += "\\";
|
|
1535
|
+
break;
|
|
1536
|
+
case '"':
|
|
1537
|
+
value += '"';
|
|
1538
|
+
break;
|
|
1539
|
+
case "'":
|
|
1540
|
+
value += "'";
|
|
1541
|
+
break;
|
|
1542
|
+
default:
|
|
1543
|
+
value += escaped;
|
|
1544
|
+
}
|
|
1545
|
+
} else if (this.peek() === "\n") {
|
|
1546
|
+
this.line++;
|
|
1547
|
+
this.column = 0;
|
|
1548
|
+
value += this.advance();
|
|
1549
|
+
} else {
|
|
1550
|
+
value += this.advance();
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
this.advance();
|
|
1554
|
+
const token = {
|
|
1555
|
+
type: "STRING",
|
|
1556
|
+
value,
|
|
1557
|
+
line: startLine,
|
|
1558
|
+
column: startColumn,
|
|
1559
|
+
offset: this.pos - (value.length + 2)
|
|
1560
|
+
// approximation for start of string
|
|
1561
|
+
};
|
|
1562
|
+
return token;
|
|
1563
|
+
}
|
|
1564
|
+
readTemplateString() {
|
|
1565
|
+
const startLine = this.line;
|
|
1566
|
+
const startColumn = this.column;
|
|
1567
|
+
const startOff = this.pos;
|
|
1568
|
+
this.advance();
|
|
1569
|
+
let value = "";
|
|
1570
|
+
let braceDepth = 0;
|
|
1571
|
+
while (this.pos < this.source.length) {
|
|
1572
|
+
const c = this.source[this.pos];
|
|
1573
|
+
if (c === "`" && braceDepth === 0) {
|
|
1574
|
+
this.advance();
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1577
|
+
if (c === "$" && this.peek(1) === "{") {
|
|
1578
|
+
braceDepth++;
|
|
1579
|
+
value += "${";
|
|
1580
|
+
this.advance();
|
|
1581
|
+
this.advance();
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (c === "{" && braceDepth > 0) {
|
|
1585
|
+
braceDepth++;
|
|
1586
|
+
value += c;
|
|
1587
|
+
this.advance();
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (c === "}" && braceDepth > 0) {
|
|
1591
|
+
braceDepth--;
|
|
1592
|
+
value += c;
|
|
1593
|
+
this.advance();
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
if (c === "\n") {
|
|
1597
|
+
this.line++;
|
|
1598
|
+
this.column = 0;
|
|
1599
|
+
}
|
|
1600
|
+
value += c;
|
|
1601
|
+
this.advance();
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
type: "TEMPLATE_STRING",
|
|
1605
|
+
value,
|
|
1606
|
+
line: startLine,
|
|
1607
|
+
column: startColumn,
|
|
1608
|
+
offset: startOff
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
readNumber() {
|
|
1612
|
+
const startColumn = this.column;
|
|
1613
|
+
let value = "";
|
|
1614
|
+
if (this.peek() === "-") {
|
|
1615
|
+
value += this.advance();
|
|
1616
|
+
}
|
|
1617
|
+
while (this.isDigit(this.peek())) {
|
|
1618
|
+
value += this.advance();
|
|
1619
|
+
}
|
|
1620
|
+
if (this.peek() === "." && this.isDigit(this.peek(1))) {
|
|
1621
|
+
value += this.advance();
|
|
1622
|
+
while (this.isDigit(this.peek())) {
|
|
1623
|
+
value += this.advance();
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (this.peek() === "e" || this.peek() === "E") {
|
|
1627
|
+
value += this.advance();
|
|
1628
|
+
if (this.peek() === "+" || this.peek() === "-") {
|
|
1629
|
+
value += this.advance();
|
|
1630
|
+
}
|
|
1631
|
+
while (this.isDigit(this.peek())) {
|
|
1632
|
+
value += this.advance();
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
while (this.isAlpha(this.peek()) || this.peek() === "%") {
|
|
1636
|
+
value += this.advance();
|
|
1637
|
+
}
|
|
1638
|
+
const token = {
|
|
1639
|
+
type: "NUMBER",
|
|
1640
|
+
value,
|
|
1641
|
+
line: this.line,
|
|
1642
|
+
column: startColumn,
|
|
1643
|
+
offset: this.pos - value.length
|
|
1644
|
+
};
|
|
1645
|
+
return token;
|
|
1646
|
+
}
|
|
1647
|
+
readExpression() {
|
|
1648
|
+
const startLine = this.line;
|
|
1649
|
+
const startColumn = this.column;
|
|
1650
|
+
this.advance();
|
|
1651
|
+
this.advance();
|
|
1652
|
+
let value = "";
|
|
1653
|
+
let braceDepth = 1;
|
|
1654
|
+
while (braceDepth > 0 && this.pos < this.source.length) {
|
|
1655
|
+
if (this.peek() === "{") {
|
|
1656
|
+
braceDepth++;
|
|
1657
|
+
} else if (this.peek() === "}") {
|
|
1658
|
+
braceDepth--;
|
|
1659
|
+
if (braceDepth === 0) {
|
|
1660
|
+
break;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (this.peek() === "\n") {
|
|
1664
|
+
this.line++;
|
|
1665
|
+
this.column = 0;
|
|
1666
|
+
}
|
|
1667
|
+
value += this.advance();
|
|
1668
|
+
}
|
|
1669
|
+
this.advance();
|
|
1670
|
+
return {
|
|
1671
|
+
type: "EXPRESSION",
|
|
1672
|
+
value: value.trim(),
|
|
1673
|
+
line: startLine,
|
|
1674
|
+
column: startColumn,
|
|
1675
|
+
offset: this.pos - (value.length + 3)
|
|
1676
|
+
// ${...}
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
readIdentifier() {
|
|
1680
|
+
const startColumn = this.column;
|
|
1681
|
+
let value = "";
|
|
1682
|
+
while (this.isIdentifierPart(this.peek())) {
|
|
1683
|
+
value += this.advance();
|
|
1684
|
+
}
|
|
1685
|
+
if (value === "true" || value === "false") {
|
|
1686
|
+
return {
|
|
1687
|
+
type: "BOOLEAN",
|
|
1688
|
+
value,
|
|
1689
|
+
line: this.line,
|
|
1690
|
+
column: startColumn,
|
|
1691
|
+
offset: this.pos - value.length
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
if (value === "null" || value === "none") {
|
|
1695
|
+
return {
|
|
1696
|
+
type: "NULL",
|
|
1697
|
+
value,
|
|
1698
|
+
line: this.line,
|
|
1699
|
+
column: startColumn,
|
|
1700
|
+
offset: this.pos - value.length
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
const token = {
|
|
1704
|
+
type: "IDENTIFIER",
|
|
1705
|
+
value,
|
|
1706
|
+
line: this.line,
|
|
1707
|
+
column: startColumn,
|
|
1708
|
+
offset: this.pos - value.length
|
|
1709
|
+
};
|
|
1710
|
+
switch (value) {
|
|
1711
|
+
case "state_machine":
|
|
1712
|
+
token.type = "STATE_MACHINE";
|
|
1713
|
+
break;
|
|
1714
|
+
case "initial":
|
|
1715
|
+
token.type = "INITIAL";
|
|
1716
|
+
break;
|
|
1717
|
+
case "state":
|
|
1718
|
+
token.type = "STATE";
|
|
1719
|
+
break;
|
|
1720
|
+
case "on_entry":
|
|
1721
|
+
token.type = "ON_ENTRY";
|
|
1722
|
+
break;
|
|
1723
|
+
case "on_exit":
|
|
1724
|
+
token.type = "ON_EXIT";
|
|
1725
|
+
break;
|
|
1726
|
+
case "transition":
|
|
1727
|
+
token.type = "TRANSITION";
|
|
1728
|
+
break;
|
|
1729
|
+
case "match":
|
|
1730
|
+
token.type = "MATCH";
|
|
1731
|
+
break;
|
|
1732
|
+
case "on_error":
|
|
1733
|
+
token.type = "ON_ERROR";
|
|
1734
|
+
break;
|
|
1735
|
+
case "assert":
|
|
1736
|
+
token.type = "ASSERT";
|
|
1737
|
+
break;
|
|
1738
|
+
case "_":
|
|
1739
|
+
token.type = "UNDERSCORE";
|
|
1740
|
+
break;
|
|
1741
|
+
}
|
|
1742
|
+
return token;
|
|
1743
|
+
}
|
|
1744
|
+
isDigit(char) {
|
|
1745
|
+
return char >= "0" && char <= "9";
|
|
1746
|
+
}
|
|
1747
|
+
isAlpha(char) {
|
|
1748
|
+
return char >= "a" && char <= "z" || char >= "A" && char <= "Z";
|
|
1749
|
+
}
|
|
1750
|
+
isIdentifierStart(char) {
|
|
1751
|
+
return this.isAlpha(char) || char === "_" || char === "$";
|
|
1752
|
+
}
|
|
1753
|
+
isIdentifierPart(char) {
|
|
1754
|
+
return this.isIdentifierStart(char) || this.isDigit(char) || char === "-";
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
var HoloScriptPlusParser = class {
|
|
1758
|
+
constructor(options = {}) {
|
|
1759
|
+
this.tokens = [];
|
|
1760
|
+
this.pos = 0;
|
|
1761
|
+
this.source = "";
|
|
1762
|
+
this.errors = [];
|
|
1763
|
+
this.warnings = [];
|
|
1764
|
+
this.imports = [];
|
|
1765
|
+
this.hasState = false;
|
|
1766
|
+
this.hasVRTraits = false;
|
|
1767
|
+
this.hasControlFlow = false;
|
|
1768
|
+
this.compiledExpressions = /* @__PURE__ */ new Map();
|
|
1769
|
+
this.errorRecovery = new ErrorRecovery();
|
|
1770
|
+
/**
|
|
1771
|
+
* Internal incremental parser for parseIncremental.
|
|
1772
|
+
* Uses ChunkBasedIncrementalParser with AST-aware chunking and
|
|
1773
|
+
* dependency tracking — no fallback to full re-parse for unchanged chunks.
|
|
1774
|
+
*/
|
|
1775
|
+
this._incrementalParser = null;
|
|
1776
|
+
/**
|
|
1777
|
+
* Per-cache incremental parser map so callers can supply their own cache
|
|
1778
|
+
* and still get a dedicated ChunkBasedIncrementalParser instance.
|
|
1779
|
+
*/
|
|
1780
|
+
this._incrementalParserCache = /* @__PURE__ */ new Map();
|
|
1781
|
+
this.options = {
|
|
1782
|
+
enableVRTraits: true,
|
|
1783
|
+
enableTypeScriptImports: true,
|
|
1784
|
+
strict: false,
|
|
1785
|
+
...options
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Get quick fixes for all errors in the last parse
|
|
1790
|
+
* Useful for IDE integrations and LSP
|
|
1791
|
+
*/
|
|
1792
|
+
getQuickFixes() {
|
|
1793
|
+
const fixes = /* @__PURE__ */ new Map();
|
|
1794
|
+
for (const error of this.errors) {
|
|
1795
|
+
const errorFixes = generateQuickFixes(
|
|
1796
|
+
{
|
|
1797
|
+
code: error.code,
|
|
1798
|
+
message: error.message,
|
|
1799
|
+
line: error.line,
|
|
1800
|
+
column: error.column,
|
|
1801
|
+
source: this.source
|
|
1802
|
+
},
|
|
1803
|
+
this.source
|
|
1804
|
+
);
|
|
1805
|
+
if (errorFixes.length > 0) {
|
|
1806
|
+
fixes.set(error.line, errorFixes);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return fixes;
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Get enriched errors with additional suggestions from ErrorRecovery
|
|
1813
|
+
*/
|
|
1814
|
+
getEnrichedErrors() {
|
|
1815
|
+
return this.errors.map(
|
|
1816
|
+
(error) => enrichErrorWithSuggestions(
|
|
1817
|
+
{
|
|
1818
|
+
code: error.code,
|
|
1819
|
+
message: error.message,
|
|
1820
|
+
line: error.line,
|
|
1821
|
+
column: error.column,
|
|
1822
|
+
source: this.source
|
|
1823
|
+
},
|
|
1824
|
+
this.source
|
|
1825
|
+
)
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
parse(source) {
|
|
1829
|
+
this.source = source;
|
|
1830
|
+
this.errors = [];
|
|
1831
|
+
this.warnings = [];
|
|
1832
|
+
this.imports = [];
|
|
1833
|
+
this.hasState = false;
|
|
1834
|
+
this.hasVRTraits = false;
|
|
1835
|
+
this.hasControlFlow = false;
|
|
1836
|
+
this.compiledExpressions = /* @__PURE__ */ new Map();
|
|
1837
|
+
this.pos = 0;
|
|
1838
|
+
this.errorRecovery.clear();
|
|
1839
|
+
const lexer = new Lexer(source);
|
|
1840
|
+
this.tokens = lexer.tokenize();
|
|
1841
|
+
const root = this.parseDocument();
|
|
1842
|
+
return this.buildResult(root);
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Performs an incremental parse using ChunkBasedIncrementalParser with
|
|
1846
|
+
* AST-aware chunking, hash-based caching, and dependency tracking.
|
|
1847
|
+
*
|
|
1848
|
+
* Unchanged chunks are served from cache (zero re-parse cost). Only chunks
|
|
1849
|
+
* whose content hash changed — plus chunks that reference changed ones via
|
|
1850
|
+
* `using`, spread, or `@composition` — are re-parsed.
|
|
1851
|
+
*
|
|
1852
|
+
* Previous implementation fell back to a full re-parse per chunk because it
|
|
1853
|
+
* used `ChunkDetector.detect()` inline without the stateful comparison that
|
|
1854
|
+
* `ChunkBasedIncrementalParser` provides. This is now wired through the
|
|
1855
|
+
* superior engine (APL WIT audit gap #3, 2026-05-21).
|
|
1856
|
+
*/
|
|
1857
|
+
parseIncremental(source, cache = globalParseCache) {
|
|
1858
|
+
let incrementalParser = this._incrementalParserCache.get(cache);
|
|
1859
|
+
if (!incrementalParser) {
|
|
1860
|
+
incrementalParser = new ChunkBasedIncrementalParser(cache);
|
|
1861
|
+
this._incrementalParserCache.set(cache, incrementalParser);
|
|
1862
|
+
}
|
|
1863
|
+
const incrementalResult = incrementalParser.parse(source);
|
|
1864
|
+
const chunks = ChunkDetector.detect(source);
|
|
1865
|
+
const children = incrementalResult.ast.type === "fragment" ? incrementalResult.ast.children || [] : [incrementalResult.ast];
|
|
1866
|
+
for (let i = 0; i < children.length; i++) {
|
|
1867
|
+
const chunk = chunks[i];
|
|
1868
|
+
if (chunk && chunk.startLine > 1) {
|
|
1869
|
+
this.offsetNodeLoc(children[i], chunk.startLine - 1);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
this.source = source;
|
|
1873
|
+
this.errors = [];
|
|
1874
|
+
this.warnings = [];
|
|
1875
|
+
this.imports = [];
|
|
1876
|
+
this.hasState = false;
|
|
1877
|
+
this.hasVRTraits = false;
|
|
1878
|
+
this.hasControlFlow = false;
|
|
1879
|
+
this.compiledExpressions = /* @__PURE__ */ new Map();
|
|
1880
|
+
for (const node of children) {
|
|
1881
|
+
if (node && typeof node === "object") {
|
|
1882
|
+
const nodeAny = node;
|
|
1883
|
+
if (nodeAny.traits instanceof Map) {
|
|
1884
|
+
for (const [traitName] of nodeAny.traits) {
|
|
1885
|
+
const name = String(traitName);
|
|
1886
|
+
if (name === "state") this.hasState = true;
|
|
1887
|
+
if (name === "grabbable" || name === "throwable" || name === "hoverable" || name === "clickable" || name === "collidable") {
|
|
1888
|
+
this.hasVRTraits = true;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
const root = incrementalResult.ast.type === "fragment" ? incrementalResult.ast : {
|
|
1895
|
+
type: "fragment",
|
|
1896
|
+
id: "root",
|
|
1897
|
+
properties: {},
|
|
1898
|
+
directives: [],
|
|
1899
|
+
children: [incrementalResult.ast],
|
|
1900
|
+
traits: /* @__PURE__ */ new Map(),
|
|
1901
|
+
loc: incrementalResult.ast.loc || { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
1902
|
+
body: [incrementalResult.ast]
|
|
1903
|
+
};
|
|
1904
|
+
if (root.type === "fragment" && root.loc) {
|
|
1905
|
+
root.loc.start = { line: 1, column: 1 };
|
|
1906
|
+
if (chunks.length > 0) {
|
|
1907
|
+
root.loc.end = { line: chunks[chunks.length - 1].endLine, column: 1 };
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const result = this.buildResult(root);
|
|
1911
|
+
result.incrementalMetrics = {
|
|
1912
|
+
cached: incrementalResult.cached,
|
|
1913
|
+
parsed: incrementalResult.parsed,
|
|
1914
|
+
duration: incrementalResult.duration,
|
|
1915
|
+
changedChunks: incrementalResult.changedChunks
|
|
1916
|
+
};
|
|
1917
|
+
return result;
|
|
1918
|
+
}
|
|
1919
|
+
buildResult(root) {
|
|
1920
|
+
const isFragment = root.type === "fragment";
|
|
1921
|
+
const directives = root.directives || [];
|
|
1922
|
+
let version;
|
|
1923
|
+
const migrations = [];
|
|
1924
|
+
for (const d of directives) {
|
|
1925
|
+
if (d.type === "version") {
|
|
1926
|
+
version = d.version;
|
|
1927
|
+
} else if (d.type === "migrate") {
|
|
1928
|
+
migrations.push({
|
|
1929
|
+
type: "Migration",
|
|
1930
|
+
fromVersion: d.fromVersion,
|
|
1931
|
+
body: d.body
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (version === void 0) version = "1.0";
|
|
1936
|
+
const children = isFragment ? root.children || [] : [root];
|
|
1937
|
+
const worlds = children.filter((c) => c.type === "world");
|
|
1938
|
+
const compositions = children.filter((c) => c.type === "composition");
|
|
1939
|
+
const templates = children.filter((c) => c.type === "template");
|
|
1940
|
+
const npcs = children.filter((c) => c.type === "npc");
|
|
1941
|
+
const ast = {
|
|
1942
|
+
type: "Program",
|
|
1943
|
+
id: "root",
|
|
1944
|
+
properties: isFragment ? root.properties || {} : {},
|
|
1945
|
+
directives,
|
|
1946
|
+
children,
|
|
1947
|
+
worlds,
|
|
1948
|
+
compositions,
|
|
1949
|
+
templates,
|
|
1950
|
+
npcs,
|
|
1951
|
+
traits: isFragment ? root.traits || /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map(),
|
|
1952
|
+
loc: root.loc,
|
|
1953
|
+
body: children,
|
|
1954
|
+
version,
|
|
1955
|
+
migrations: migrations.length > 0 ? migrations : void 0,
|
|
1956
|
+
root,
|
|
1957
|
+
imports: this.imports,
|
|
1958
|
+
hasState: this.hasState,
|
|
1959
|
+
hasVRTraits: this.hasVRTraits,
|
|
1960
|
+
hasControlFlow: this.hasControlFlow
|
|
1961
|
+
};
|
|
1962
|
+
return {
|
|
1963
|
+
success: this.errors.length === 0,
|
|
1964
|
+
ast,
|
|
1965
|
+
compiledExpressions: this.compiledExpressions,
|
|
1966
|
+
requiredCompanions: this.imports.map((i) => i.path),
|
|
1967
|
+
features: {
|
|
1968
|
+
state: this.hasState,
|
|
1969
|
+
vrTraits: this.hasVRTraits,
|
|
1970
|
+
loops: this.hasControlFlow,
|
|
1971
|
+
conditionals: this.hasControlFlow,
|
|
1972
|
+
lifecycleHooks: (root.directives || []).some((d) => d.type === "lifecycle")
|
|
1973
|
+
},
|
|
1974
|
+
warnings: this.warnings,
|
|
1975
|
+
errors: this.errors
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
offsetNodeLoc(node, lineOffset) {
|
|
1979
|
+
if (node.loc) {
|
|
1980
|
+
if (node.loc.start) node.loc.start.line += lineOffset;
|
|
1981
|
+
if (node.loc.end) node.loc.end.line += lineOffset;
|
|
1982
|
+
}
|
|
1983
|
+
if (node.children) {
|
|
1984
|
+
node.children.forEach((child) => this.offsetNodeLoc(child, lineOffset));
|
|
1985
|
+
}
|
|
1986
|
+
if (node.body && Array.isArray(node.body)) {
|
|
1987
|
+
node.body.forEach((child) => this.offsetNodeLoc(child, lineOffset));
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
parseDocument() {
|
|
1991
|
+
this.skipNewlines();
|
|
1992
|
+
const topLevelNodes = [];
|
|
1993
|
+
const globalDirectives = [];
|
|
1994
|
+
while (!this.check("EOF")) {
|
|
1995
|
+
const currentDirectives = [];
|
|
1996
|
+
try {
|
|
1997
|
+
while (this.check("AT")) {
|
|
1998
|
+
const directive = this.parseDirective();
|
|
1999
|
+
if (directive) {
|
|
2000
|
+
const { type } = directive;
|
|
2001
|
+
if (type === "version" || type === "migrate" || type === "import" || type === "export") {
|
|
2002
|
+
globalDirectives.push(directive);
|
|
2003
|
+
if (type === "export") {
|
|
2004
|
+
currentDirectives.push(directive);
|
|
2005
|
+
}
|
|
2006
|
+
} else {
|
|
2007
|
+
currentDirectives.push(directive);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
this.skipNewlines();
|
|
2011
|
+
}
|
|
2012
|
+
const isNodeStart = this.check("IDENTIFIER") || this.check("STATE_MACHINE") || this.check("STATE") || this.check("TRANSITION") || this.check("INITIAL");
|
|
2013
|
+
if (isNodeStart) {
|
|
2014
|
+
const node = this.parseNode();
|
|
2015
|
+
const existingDirectives = node.directives || [];
|
|
2016
|
+
node.directives = [...currentDirectives, ...existingDirectives];
|
|
2017
|
+
if (node.type === "template") {
|
|
2018
|
+
for (const d of currentDirectives) {
|
|
2019
|
+
if (d.type === "version") {
|
|
2020
|
+
node.version = d.version;
|
|
2021
|
+
} else if (d.type === "migrate") {
|
|
2022
|
+
if (!node.migrations) node.migrations = [];
|
|
2023
|
+
node.migrations.push({
|
|
2024
|
+
type: "Migration",
|
|
2025
|
+
fromVersion: d.fromVersion,
|
|
2026
|
+
body: d.body
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
topLevelNodes.push(node);
|
|
2032
|
+
} else {
|
|
2033
|
+
if (currentDirectives.length > 0) {
|
|
2034
|
+
if (this.check("EOF")) {
|
|
2035
|
+
globalDirectives.push(...currentDirectives);
|
|
2036
|
+
} else {
|
|
2037
|
+
this.error(
|
|
2038
|
+
`Expected node after directives, got ${this.current().type}. Valid nodes: orb, template, logic, object, world, composition, scene, group`,
|
|
2039
|
+
"HSP003"
|
|
2040
|
+
);
|
|
2041
|
+
globalDirectives.push(...currentDirectives);
|
|
2042
|
+
}
|
|
2043
|
+
} else if (!this.check("EOF")) {
|
|
2044
|
+
this.error(
|
|
2045
|
+
`Unexpected token ${this.current().type} "${this.current().value}" at top level. Expected: composition, object, world, template, logic, or @directive`,
|
|
2046
|
+
"HSP001"
|
|
2047
|
+
);
|
|
2048
|
+
throw new Error("ParseError");
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (this.check("IDENTIFIER") && this.current().value === "connect") {
|
|
2052
|
+
const connection = this.parseConnectionStatement();
|
|
2053
|
+
const node = {
|
|
2054
|
+
type: "connection",
|
|
2055
|
+
properties: connection,
|
|
2056
|
+
directives: [],
|
|
2057
|
+
children: [],
|
|
2058
|
+
traits: /* @__PURE__ */ new Map(),
|
|
2059
|
+
loc: {
|
|
2060
|
+
start: { line: this.current().line, column: 0 },
|
|
2061
|
+
end: { line: this.current().line, column: 0 }
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
topLevelNodes.push(node);
|
|
2065
|
+
}
|
|
2066
|
+
} catch (e) {
|
|
2067
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2068
|
+
if (errorMessage !== "ParseError" && errorMessage !== "Unexpected token") {
|
|
2069
|
+
console.error(e);
|
|
2070
|
+
}
|
|
2071
|
+
this.synchronize();
|
|
2072
|
+
}
|
|
2073
|
+
this.skipNewlines();
|
|
2074
|
+
}
|
|
2075
|
+
if (topLevelNodes.length === 1 && globalDirectives.length === 0) {
|
|
2076
|
+
return topLevelNodes[0];
|
|
2077
|
+
}
|
|
2078
|
+
return {
|
|
2079
|
+
type: "fragment",
|
|
2080
|
+
id: "root",
|
|
2081
|
+
properties: {},
|
|
2082
|
+
directives: globalDirectives,
|
|
2083
|
+
children: topLevelNodes,
|
|
2084
|
+
traits: /* @__PURE__ */ new Map(),
|
|
2085
|
+
loc: {
|
|
2086
|
+
start: { line: 1, column: 1 },
|
|
2087
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2088
|
+
},
|
|
2089
|
+
body: topLevelNodes
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
parseNode() {
|
|
2093
|
+
const startToken = this.current();
|
|
2094
|
+
const typeToken = this.match([
|
|
2095
|
+
"IDENTIFIER",
|
|
2096
|
+
"STATE_MACHINE",
|
|
2097
|
+
"STATE",
|
|
2098
|
+
"TRANSITION",
|
|
2099
|
+
"INITIAL",
|
|
2100
|
+
"ON_ENTRY",
|
|
2101
|
+
"ON_EXIT"
|
|
2102
|
+
]) || this.expect("IDENTIFIER", "Expected element type");
|
|
2103
|
+
const type = typeToken.value;
|
|
2104
|
+
if (type === "template") {
|
|
2105
|
+
const templateName = this.expect("STRING", "Expected template name").value;
|
|
2106
|
+
const templateBody = this.parseBlockContent();
|
|
2107
|
+
let version;
|
|
2108
|
+
const migrations = [];
|
|
2109
|
+
const directives2 = [];
|
|
2110
|
+
const children2 = [];
|
|
2111
|
+
for (const [key, value] of Object.entries(templateBody)) {
|
|
2112
|
+
if (key === "@version") {
|
|
2113
|
+
const v = value;
|
|
2114
|
+
if (v.type === "version") version = v.version;
|
|
2115
|
+
delete templateBody[key];
|
|
2116
|
+
} else if (key === "@migrate") {
|
|
2117
|
+
const m = value;
|
|
2118
|
+
if (m.type === "migrate") {
|
|
2119
|
+
migrations.push({
|
|
2120
|
+
type: "Migration",
|
|
2121
|
+
fromVersion: m.fromVersion,
|
|
2122
|
+
body: m.body
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
delete templateBody[key];
|
|
2126
|
+
} else if (key.startsWith("@")) {
|
|
2127
|
+
directives2.push(value);
|
|
2128
|
+
delete templateBody[key];
|
|
2129
|
+
} else if (typeof value === "object" && value && "type" in value) {
|
|
2130
|
+
children2.push(value);
|
|
2131
|
+
delete templateBody[key];
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return {
|
|
2135
|
+
type: "template",
|
|
2136
|
+
name: templateName,
|
|
2137
|
+
properties: templateBody,
|
|
2138
|
+
// Now contains only true properties
|
|
2139
|
+
version,
|
|
2140
|
+
migrations,
|
|
2141
|
+
directives: directives2,
|
|
2142
|
+
children: children2,
|
|
2143
|
+
traits: /* @__PURE__ */ new Map(),
|
|
2144
|
+
loc: {
|
|
2145
|
+
start: { line: startToken.line, column: startToken.column },
|
|
2146
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
if (type === "logic") {
|
|
2151
|
+
const logicBody = this.parseLogicBlock();
|
|
2152
|
+
return {
|
|
2153
|
+
type: "logic",
|
|
2154
|
+
name: "logic",
|
|
2155
|
+
id: "logic",
|
|
2156
|
+
properties: {},
|
|
2157
|
+
directives: [],
|
|
2158
|
+
children: [],
|
|
2159
|
+
traits: /* @__PURE__ */ new Map(),
|
|
2160
|
+
body: logicBody,
|
|
2161
|
+
loc: {
|
|
2162
|
+
start: { line: startToken.line, column: startToken.column },
|
|
2163
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
if ([
|
|
2168
|
+
"module",
|
|
2169
|
+
"script",
|
|
2170
|
+
"struct",
|
|
2171
|
+
"enum",
|
|
2172
|
+
"class",
|
|
2173
|
+
"interface",
|
|
2174
|
+
"action",
|
|
2175
|
+
"function",
|
|
2176
|
+
"async",
|
|
2177
|
+
"on"
|
|
2178
|
+
].includes(type)) {
|
|
2179
|
+
let name = "anonymous";
|
|
2180
|
+
if (this.check("IDENTIFIER")) {
|
|
2181
|
+
name = this.advance().value;
|
|
2182
|
+
} else if (this.check("STRING")) {
|
|
2183
|
+
name = this.advance().value;
|
|
2184
|
+
}
|
|
2185
|
+
while (this.check("DOT")) {
|
|
2186
|
+
this.advance();
|
|
2187
|
+
if (this.check("IDENTIFIER")) this.advance();
|
|
2188
|
+
}
|
|
2189
|
+
if (this.check("LPAREN")) {
|
|
2190
|
+
this.skipParens();
|
|
2191
|
+
}
|
|
2192
|
+
let bodyContent = "";
|
|
2193
|
+
if (this.check("LBRACE")) {
|
|
2194
|
+
bodyContent = this.parseRawBlock();
|
|
2195
|
+
}
|
|
2196
|
+
return {
|
|
2197
|
+
type,
|
|
2198
|
+
name,
|
|
2199
|
+
id: name,
|
|
2200
|
+
properties: {},
|
|
2201
|
+
directives: [],
|
|
2202
|
+
children: [],
|
|
2203
|
+
traits: /* @__PURE__ */ new Map(),
|
|
2204
|
+
body: bodyContent,
|
|
2205
|
+
loc: {
|
|
2206
|
+
start: { line: startToken.line, column: startToken.column },
|
|
2207
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
if (startToken.type === "STATE_MACHINE" || type === "state_machine") {
|
|
2212
|
+
return this.parseStateMachine();
|
|
2213
|
+
}
|
|
2214
|
+
if (type === "on_error" || startToken.type === "ON_ERROR") {
|
|
2215
|
+
return this.parseOnErrorNode();
|
|
2216
|
+
}
|
|
2217
|
+
if (type === "assert" || startToken.type === "ASSERT") {
|
|
2218
|
+
return this.parseAssertNode();
|
|
2219
|
+
}
|
|
2220
|
+
if (type === "environment") {
|
|
2221
|
+
const envBody = this.parseEnvironmentBlock();
|
|
2222
|
+
return {
|
|
2223
|
+
type: "environment",
|
|
2224
|
+
properties: envBody.properties,
|
|
2225
|
+
directives: envBody.directives,
|
|
2226
|
+
children: [],
|
|
2227
|
+
traits: /* @__PURE__ */ new Map(),
|
|
2228
|
+
loc: {
|
|
2229
|
+
start: { line: startToken.line, column: startToken.column },
|
|
2230
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
if (type === "composition" || type === "world") {
|
|
2235
|
+
let id2;
|
|
2236
|
+
if (this.check("HASH")) {
|
|
2237
|
+
this.advance();
|
|
2238
|
+
id2 = this.expect("IDENTIFIER", "Expected ID after #").value;
|
|
2239
|
+
}
|
|
2240
|
+
if (this.check("STRING")) {
|
|
2241
|
+
id2 = this.advance().value;
|
|
2242
|
+
}
|
|
2243
|
+
if (this.check("IDENTIFIER")) {
|
|
2244
|
+
id2 = this.advance().value;
|
|
2245
|
+
}
|
|
2246
|
+
const compBody = this.parseCompositionBlock();
|
|
2247
|
+
const traits2 = /* @__PURE__ */ new Map();
|
|
2248
|
+
for (const child of compBody.children) {
|
|
2249
|
+
if (child.type === "fragment" && Array.isArray(child.directives)) {
|
|
2250
|
+
for (const d of child.directives) {
|
|
2251
|
+
if (d.type === "trait") {
|
|
2252
|
+
traits2.set(d.name, d.config);
|
|
2253
|
+
this.hasVRTraits = true;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return {
|
|
2259
|
+
type: type === "composition" ? "composition" : "world",
|
|
2260
|
+
name: id2,
|
|
2261
|
+
id: id2,
|
|
2262
|
+
properties: compBody.properties || {},
|
|
2263
|
+
directives: [],
|
|
2264
|
+
children: compBody.children,
|
|
2265
|
+
traits: traits2,
|
|
2266
|
+
body: compBody,
|
|
2267
|
+
loc: {
|
|
2268
|
+
start: { line: startToken.line, column: startToken.column },
|
|
2269
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
let id;
|
|
2274
|
+
let templateRef;
|
|
2275
|
+
if (this.check("HASH")) {
|
|
2276
|
+
this.advance();
|
|
2277
|
+
id = this.expect("IDENTIFIER", "Expected ID after #").value;
|
|
2278
|
+
}
|
|
2279
|
+
if (this.check("STRING")) {
|
|
2280
|
+
id = this.advance().value;
|
|
2281
|
+
}
|
|
2282
|
+
if (this.check("IDENTIFIER") && this.current().value !== "using") {
|
|
2283
|
+
id = this.advance().value;
|
|
2284
|
+
}
|
|
2285
|
+
if (this.check("LPAREN")) {
|
|
2286
|
+
this.skipParens();
|
|
2287
|
+
}
|
|
2288
|
+
if (this.check("IDENTIFIER") && this.current().value === "using") {
|
|
2289
|
+
this.advance();
|
|
2290
|
+
templateRef = this.expect("STRING", "Expected template name after using").value;
|
|
2291
|
+
}
|
|
2292
|
+
if (this.check("COLON")) {
|
|
2293
|
+
this.advance();
|
|
2294
|
+
if (this.check("IDENTIFIER")) this.advance();
|
|
2295
|
+
if (this.check("LBRACKET")) {
|
|
2296
|
+
this.advance();
|
|
2297
|
+
if (this.check("RBRACKET")) this.advance();
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
const properties = {};
|
|
2301
|
+
const children = [];
|
|
2302
|
+
const directives = [];
|
|
2303
|
+
const traits = /* @__PURE__ */ new Map();
|
|
2304
|
+
if (templateRef) {
|
|
2305
|
+
properties.__templateRef = templateRef;
|
|
2306
|
+
}
|
|
2307
|
+
while (!this.check("LBRACE") && !this.check("EOF")) {
|
|
2308
|
+
try {
|
|
2309
|
+
if (this.check("NEWLINE")) {
|
|
2310
|
+
this.skipNewlines();
|
|
2311
|
+
if (this.check("LBRACE")) break;
|
|
2312
|
+
if (!this.check("AT")) {
|
|
2313
|
+
if (this.check("EOF")) break;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
if (this.check("AT")) {
|
|
2317
|
+
const directive = this.parseDirective();
|
|
2318
|
+
if (directive) {
|
|
2319
|
+
if (directive.type === "trait") {
|
|
2320
|
+
traits.set(directive.name, directive.config);
|
|
2321
|
+
this.hasVRTraits = true;
|
|
2322
|
+
directives.push(directive);
|
|
2323
|
+
} else {
|
|
2324
|
+
directives.push(directive);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
} else if (this.check("SPREAD")) {
|
|
2328
|
+
const startToken2 = this.advance();
|
|
2329
|
+
const targetExpr = this.parseUnary();
|
|
2330
|
+
let target;
|
|
2331
|
+
if (typeof targetExpr === "object" && targetExpr && "__ref" in targetExpr) {
|
|
2332
|
+
target = targetExpr.__ref;
|
|
2333
|
+
} else if (typeof targetExpr === "string") {
|
|
2334
|
+
target = targetExpr;
|
|
2335
|
+
} else {
|
|
2336
|
+
this.error(
|
|
2337
|
+
"Expected identifier or member expression after spread operator (...)",
|
|
2338
|
+
"HSP002"
|
|
2339
|
+
);
|
|
2340
|
+
target = "unknown";
|
|
2341
|
+
}
|
|
2342
|
+
children.push({
|
|
2343
|
+
type: "spread",
|
|
2344
|
+
target,
|
|
2345
|
+
loc: {
|
|
2346
|
+
start: { line: startToken2.line, column: startToken2.column },
|
|
2347
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
} else if (this.check("IDENTIFIER")) {
|
|
2351
|
+
const key = this.advance().value;
|
|
2352
|
+
let value = true;
|
|
2353
|
+
if (this.check("COLON")) {
|
|
2354
|
+
this.advance();
|
|
2355
|
+
value = this.parseValue();
|
|
2356
|
+
}
|
|
2357
|
+
properties[key] = value;
|
|
2358
|
+
} else {
|
|
2359
|
+
if (!this.check("LBRACE")) {
|
|
2360
|
+
this.error(
|
|
2361
|
+
`Unexpected token in properties: ${this.current().type}. Expected property name, @directive, or spread (...)`,
|
|
2362
|
+
"HSP101"
|
|
2363
|
+
);
|
|
2364
|
+
this.synchronizeProperty();
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
} catch (e) {
|
|
2368
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2369
|
+
if (errorMessage !== "ParseError") console.error(e);
|
|
2370
|
+
this.synchronizeProperty();
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
if (this.check("LBRACE")) {
|
|
2374
|
+
this.advance();
|
|
2375
|
+
this.skipNewlines();
|
|
2376
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
2377
|
+
try {
|
|
2378
|
+
this.skipNewlines();
|
|
2379
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
2380
|
+
if (this.check("AT")) {
|
|
2381
|
+
const directive = this.parseDirective();
|
|
2382
|
+
if (directive) {
|
|
2383
|
+
if (directive.type === "trait") {
|
|
2384
|
+
traits.set(directive.name, directive.config);
|
|
2385
|
+
this.hasVRTraits = true;
|
|
2386
|
+
}
|
|
2387
|
+
directives.push(directive);
|
|
2388
|
+
}
|
|
2389
|
+
} else if (this.check("SPREAD")) {
|
|
2390
|
+
const startToken2 = this.advance();
|
|
2391
|
+
const targetExpr = this.parseUnary();
|
|
2392
|
+
let target;
|
|
2393
|
+
if (typeof targetExpr === "object" && targetExpr && "__ref" in targetExpr) {
|
|
2394
|
+
target = targetExpr.__ref;
|
|
2395
|
+
} else if (typeof targetExpr === "string") {
|
|
2396
|
+
target = targetExpr;
|
|
2397
|
+
} else {
|
|
2398
|
+
this.error(
|
|
2399
|
+
"Expected identifier or member expression after spread operator (...)",
|
|
2400
|
+
"HSP002"
|
|
2401
|
+
);
|
|
2402
|
+
target = "unknown";
|
|
2403
|
+
}
|
|
2404
|
+
const spreadNode = {
|
|
2405
|
+
type: "spread",
|
|
2406
|
+
target,
|
|
2407
|
+
loc: {
|
|
2408
|
+
start: { line: startToken2.line, column: startToken2.column },
|
|
2409
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
children.push(spreadNode);
|
|
2413
|
+
const key = "__spread_" + target;
|
|
2414
|
+
properties[key] = spreadNode;
|
|
2415
|
+
} else {
|
|
2416
|
+
const token = this.current();
|
|
2417
|
+
const isKeyToken = token.type === "IDENTIFIER" || token.type === "STRING" || token.type === "HASH" || token.type === "STATE" || token.type === "STATE_MACHINE" || token.type === "INITIAL" || token.type === "ON_ENTRY" || token.type === "ON_EXIT" || token.type === "TRANSITION";
|
|
2418
|
+
if (isKeyToken) {
|
|
2419
|
+
const saved = this.pos;
|
|
2420
|
+
const name = this.advance().value;
|
|
2421
|
+
const childNodeKeywords = [
|
|
2422
|
+
"logic",
|
|
2423
|
+
"template",
|
|
2424
|
+
"environment",
|
|
2425
|
+
"state",
|
|
2426
|
+
"object",
|
|
2427
|
+
"composition",
|
|
2428
|
+
"system",
|
|
2429
|
+
"core_config",
|
|
2430
|
+
"narrative",
|
|
2431
|
+
"quest",
|
|
2432
|
+
"objective",
|
|
2433
|
+
"dialogue",
|
|
2434
|
+
"choice",
|
|
2435
|
+
"visual_metadata",
|
|
2436
|
+
"spatial_group",
|
|
2437
|
+
"scene",
|
|
2438
|
+
"group",
|
|
2439
|
+
"world",
|
|
2440
|
+
"module",
|
|
2441
|
+
"struct",
|
|
2442
|
+
"orb",
|
|
2443
|
+
"on_error",
|
|
2444
|
+
"assert",
|
|
2445
|
+
"transition",
|
|
2446
|
+
"on_entry",
|
|
2447
|
+
"on_exit",
|
|
2448
|
+
"page",
|
|
2449
|
+
"include"
|
|
2450
|
+
];
|
|
2451
|
+
if (name === "transition" && this.check("STRING")) {
|
|
2452
|
+
const event = this.advance().value;
|
|
2453
|
+
if (this.check("ARROW")) {
|
|
2454
|
+
this.advance();
|
|
2455
|
+
const targetState = this.expect("STRING", "Expected target state string").value;
|
|
2456
|
+
let block = {};
|
|
2457
|
+
if (this.check("LBRACE")) {
|
|
2458
|
+
block = this.parseBlockContent();
|
|
2459
|
+
}
|
|
2460
|
+
children.push({
|
|
2461
|
+
type: "transition",
|
|
2462
|
+
name: `${event}_to_${targetState}`,
|
|
2463
|
+
event,
|
|
2464
|
+
target: targetState,
|
|
2465
|
+
guard: block.guard || "",
|
|
2466
|
+
action: block.action || "",
|
|
2467
|
+
block
|
|
2468
|
+
});
|
|
2469
|
+
continue;
|
|
2470
|
+
} else {
|
|
2471
|
+
this.pos = saved;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (name === "on_entry" || name === "on_exit") {
|
|
2475
|
+
let block = "";
|
|
2476
|
+
if (this.check("LBRACE")) {
|
|
2477
|
+
block = this.parseCodeBlock();
|
|
2478
|
+
} else {
|
|
2479
|
+
while (!this.check("RBRACE") && !this.check("EOF")) this.advance();
|
|
2480
|
+
}
|
|
2481
|
+
children.push({
|
|
2482
|
+
type: "method",
|
|
2483
|
+
name,
|
|
2484
|
+
params: [],
|
|
2485
|
+
returnType: "unknown",
|
|
2486
|
+
body: block
|
|
2487
|
+
});
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
if (this.check("LPAREN")) {
|
|
2491
|
+
const possibleMethod = saved;
|
|
2492
|
+
this.pos = saved;
|
|
2493
|
+
const methodName = this.advance().value;
|
|
2494
|
+
const params = [];
|
|
2495
|
+
this.advance();
|
|
2496
|
+
while (!this.check("RPAREN") && !this.check("EOF") && !this.check("LBRACE")) {
|
|
2497
|
+
if (this.check("IDENTIFIER")) {
|
|
2498
|
+
params.push(this.advance().value);
|
|
2499
|
+
} else {
|
|
2500
|
+
this.advance();
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (this.check("RPAREN")) this.advance();
|
|
2504
|
+
let returnType = "unknown";
|
|
2505
|
+
if (this.check("COLON")) {
|
|
2506
|
+
this.advance();
|
|
2507
|
+
if (this.check("IDENTIFIER")) {
|
|
2508
|
+
returnType = this.advance().value;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
if (this.check("LBRACE")) {
|
|
2512
|
+
const body = this.parseCodeBlock();
|
|
2513
|
+
children.push({
|
|
2514
|
+
type: "method",
|
|
2515
|
+
name: methodName,
|
|
2516
|
+
params,
|
|
2517
|
+
returnType,
|
|
2518
|
+
body
|
|
2519
|
+
});
|
|
2520
|
+
continue;
|
|
2521
|
+
} else {
|
|
2522
|
+
this.pos = possibleMethod;
|
|
2523
|
+
this.advance();
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
if (this.check("COLON") || this.check("EQUALS")) {
|
|
2527
|
+
this.advance();
|
|
2528
|
+
properties[name] = this.parseValue();
|
|
2529
|
+
} else if (childNodeKeywords.includes(name) && (this.check("LBRACE") || this.check("STRING"))) {
|
|
2530
|
+
this.pos = saved;
|
|
2531
|
+
children.push(this.parseNode());
|
|
2532
|
+
} else if (this.current().type === "IDENTIFIER") {
|
|
2533
|
+
this.pos = saved;
|
|
2534
|
+
children.push(this.parseNode());
|
|
2535
|
+
} else {
|
|
2536
|
+
properties[name] = true;
|
|
2537
|
+
while (this.check("LPAREN") || this.check("DOT") || this.check("LBRACE")) {
|
|
2538
|
+
if (this.check("LPAREN")) this.skipParens();
|
|
2539
|
+
else if (this.check("LBRACE")) this.skipBraces();
|
|
2540
|
+
else {
|
|
2541
|
+
this.advance();
|
|
2542
|
+
if (this.check("IDENTIFIER")) this.advance();
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
} else if (this.check("COMMA")) {
|
|
2547
|
+
this.advance();
|
|
2548
|
+
} else {
|
|
2549
|
+
this.error(
|
|
2550
|
+
`Unexpected token ${this.current().type} "${this.current().value}" in node body`
|
|
2551
|
+
);
|
|
2552
|
+
this.synchronizeProperty();
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
this.skipNewlines();
|
|
2556
|
+
} catch (e) {
|
|
2557
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2558
|
+
if (errorMessage !== "ParseError") console.error(e);
|
|
2559
|
+
this.synchronizeProperty();
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
this.expect("RBRACE", "Expected }");
|
|
2563
|
+
}
|
|
2564
|
+
return {
|
|
2565
|
+
type,
|
|
2566
|
+
name: id,
|
|
2567
|
+
// Mapping id to name for runtime compatibility
|
|
2568
|
+
id,
|
|
2569
|
+
properties,
|
|
2570
|
+
directives,
|
|
2571
|
+
children,
|
|
2572
|
+
traits,
|
|
2573
|
+
loc: {
|
|
2574
|
+
start: { line: startToken.line, column: startToken.column },
|
|
2575
|
+
end: { line: this.current().line, column: this.current().column }
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
parseDirective() {
|
|
2580
|
+
this.expect("AT", "Expected @");
|
|
2581
|
+
const nameToken = this.current();
|
|
2582
|
+
const isKeyword = ["STATE_MACHINE", "STATE", "ON_ENTRY", "ON_EXIT", "TRANSITION"].includes(
|
|
2583
|
+
nameToken.type
|
|
2584
|
+
);
|
|
2585
|
+
if (nameToken.type !== "IDENTIFIER" && !isKeyword) {
|
|
2586
|
+
this.error(
|
|
2587
|
+
`Expected directive name, got ${nameToken.type}. Directives start with @ followed by name (e.g., @grabbable)`,
|
|
2588
|
+
"HSP201"
|
|
2589
|
+
);
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
this.advance();
|
|
2593
|
+
const name = nameToken.value;
|
|
2594
|
+
if (name === "version") {
|
|
2595
|
+
this.expect("LPAREN", "Expected ( after @version");
|
|
2596
|
+
const versionToken = this.expect("NUMBER", "Expected version number");
|
|
2597
|
+
const version = Number(versionToken.value);
|
|
2598
|
+
this.expect("RPAREN", "Expected ) after version number");
|
|
2599
|
+
return { type: "version", version };
|
|
2600
|
+
}
|
|
2601
|
+
if (name === "migrate") {
|
|
2602
|
+
const fromToken = this.expect("IDENTIFIER", 'Expected "from" after @migrate');
|
|
2603
|
+
if (fromToken.value !== "from") {
|
|
2604
|
+
this.error('Expected "from" after @migrate');
|
|
2605
|
+
}
|
|
2606
|
+
this.expect("LPAREN", "Expected ( after from");
|
|
2607
|
+
const fromVersionToken = this.expect("NUMBER", "Expected version number");
|
|
2608
|
+
const fromVersion = Number(fromVersionToken.value);
|
|
2609
|
+
this.expect("RPAREN", "Expected ) after version number");
|
|
2610
|
+
const body = this.check("LBRACE") ? this.parseCodeBlock() : "";
|
|
2611
|
+
return { type: "migrate", fromVersion, body };
|
|
2612
|
+
}
|
|
2613
|
+
if (VR_TRAITS.includes(name)) {
|
|
2614
|
+
if (!this.options.enableVRTraits) {
|
|
2615
|
+
this.warn(`VR trait @${name} is disabled`);
|
|
2616
|
+
return null;
|
|
2617
|
+
}
|
|
2618
|
+
let config2 = {};
|
|
2619
|
+
if (this.check("LPAREN")) {
|
|
2620
|
+
config2 = this.parseTraitConfig();
|
|
2621
|
+
} else if (this.check("LBRACE")) {
|
|
2622
|
+
config2 = this.parseBlockContent();
|
|
2623
|
+
}
|
|
2624
|
+
return this.parseTraitSumTail({ type: "trait", name, config: config2 });
|
|
2625
|
+
}
|
|
2626
|
+
if (LIFECYCLE_HOOKS.includes(name)) {
|
|
2627
|
+
const params = [];
|
|
2628
|
+
if (this.check("LPAREN")) {
|
|
2629
|
+
this.advance();
|
|
2630
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
2631
|
+
params.push(this.expect("IDENTIFIER", "Expected parameter name").value);
|
|
2632
|
+
if (this.check("COLON")) {
|
|
2633
|
+
this.advance();
|
|
2634
|
+
this.expect("IDENTIFIER", "Expected type");
|
|
2635
|
+
}
|
|
2636
|
+
if (this.check("COMMA")) this.advance();
|
|
2637
|
+
}
|
|
2638
|
+
this.expect("RPAREN", "Expected )");
|
|
2639
|
+
}
|
|
2640
|
+
if (this.check("COLON")) {
|
|
2641
|
+
this.advance();
|
|
2642
|
+
if (this.check("LPAREN")) this.skipParens();
|
|
2643
|
+
if (this.check("ARROW")) this.advance();
|
|
2644
|
+
}
|
|
2645
|
+
let body = "";
|
|
2646
|
+
if (this.check("ARROW")) {
|
|
2647
|
+
this.advance();
|
|
2648
|
+
body = this.parseInlineExpression();
|
|
2649
|
+
} else if (this.check("LBRACE")) {
|
|
2650
|
+
body = this.parseCodeBlock();
|
|
2651
|
+
}
|
|
2652
|
+
return {
|
|
2653
|
+
type: "lifecycle",
|
|
2654
|
+
hook: name,
|
|
2655
|
+
params,
|
|
2656
|
+
body
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
if (name === "state") {
|
|
2660
|
+
this.hasState = true;
|
|
2661
|
+
const body = this.parseStateBlock();
|
|
2662
|
+
return { type: "state", body };
|
|
2663
|
+
}
|
|
2664
|
+
if (name === "bindings") {
|
|
2665
|
+
const bindings = this.parseBindingsBlock();
|
|
2666
|
+
return { type: "bindings", bindings };
|
|
2667
|
+
}
|
|
2668
|
+
if (name === "for") {
|
|
2669
|
+
this.hasControlFlow = true;
|
|
2670
|
+
const variable = this.expect("IDENTIFIER", "Expected variable name").value;
|
|
2671
|
+
this.expect("IDENTIFIER", 'Expected "in"');
|
|
2672
|
+
const iterable = this.parseInlineExpression();
|
|
2673
|
+
const body = this.parseControlFlowBody();
|
|
2674
|
+
return { type: "for", variable, iterable, body };
|
|
2675
|
+
}
|
|
2676
|
+
if (name === "forEach") {
|
|
2677
|
+
this.hasControlFlow = true;
|
|
2678
|
+
const variable = this.expect("IDENTIFIER", "Expected variable name").value;
|
|
2679
|
+
this.expect("IDENTIFIER", 'Expected "in"');
|
|
2680
|
+
const collection = this.parseInlineExpression();
|
|
2681
|
+
const body = this.parseControlFlowBody();
|
|
2682
|
+
return { type: "forEach", variable, collection, body };
|
|
2683
|
+
}
|
|
2684
|
+
if (name === "while") {
|
|
2685
|
+
this.hasControlFlow = true;
|
|
2686
|
+
const condition = this.parseInlineExpression();
|
|
2687
|
+
const body = this.parseControlFlowBody();
|
|
2688
|
+
return { type: "while", condition, body };
|
|
2689
|
+
}
|
|
2690
|
+
if (name === "if") {
|
|
2691
|
+
this.hasControlFlow = true;
|
|
2692
|
+
const condition = this.parseInlineExpression();
|
|
2693
|
+
const body = this.parseControlFlowBody();
|
|
2694
|
+
let elseBody;
|
|
2695
|
+
this.skipNewlines();
|
|
2696
|
+
if (this.check("AT")) {
|
|
2697
|
+
const saved = this.pos;
|
|
2698
|
+
this.advance();
|
|
2699
|
+
if (this.check("IDENTIFIER") && this.current().value === "else") {
|
|
2700
|
+
this.advance();
|
|
2701
|
+
elseBody = this.parseControlFlowBody();
|
|
2702
|
+
} else {
|
|
2703
|
+
this.pos = saved;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
return { type: "if", condition, body, else: elseBody };
|
|
2707
|
+
}
|
|
2708
|
+
if (name === "import") {
|
|
2709
|
+
if (!this.options.enableTypeScriptImports) {
|
|
2710
|
+
this.warn("@import is disabled");
|
|
2711
|
+
return null;
|
|
2712
|
+
}
|
|
2713
|
+
let namedImports;
|
|
2714
|
+
let isWildcard = false;
|
|
2715
|
+
if (this.check("LBRACE")) {
|
|
2716
|
+
this.advance();
|
|
2717
|
+
namedImports = [];
|
|
2718
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
2719
|
+
if (this.check("IDENTIFIER")) {
|
|
2720
|
+
namedImports.push(this.advance().value);
|
|
2721
|
+
}
|
|
2722
|
+
if (this.check("COMMA")) this.advance();
|
|
2723
|
+
}
|
|
2724
|
+
this.expect("RBRACE", "Expected } in named import list");
|
|
2725
|
+
if (this.check("IDENTIFIER") && this.current().value === "from") {
|
|
2726
|
+
this.advance();
|
|
2727
|
+
} else {
|
|
2728
|
+
this.warn("Expected 'from' after named import specifiers");
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
if (this.check("ASTERISK") || this.check("IDENTIFIER") && this.current().value === "*") {
|
|
2732
|
+
this.advance();
|
|
2733
|
+
isWildcard = true;
|
|
2734
|
+
if (this.check("IDENTIFIER") && this.current().value === "as") {
|
|
2735
|
+
this.advance();
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
const path = this.expect("STRING", "Expected import path string").value;
|
|
2739
|
+
let alias = path.split("/").pop()?.replace(/\.[^.]+$/, "") || "import";
|
|
2740
|
+
if (!namedImports && this.check("IDENTIFIER") && this.current().value === "as") {
|
|
2741
|
+
this.advance();
|
|
2742
|
+
alias = this.expect("IDENTIFIER", "Expected alias after as").value;
|
|
2743
|
+
}
|
|
2744
|
+
if (isWildcard && this.check("IDENTIFIER") && this.current().value !== "from") {
|
|
2745
|
+
alias = this.advance().value;
|
|
2746
|
+
if (this.check("IDENTIFIER") && this.current().value === "from") {
|
|
2747
|
+
this.advance();
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
this.imports.push({ path, alias, namedImports, isWildcard });
|
|
2751
|
+
return {
|
|
2752
|
+
type: "import",
|
|
2753
|
+
path,
|
|
2754
|
+
alias,
|
|
2755
|
+
namedImports,
|
|
2756
|
+
isWildcard
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
if (name === "export") {
|
|
2760
|
+
let exportKind = "any";
|
|
2761
|
+
if (this.check("IDENTIFIER")) {
|
|
2762
|
+
const kindToken = this.current();
|
|
2763
|
+
const knownKinds = ["template", "object", "composition", "trait", "group", "scene"];
|
|
2764
|
+
if (knownKinds.includes(kindToken.value)) {
|
|
2765
|
+
exportKind = this.advance().value;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
let exportName;
|
|
2769
|
+
if (this.check("STRING")) {
|
|
2770
|
+
exportName = this.advance().value;
|
|
2771
|
+
} else if (this.check("IDENTIFIER")) {
|
|
2772
|
+
exportName = this.advance().value;
|
|
2773
|
+
}
|
|
2774
|
+
return {
|
|
2775
|
+
type: "export",
|
|
2776
|
+
exportKind,
|
|
2777
|
+
exportName
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
if (name === "manifest") {
|
|
2781
|
+
let config2 = {};
|
|
2782
|
+
let manifestName = "default";
|
|
2783
|
+
if (this.check("LPAREN")) {
|
|
2784
|
+
this.advance();
|
|
2785
|
+
if (this.check("STRING")) {
|
|
2786
|
+
manifestName = this.advance().value;
|
|
2787
|
+
}
|
|
2788
|
+
while (!this.check("RPAREN") && !this.check("EOF")) this.advance();
|
|
2789
|
+
this.expect("RPAREN", "Expected )");
|
|
2790
|
+
} else if (this.check("STRING")) {
|
|
2791
|
+
manifestName = this.advance().value;
|
|
2792
|
+
}
|
|
2793
|
+
if (this.check("LBRACE")) {
|
|
2794
|
+
const block = this.parseBlockContent();
|
|
2795
|
+
config2 = { ...config2, ...block };
|
|
2796
|
+
}
|
|
2797
|
+
return { type: "manifest", name: manifestName, ...config2 };
|
|
2798
|
+
}
|
|
2799
|
+
if (name === "asset") {
|
|
2800
|
+
if (this.check("LPAREN")) {
|
|
2801
|
+
const config2 = this.parseTraitConfig();
|
|
2802
|
+
return { ...config2, type: "asset" };
|
|
2803
|
+
}
|
|
2804
|
+
const assetId = this.expect("STRING", "Expected asset ID").value;
|
|
2805
|
+
return { type: "asset", id: assetId };
|
|
2806
|
+
}
|
|
2807
|
+
if (name === "semantic") {
|
|
2808
|
+
const semanticName = this.check("LPAREN") ? this.parseParenString() : this.expect("STRING", "Expected semantic name").value;
|
|
2809
|
+
const config2 = this.parseBlockContent();
|
|
2810
|
+
return { ...config2, type: "semantic", name: semanticName };
|
|
2811
|
+
}
|
|
2812
|
+
if (name === "annotate") {
|
|
2813
|
+
const annotateName = this.check("LPAREN") ? this.parseParenString() : this.expect("STRING", "Expected annotation type").value;
|
|
2814
|
+
let config2 = {};
|
|
2815
|
+
if (this.check("COMMA")) {
|
|
2816
|
+
this.advance();
|
|
2817
|
+
config2 = this.parseValue();
|
|
2818
|
+
if (this.check("RPAREN")) this.advance();
|
|
2819
|
+
} else if (this.check("RPAREN")) {
|
|
2820
|
+
this.advance();
|
|
2821
|
+
}
|
|
2822
|
+
return { type: "annotate", annotationType: annotateName, config: config2 };
|
|
2823
|
+
}
|
|
2824
|
+
if (name === "semantic_ref") {
|
|
2825
|
+
const refName = this.check("LPAREN") ? this.parseParenString() : this.expect("STRING", "Expected semantic reference").value;
|
|
2826
|
+
return { type: "semantic_ref", ref: refName };
|
|
2827
|
+
}
|
|
2828
|
+
if (name === "bindings") {
|
|
2829
|
+
const bindings = this.parseBindingsBlock();
|
|
2830
|
+
return { type: "bindings", bindings };
|
|
2831
|
+
}
|
|
2832
|
+
if (name === "world_metadata") {
|
|
2833
|
+
const config2 = this.parseBlockContent();
|
|
2834
|
+
return { ...config2, type: "world_metadata" };
|
|
2835
|
+
}
|
|
2836
|
+
if (name === "world_config") {
|
|
2837
|
+
const config2 = this.parseBlockContent();
|
|
2838
|
+
return { ...config2, type: "world_config" };
|
|
2839
|
+
}
|
|
2840
|
+
if (name === "zones") {
|
|
2841
|
+
const zones = this.parseNamedBlockList("zone");
|
|
2842
|
+
return { type: "zones", zones };
|
|
2843
|
+
}
|
|
2844
|
+
if (name === "spawn_points") {
|
|
2845
|
+
const spawns = this.parseNamedBlockList("spawn");
|
|
2846
|
+
return { type: "spawn_points", spawns };
|
|
2847
|
+
}
|
|
2848
|
+
if (name === "skybox") {
|
|
2849
|
+
const config2 = this.parseBlockContent();
|
|
2850
|
+
return { ...config2, type: "skybox" };
|
|
2851
|
+
}
|
|
2852
|
+
if (name === "ambient_light") {
|
|
2853
|
+
const config2 = this.parseBlockContent();
|
|
2854
|
+
return { ...config2, type: "ambient_light" };
|
|
2855
|
+
}
|
|
2856
|
+
if (name === "directional_light") {
|
|
2857
|
+
let lightName = "default";
|
|
2858
|
+
if (this.check("LPAREN")) {
|
|
2859
|
+
lightName = this.parseParenString();
|
|
2860
|
+
}
|
|
2861
|
+
const config2 = this.parseBlockContent();
|
|
2862
|
+
return { ...config2, type: "directional_light", name: lightName };
|
|
2863
|
+
}
|
|
2864
|
+
if (name === "fog") {
|
|
2865
|
+
const config2 = this.parseBlockContent();
|
|
2866
|
+
return { ...config2, type: "fog" };
|
|
2867
|
+
}
|
|
2868
|
+
if (name === "artwork_metadata") {
|
|
2869
|
+
const config2 = this.parseBlockContent();
|
|
2870
|
+
return { ...config2, type: "artwork_metadata" };
|
|
2871
|
+
}
|
|
2872
|
+
if (name === "npc_behavior") {
|
|
2873
|
+
const config2 = this.parseBlockContent();
|
|
2874
|
+
return { ...config2, type: "npc_behavior" };
|
|
2875
|
+
}
|
|
2876
|
+
if (name === "interactive") {
|
|
2877
|
+
const config2 = this.parseBlockContent();
|
|
2878
|
+
return { ...config2, type: "interactive" };
|
|
2879
|
+
}
|
|
2880
|
+
if (name === "lod") {
|
|
2881
|
+
const config2 = this.parseBlockContent();
|
|
2882
|
+
return { ...config2, type: "lod" };
|
|
2883
|
+
}
|
|
2884
|
+
if (name === "external_api") {
|
|
2885
|
+
let externalApiName = "default";
|
|
2886
|
+
if (this.check("STRING")) {
|
|
2887
|
+
externalApiName = this.advance().value;
|
|
2888
|
+
} else if (this.check("IDENTIFIER")) {
|
|
2889
|
+
externalApiName = this.advance().value;
|
|
2890
|
+
}
|
|
2891
|
+
const config2 = this.check("LBRACE") ? this.parseBlockContent() : this.parseTraitConfig();
|
|
2892
|
+
return {
|
|
2893
|
+
type: "external_api",
|
|
2894
|
+
name: externalApiName,
|
|
2895
|
+
...config2
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
if (name === "generate") {
|
|
2899
|
+
const config2 = this.parseTraitConfig();
|
|
2900
|
+
const prompt = config2.prompt || "";
|
|
2901
|
+
const context = config2.context || "";
|
|
2902
|
+
const target = config2.target || "children";
|
|
2903
|
+
return { type: "generate", prompt, context, target };
|
|
2904
|
+
}
|
|
2905
|
+
if (name === "npc") {
|
|
2906
|
+
const npcName = this.expect("STRING", "Expected NPC name").value;
|
|
2907
|
+
const props = this.parsePropsBlock();
|
|
2908
|
+
return { type: "npc", name: npcName, props };
|
|
2909
|
+
}
|
|
2910
|
+
if (name === "dialog") {
|
|
2911
|
+
const dialogName = this.expect("STRING", "Expected dialog name").value;
|
|
2912
|
+
const { props, options } = this.parseDialogBlock();
|
|
2913
|
+
return { type: "dialog", name: dialogName, props, options };
|
|
2914
|
+
}
|
|
2915
|
+
if (name === "hololand") {
|
|
2916
|
+
if (this.check("DOT")) {
|
|
2917
|
+
this.advance();
|
|
2918
|
+
const eventName = this.expect("IDENTIFIER", "Expected event name").value;
|
|
2919
|
+
const params = [];
|
|
2920
|
+
if (this.check("LPAREN")) {
|
|
2921
|
+
this.advance();
|
|
2922
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
2923
|
+
params.push(this.expect("IDENTIFIER", "Expected parameter").value);
|
|
2924
|
+
if (this.check("COLON")) {
|
|
2925
|
+
this.advance();
|
|
2926
|
+
this.expect("IDENTIFIER", "Expected type");
|
|
2927
|
+
}
|
|
2928
|
+
if (this.check("COMMA")) this.advance();
|
|
2929
|
+
}
|
|
2930
|
+
this.expect("RPAREN", "Expected )");
|
|
2931
|
+
}
|
|
2932
|
+
return { type: "hololand_event", event: eventName, params };
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
if (STRUCTURAL_DIRECTIVES.includes(name)) {
|
|
2936
|
+
let nodeName;
|
|
2937
|
+
if (this.check("STRING")) {
|
|
2938
|
+
nodeName = this.advance().value;
|
|
2939
|
+
}
|
|
2940
|
+
const config2 = this.check("LBRACE") ? this.parseBlockContent() : this.parseTraitConfig();
|
|
2941
|
+
return { ...config2, type: name, name: nodeName };
|
|
2942
|
+
}
|
|
2943
|
+
if (this.options.strict) {
|
|
2944
|
+
this.traitError(name);
|
|
2945
|
+
} else {
|
|
2946
|
+
this.warn(`Unknown directive @${name}`);
|
|
2947
|
+
}
|
|
2948
|
+
let config = {};
|
|
2949
|
+
if (this.check("LPAREN")) {
|
|
2950
|
+
config = this.parseTraitConfig();
|
|
2951
|
+
} else if (this.check("LBRACE")) {
|
|
2952
|
+
config = this.parseBlockContent();
|
|
2953
|
+
}
|
|
2954
|
+
if (this.check("COLON")) {
|
|
2955
|
+
this.advance();
|
|
2956
|
+
if (this.check("LPAREN")) this.skipParens();
|
|
2957
|
+
if (this.check("ARROW")) this.advance();
|
|
2958
|
+
if (this.check("LBRACE")) {
|
|
2959
|
+
config.body = this.parseCodeBlock();
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
return this.parseTraitSumTail({ type: "trait", name, config });
|
|
2963
|
+
}
|
|
2964
|
+
parseTraitSumTail(first) {
|
|
2965
|
+
if (!this.check("PLUS")) {
|
|
2966
|
+
return first;
|
|
2967
|
+
}
|
|
2968
|
+
const alternatives = [
|
|
2969
|
+
{ type: "trait_atom", name: first.name, config: first.config ?? {} }
|
|
2970
|
+
];
|
|
2971
|
+
while (this.check("PLUS")) {
|
|
2972
|
+
this.advance();
|
|
2973
|
+
alternatives.push(this.parseTraitAtom());
|
|
2974
|
+
}
|
|
2975
|
+
return {
|
|
2976
|
+
type: "trait_sum",
|
|
2977
|
+
operation: "additive",
|
|
2978
|
+
alternatives
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
parseTraitAtom() {
|
|
2982
|
+
this.expect("AT", "Expected @ in trait sum alternative");
|
|
2983
|
+
const name = this.expect("IDENTIFIER", "Expected trait name in trait sum alternative").value;
|
|
2984
|
+
let config = {};
|
|
2985
|
+
if (this.check("LPAREN")) {
|
|
2986
|
+
config = this.parseTraitConfig();
|
|
2987
|
+
} else if (this.check("LBRACE")) {
|
|
2988
|
+
config = this.parseBlockContent();
|
|
2989
|
+
}
|
|
2990
|
+
return { type: "trait_atom", name, config };
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Parse a string inside parentheses: ("name")
|
|
2994
|
+
*/
|
|
2995
|
+
parseParenString() {
|
|
2996
|
+
this.expect("LPAREN", "Expected (");
|
|
2997
|
+
const value = this.expect("STRING", "Expected string").value;
|
|
2998
|
+
this.expect("RPAREN", "Expected )");
|
|
2999
|
+
return value;
|
|
3000
|
+
}
|
|
3001
|
+
parseConnectionStatement() {
|
|
3002
|
+
this.expect("IDENTIFIER", "Expected connect");
|
|
3003
|
+
const from = this.expect("IDENTIFIER", "Expected from name").value;
|
|
3004
|
+
this.expect("IDENTIFIER", "Expected to");
|
|
3005
|
+
const to = this.expect("IDENTIFIER", "Expected to name").value;
|
|
3006
|
+
return { from, to };
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Parse block content: { key: value, ... }
|
|
3010
|
+
*/
|
|
3011
|
+
parseBlockContent() {
|
|
3012
|
+
const content = {};
|
|
3013
|
+
if (!this.check("LBRACE")) {
|
|
3014
|
+
return content;
|
|
3015
|
+
}
|
|
3016
|
+
this.advance();
|
|
3017
|
+
this.skipNewlines();
|
|
3018
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3019
|
+
this.skipNewlines();
|
|
3020
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3021
|
+
this.skipNewlines();
|
|
3022
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3023
|
+
const token = this.current();
|
|
3024
|
+
const isKeyToken = token.type === "IDENTIFIER" || token.type === "STRING" || token.type === "STATE" || token.type === "STATE_MACHINE" || token.type === "INITIAL" || token.type === "ON_ENTRY" || token.type === "ON_EXIT" || token.type === "TRANSITION";
|
|
3025
|
+
if (isKeyToken) {
|
|
3026
|
+
const next = this.peek(1);
|
|
3027
|
+
if (next.type === "COLON" || next.type === "EQUALS") {
|
|
3028
|
+
const key = this.advance().value;
|
|
3029
|
+
this.advance();
|
|
3030
|
+
content[key] = this.parseValue();
|
|
3031
|
+
} else if (next.type === "STRING" || next.type === "LBRACE" || next.type === "IDENTIFIER") {
|
|
3032
|
+
const node = this.parseNode();
|
|
3033
|
+
const type = node.type;
|
|
3034
|
+
const name = node.name || `unnamed_${type}_${Object.keys(content).length}`;
|
|
3035
|
+
content[name] = node;
|
|
3036
|
+
} else {
|
|
3037
|
+
const key = this.advance().value;
|
|
3038
|
+
content[key] = true;
|
|
3039
|
+
}
|
|
3040
|
+
} else if (this.check("SPREAD")) {
|
|
3041
|
+
this.advance();
|
|
3042
|
+
const val = this.parseValue();
|
|
3043
|
+
const spreadKey = `__spread_${Object.keys(content).length}`;
|
|
3044
|
+
content[spreadKey] = { type: "spread", argument: val };
|
|
3045
|
+
} else if (this.check("AT")) {
|
|
3046
|
+
const directive = this.parseDirective();
|
|
3047
|
+
if (directive) {
|
|
3048
|
+
const dirKey = ("name" in directive ? directive.name : void 0) || directive.type;
|
|
3049
|
+
content[`@${dirKey}`] = directive;
|
|
3050
|
+
}
|
|
3051
|
+
} else if (this.check("LBRACE")) {
|
|
3052
|
+
let depth = 1;
|
|
3053
|
+
this.advance();
|
|
3054
|
+
while (depth > 0 && !this.check("EOF")) {
|
|
3055
|
+
if (this.check("LBRACE")) depth++;
|
|
3056
|
+
if (this.check("RBRACE")) depth--;
|
|
3057
|
+
this.advance();
|
|
3058
|
+
}
|
|
3059
|
+
} else if (this.check("LPAREN")) {
|
|
3060
|
+
this.skipParens();
|
|
3061
|
+
} else {
|
|
3062
|
+
this.advance();
|
|
3063
|
+
}
|
|
3064
|
+
if (this.check("COMMA")) this.advance();
|
|
3065
|
+
this.skipNewlines();
|
|
3066
|
+
}
|
|
3067
|
+
this.expect("RBRACE", "Expected }");
|
|
3068
|
+
return content;
|
|
3069
|
+
}
|
|
3070
|
+
/**
|
|
3071
|
+
* Parse bindings block: { bind(expr) -> target, ... }
|
|
3072
|
+
*/
|
|
3073
|
+
parseBindingsBlock() {
|
|
3074
|
+
const bindings = [];
|
|
3075
|
+
if (!this.check("LBRACE")) {
|
|
3076
|
+
return bindings;
|
|
3077
|
+
}
|
|
3078
|
+
this.advance();
|
|
3079
|
+
this.skipNewlines();
|
|
3080
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3081
|
+
this.skipNewlines();
|
|
3082
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3083
|
+
if (this.check("IDENTIFIER") && this.current().value === "bind") {
|
|
3084
|
+
this.advance();
|
|
3085
|
+
if (this.check("LPAREN")) {
|
|
3086
|
+
this.advance();
|
|
3087
|
+
let source = "";
|
|
3088
|
+
let parenDepth = 1;
|
|
3089
|
+
while (parenDepth > 0 && !this.check("EOF")) {
|
|
3090
|
+
if (this.check("LPAREN")) parenDepth++;
|
|
3091
|
+
if (this.check("RPAREN")) {
|
|
3092
|
+
parenDepth--;
|
|
3093
|
+
if (parenDepth === 0) break;
|
|
3094
|
+
}
|
|
3095
|
+
source += this.advance().value + " ";
|
|
3096
|
+
}
|
|
3097
|
+
this.expect("RPAREN", "Expected )");
|
|
3098
|
+
source = source.trim();
|
|
3099
|
+
if (this.check("ARROW")) {
|
|
3100
|
+
this.advance();
|
|
3101
|
+
}
|
|
3102
|
+
let target = "";
|
|
3103
|
+
while (!this.check("NEWLINE") && !this.check("RBRACE") && !this.check("EOF")) {
|
|
3104
|
+
target += this.advance().value;
|
|
3105
|
+
}
|
|
3106
|
+
target = target.trim();
|
|
3107
|
+
bindings.push({ source, target });
|
|
3108
|
+
}
|
|
3109
|
+
} else {
|
|
3110
|
+
while (!this.check("NEWLINE") && !this.check("RBRACE") && !this.check("EOF")) {
|
|
3111
|
+
this.advance();
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
this.skipNewlines();
|
|
3115
|
+
}
|
|
3116
|
+
this.expect("RBRACE", "Expected }");
|
|
3117
|
+
return bindings;
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Parse named block list: zone "name" { ... } or spawn "name" { ... }
|
|
3121
|
+
*/
|
|
3122
|
+
parseNamedBlockList(blockType) {
|
|
3123
|
+
const blocks = [];
|
|
3124
|
+
if (!this.check("LBRACE")) {
|
|
3125
|
+
return blocks;
|
|
3126
|
+
}
|
|
3127
|
+
this.advance();
|
|
3128
|
+
this.skipNewlines();
|
|
3129
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3130
|
+
this.skipNewlines();
|
|
3131
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3132
|
+
if (this.check("IDENTIFIER") && this.current().value === blockType) {
|
|
3133
|
+
this.advance();
|
|
3134
|
+
const blockName = this.expect("STRING", `Expected ${blockType} name`).value;
|
|
3135
|
+
const config = this.parseBlockContent();
|
|
3136
|
+
blocks.push({ name: blockName, config });
|
|
3137
|
+
} else {
|
|
3138
|
+
this.advance();
|
|
3139
|
+
}
|
|
3140
|
+
this.skipNewlines();
|
|
3141
|
+
}
|
|
3142
|
+
this.expect("RBRACE", "Expected }");
|
|
3143
|
+
return blocks;
|
|
3144
|
+
}
|
|
3145
|
+
/**
|
|
3146
|
+
* Parse composition block: composition "Name" { ... }
|
|
3147
|
+
*/
|
|
3148
|
+
parseCompositionBlock() {
|
|
3149
|
+
const result = {
|
|
3150
|
+
systems: [],
|
|
3151
|
+
configs: [],
|
|
3152
|
+
children: [],
|
|
3153
|
+
properties: {}
|
|
3154
|
+
};
|
|
3155
|
+
if (!this.check("LBRACE")) {
|
|
3156
|
+
return result;
|
|
3157
|
+
}
|
|
3158
|
+
this.advance();
|
|
3159
|
+
this.skipNewlines();
|
|
3160
|
+
const childNodeKeywords = [
|
|
3161
|
+
"logic",
|
|
3162
|
+
"template",
|
|
3163
|
+
"environment",
|
|
3164
|
+
"state",
|
|
3165
|
+
"object",
|
|
3166
|
+
"composition",
|
|
3167
|
+
"system",
|
|
3168
|
+
"core_config",
|
|
3169
|
+
"narrative",
|
|
3170
|
+
"quest",
|
|
3171
|
+
"objective",
|
|
3172
|
+
"dialogue",
|
|
3173
|
+
"choice",
|
|
3174
|
+
"visual_metadata",
|
|
3175
|
+
"spatial_group",
|
|
3176
|
+
"scene",
|
|
3177
|
+
"group",
|
|
3178
|
+
"world",
|
|
3179
|
+
"module",
|
|
3180
|
+
"struct",
|
|
3181
|
+
"orb",
|
|
3182
|
+
"on_error",
|
|
3183
|
+
"assert",
|
|
3184
|
+
"topic",
|
|
3185
|
+
"channel",
|
|
3186
|
+
"config",
|
|
3187
|
+
"zone",
|
|
3188
|
+
"audio",
|
|
3189
|
+
"light",
|
|
3190
|
+
"npc",
|
|
3191
|
+
"camera",
|
|
3192
|
+
"timeline",
|
|
3193
|
+
"page",
|
|
3194
|
+
"include"
|
|
3195
|
+
];
|
|
3196
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3197
|
+
const currentDirectives = [];
|
|
3198
|
+
this.skipNewlines();
|
|
3199
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3200
|
+
while (this.check("AT")) {
|
|
3201
|
+
const directive = this.parseDirective();
|
|
3202
|
+
if (directive) {
|
|
3203
|
+
currentDirectives.push(directive);
|
|
3204
|
+
}
|
|
3205
|
+
this.skipNewlines();
|
|
3206
|
+
}
|
|
3207
|
+
const isNodeStart = this.check("IDENTIFIER") || this.check("STATE_MACHINE") || this.check("STATE") || this.check("TRANSITION") || this.check("INITIAL");
|
|
3208
|
+
if (isNodeStart) {
|
|
3209
|
+
const token = this.current();
|
|
3210
|
+
const next = this.peek(1);
|
|
3211
|
+
if (token.type === "IDENTIFIER" && token.value === "system" && (next.type === "IDENTIFIER" || next.type === "STRING") && this.peek(2).type !== "LBRACE") {
|
|
3212
|
+
const startToken = this.advance();
|
|
3213
|
+
const name = this.advance().value;
|
|
3214
|
+
result.systems.push({
|
|
3215
|
+
type: "system",
|
|
3216
|
+
name,
|
|
3217
|
+
id: name,
|
|
3218
|
+
properties: {},
|
|
3219
|
+
directives: currentDirectives,
|
|
3220
|
+
children: [],
|
|
3221
|
+
traits: /* @__PURE__ */ new Map(),
|
|
3222
|
+
loc: {
|
|
3223
|
+
start: { line: startToken.line, column: startToken.column },
|
|
3224
|
+
end: { line: this.current().line, column: this.current().column }
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
} else if (token.type === "IDENTIFIER" && token.value === "page" && next.type === "STRING") {
|
|
3228
|
+
const startToken = this.advance();
|
|
3229
|
+
const name = this.advance().value;
|
|
3230
|
+
const properties = {};
|
|
3231
|
+
if (this.check("LBRACE")) {
|
|
3232
|
+
this.advance();
|
|
3233
|
+
this.skipNewlines();
|
|
3234
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3235
|
+
this.skipNewlines();
|
|
3236
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3237
|
+
if (this.check("IDENTIFIER")) {
|
|
3238
|
+
const key = this.advance().value;
|
|
3239
|
+
if (this.check("COLON") || this.check("EQUALS")) {
|
|
3240
|
+
this.advance();
|
|
3241
|
+
properties[key] = this.parseValue();
|
|
3242
|
+
} else if (this.check("STRING")) {
|
|
3243
|
+
properties[key] = this.advance().value;
|
|
3244
|
+
} else {
|
|
3245
|
+
properties[key] = true;
|
|
3246
|
+
}
|
|
3247
|
+
} else if (this.check("COMMA")) {
|
|
3248
|
+
this.advance();
|
|
3249
|
+
} else {
|
|
3250
|
+
this.advance();
|
|
3251
|
+
}
|
|
3252
|
+
this.skipNewlines();
|
|
3253
|
+
}
|
|
3254
|
+
this.expect("RBRACE", "Expected }");
|
|
3255
|
+
}
|
|
3256
|
+
result.children.push({
|
|
3257
|
+
type: "page",
|
|
3258
|
+
name,
|
|
3259
|
+
id: name,
|
|
3260
|
+
properties,
|
|
3261
|
+
directives: currentDirectives,
|
|
3262
|
+
children: [],
|
|
3263
|
+
traits: /* @__PURE__ */ new Map(),
|
|
3264
|
+
loc: {
|
|
3265
|
+
start: { line: startToken.line, column: startToken.column },
|
|
3266
|
+
end: { line: this.current().line, column: this.current().column }
|
|
3267
|
+
}
|
|
3268
|
+
});
|
|
3269
|
+
} else if (next.type === "COLON" || next.type === "EQUALS") {
|
|
3270
|
+
const key = this.advance().value;
|
|
3271
|
+
this.advance();
|
|
3272
|
+
result.properties[key] = this.parseValue();
|
|
3273
|
+
} else if (childNodeKeywords.includes(token.value) && (next.type === "LBRACE" || next.type === "STRING" || next.type === "IDENTIFIER" || next.type === "HASH")) {
|
|
3274
|
+
const keyword = this.current().value;
|
|
3275
|
+
const node = this.parseNode();
|
|
3276
|
+
node.directives = [...currentDirectives, ...node.directives || []];
|
|
3277
|
+
if (keyword === "system" || node.type === "system") {
|
|
3278
|
+
result.systems.push(node);
|
|
3279
|
+
} else if (keyword === "core_config" || node.type === "core_config") {
|
|
3280
|
+
result.configs.push(node);
|
|
3281
|
+
} else {
|
|
3282
|
+
result.children.push(node);
|
|
3283
|
+
}
|
|
3284
|
+
} else if (!childNodeKeywords.includes(token.value) && (next.type === "STRING" || next.type === "NUMBER" || next.type === "BOOLEAN" || next.type === "NULL")) {
|
|
3285
|
+
const key = this.advance().value;
|
|
3286
|
+
result.properties[key] = this.parseValue();
|
|
3287
|
+
} else if (next.type === "LBRACE" || next.type === "IDENTIFIER" || next.type === "STRING" || next.type === "HASH") {
|
|
3288
|
+
const keyword = this.current().value;
|
|
3289
|
+
const node = this.parseNode();
|
|
3290
|
+
node.directives = [...currentDirectives, ...node.directives || []];
|
|
3291
|
+
if (keyword === "system" || node.type === "system") {
|
|
3292
|
+
result.systems.push(node);
|
|
3293
|
+
} else if (keyword === "core_config" || node.type === "core_config") {
|
|
3294
|
+
result.configs.push(node);
|
|
3295
|
+
} else {
|
|
3296
|
+
result.children.push(node);
|
|
3297
|
+
}
|
|
3298
|
+
} else if (next.type === "LPAREN") {
|
|
3299
|
+
const possibleMethod = this.pos;
|
|
3300
|
+
const methodName = this.advance().value;
|
|
3301
|
+
const params = [];
|
|
3302
|
+
this.advance();
|
|
3303
|
+
while (!this.check("RPAREN") && !this.check("EOF") && !this.check("LBRACE")) {
|
|
3304
|
+
if (this.check("IDENTIFIER")) {
|
|
3305
|
+
params.push(this.advance().value);
|
|
3306
|
+
} else {
|
|
3307
|
+
this.advance();
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
if (this.check("RPAREN")) this.advance();
|
|
3311
|
+
let returnType = "unknown";
|
|
3312
|
+
if (this.check("COLON")) {
|
|
3313
|
+
this.advance();
|
|
3314
|
+
if (this.check("IDENTIFIER")) {
|
|
3315
|
+
returnType = this.advance().value;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
if (this.check("LBRACE")) {
|
|
3319
|
+
const body = this.parseCodeBlock();
|
|
3320
|
+
result.children.push({
|
|
3321
|
+
type: "method",
|
|
3322
|
+
name: methodName,
|
|
3323
|
+
params,
|
|
3324
|
+
returnType,
|
|
3325
|
+
body
|
|
3326
|
+
});
|
|
3327
|
+
} else {
|
|
3328
|
+
this.pos = possibleMethod;
|
|
3329
|
+
const key = this.advance().value;
|
|
3330
|
+
result.properties[key] = true;
|
|
3331
|
+
}
|
|
3332
|
+
} else {
|
|
3333
|
+
const key = this.advance().value;
|
|
3334
|
+
result.properties[key] = true;
|
|
3335
|
+
}
|
|
3336
|
+
} else if (currentDirectives.length > 0) {
|
|
3337
|
+
const fragment = {
|
|
3338
|
+
type: "fragment",
|
|
3339
|
+
directives: currentDirectives,
|
|
3340
|
+
children: [],
|
|
3341
|
+
traits: /* @__PURE__ */ new Map(),
|
|
3342
|
+
properties: {}
|
|
3343
|
+
};
|
|
3344
|
+
result.children.push(fragment);
|
|
3345
|
+
} else if (this.check("COMMA")) {
|
|
3346
|
+
this.advance();
|
|
3347
|
+
} else if (this.check("LBRACKET")) {
|
|
3348
|
+
let depth = 1;
|
|
3349
|
+
this.advance();
|
|
3350
|
+
while (depth > 0 && !this.check("EOF")) {
|
|
3351
|
+
if (this.check("LBRACKET")) depth++;
|
|
3352
|
+
else if (this.check("RBRACKET")) depth--;
|
|
3353
|
+
this.advance();
|
|
3354
|
+
}
|
|
3355
|
+
} else if (this.check("LBRACE")) {
|
|
3356
|
+
let depth = 1;
|
|
3357
|
+
this.advance();
|
|
3358
|
+
while (depth > 0 && !this.check("EOF")) {
|
|
3359
|
+
if (this.check("LBRACE")) depth++;
|
|
3360
|
+
else if (this.check("RBRACE")) depth--;
|
|
3361
|
+
this.advance();
|
|
3362
|
+
}
|
|
3363
|
+
} else {
|
|
3364
|
+
if (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3365
|
+
this.error(
|
|
3366
|
+
`Unexpected token ${this.current().type} "${this.current().value}" in composition body. Expected property name, @directive, or child node`,
|
|
3367
|
+
"HSP101"
|
|
3368
|
+
);
|
|
3369
|
+
this.advance();
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
this.skipNewlines();
|
|
3373
|
+
}
|
|
3374
|
+
this.expect("RBRACE", "Expected }");
|
|
3375
|
+
return result;
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Parse logic block: logic { function..., on_tick..., on_scene_load... }
|
|
3379
|
+
*/
|
|
3380
|
+
parseLogicBlock() {
|
|
3381
|
+
const result = {
|
|
3382
|
+
functions: [],
|
|
3383
|
+
actions: [],
|
|
3384
|
+
eventHandlers: [],
|
|
3385
|
+
tickHandlers: []
|
|
3386
|
+
};
|
|
3387
|
+
if (!this.check("LBRACE")) {
|
|
3388
|
+
return result;
|
|
3389
|
+
}
|
|
3390
|
+
this.advance();
|
|
3391
|
+
this.skipNewlines();
|
|
3392
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3393
|
+
this.skipNewlines();
|
|
3394
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3395
|
+
if (this.check("IDENTIFIER")) {
|
|
3396
|
+
const keyword = this.current().value;
|
|
3397
|
+
if (keyword === "function") {
|
|
3398
|
+
this.advance();
|
|
3399
|
+
const funcName = this.expect("IDENTIFIER", "Expected function name").value;
|
|
3400
|
+
const params = [];
|
|
3401
|
+
if (this.check("LPAREN")) {
|
|
3402
|
+
this.advance();
|
|
3403
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
3404
|
+
params.push(this.expect("IDENTIFIER", "Expected parameter").value);
|
|
3405
|
+
if (this.check("COLON")) {
|
|
3406
|
+
this.advance();
|
|
3407
|
+
this.expect("IDENTIFIER", "Expected type");
|
|
3408
|
+
}
|
|
3409
|
+
if (this.check("COMMA")) this.advance();
|
|
3410
|
+
}
|
|
3411
|
+
this.expect("RPAREN", "Expected )");
|
|
3412
|
+
}
|
|
3413
|
+
if (this.check("COLON")) {
|
|
3414
|
+
this.advance();
|
|
3415
|
+
this.expect("IDENTIFIER", "Expected return type");
|
|
3416
|
+
}
|
|
3417
|
+
const body = this.parseCodeBlock();
|
|
3418
|
+
result.functions.push({ name: funcName, params, body });
|
|
3419
|
+
} else if (keyword === "action") {
|
|
3420
|
+
this.advance();
|
|
3421
|
+
const actionName = this.check("IDENTIFIER") || this.check("STRING") ? this.advance().value : "anonymous";
|
|
3422
|
+
const params = [];
|
|
3423
|
+
if (this.check("LPAREN")) {
|
|
3424
|
+
this.advance();
|
|
3425
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
3426
|
+
if (this.check("IDENTIFIER")) {
|
|
3427
|
+
params.push(this.advance().value);
|
|
3428
|
+
if (this.check("COLON")) {
|
|
3429
|
+
this.advance();
|
|
3430
|
+
while (!this.check("COMMA") && !this.check("RPAREN") && !this.check("EOF")) {
|
|
3431
|
+
this.advance();
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
if (this.check("COMMA")) {
|
|
3435
|
+
this.advance();
|
|
3436
|
+
}
|
|
3437
|
+
} else {
|
|
3438
|
+
this.advance();
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
this.expect("RPAREN", "Expected )");
|
|
3442
|
+
}
|
|
3443
|
+
const body = this.check("LBRACE") ? this.parseCodeBlock() : "";
|
|
3444
|
+
result.actions.push({ name: actionName, params, body });
|
|
3445
|
+
} else if (keyword === "on_tick") {
|
|
3446
|
+
this.advance();
|
|
3447
|
+
let interval = 1;
|
|
3448
|
+
if (this.check("LPAREN")) {
|
|
3449
|
+
this.advance();
|
|
3450
|
+
const intervalToken = this.expect("NUMBER", "Expected interval");
|
|
3451
|
+
interval = parseFloat(intervalToken.value);
|
|
3452
|
+
this.expect("RPAREN", "Expected )");
|
|
3453
|
+
}
|
|
3454
|
+
const body = this.parseCodeBlock();
|
|
3455
|
+
result.tickHandlers.push({ interval, body });
|
|
3456
|
+
} else if (keyword === "on_scene_load") {
|
|
3457
|
+
this.advance();
|
|
3458
|
+
const body = this.parseCodeBlock();
|
|
3459
|
+
result.eventHandlers.push({ event: "scene_load", params: [], body });
|
|
3460
|
+
} else if (keyword === "on") {
|
|
3461
|
+
this.advance();
|
|
3462
|
+
let eventName = "";
|
|
3463
|
+
if (this.check("AT")) {
|
|
3464
|
+
this.advance();
|
|
3465
|
+
eventName = this.expect("IDENTIFIER", "Expected event namespace").value;
|
|
3466
|
+
if (this.check("DOT")) {
|
|
3467
|
+
this.advance();
|
|
3468
|
+
eventName += "." + this.expect("IDENTIFIER", "Expected event name").value;
|
|
3469
|
+
}
|
|
3470
|
+
} else {
|
|
3471
|
+
eventName = this.expect("IDENTIFIER", "Expected event name").value;
|
|
3472
|
+
}
|
|
3473
|
+
const params = [];
|
|
3474
|
+
if (this.check("LPAREN")) {
|
|
3475
|
+
this.advance();
|
|
3476
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
3477
|
+
params.push(this.expect("IDENTIFIER", "Expected parameter").value);
|
|
3478
|
+
if (this.check("COLON")) {
|
|
3479
|
+
this.advance();
|
|
3480
|
+
this.expect("IDENTIFIER", "Expected type");
|
|
3481
|
+
}
|
|
3482
|
+
if (this.check("COMMA")) this.advance();
|
|
3483
|
+
}
|
|
3484
|
+
this.expect("RPAREN", "Expected )");
|
|
3485
|
+
}
|
|
3486
|
+
const body = this.parseCodeBlock();
|
|
3487
|
+
result.eventHandlers.push({ event: eventName, params, body });
|
|
3488
|
+
} else if (keyword === "on_start") {
|
|
3489
|
+
this.advance();
|
|
3490
|
+
const params = [];
|
|
3491
|
+
if (this.check("LPAREN")) {
|
|
3492
|
+
this.advance();
|
|
3493
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
3494
|
+
params.push(this.expect("IDENTIFIER", "Expected parameter").value);
|
|
3495
|
+
if (this.check("COMMA")) this.advance();
|
|
3496
|
+
}
|
|
3497
|
+
this.expect("RPAREN", "Expected )");
|
|
3498
|
+
}
|
|
3499
|
+
const body = this.parseCodeBlock();
|
|
3500
|
+
result.eventHandlers.push({ event: "start", params, body });
|
|
3501
|
+
} else if (keyword === "on_event") {
|
|
3502
|
+
this.advance();
|
|
3503
|
+
let eventName = "";
|
|
3504
|
+
const params = [];
|
|
3505
|
+
if (this.check("LPAREN")) {
|
|
3506
|
+
this.advance();
|
|
3507
|
+
if (this.check("STRING")) {
|
|
3508
|
+
eventName = this.advance().value;
|
|
3509
|
+
} else {
|
|
3510
|
+
eventName = this.expect("IDENTIFIER", "Expected event name").value;
|
|
3511
|
+
}
|
|
3512
|
+
if (this.check("COMMA")) {
|
|
3513
|
+
this.advance();
|
|
3514
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
3515
|
+
params.push(this.expect("IDENTIFIER", "Expected parameter").value);
|
|
3516
|
+
if (this.check("COMMA")) this.advance();
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
this.expect("RPAREN", "Expected )");
|
|
3520
|
+
}
|
|
3521
|
+
const body = this.parseCodeBlock();
|
|
3522
|
+
result.eventHandlers.push({ event: eventName, params, body });
|
|
3523
|
+
} else {
|
|
3524
|
+
this.advance();
|
|
3525
|
+
if (this.check("LPAREN")) this.skipParens();
|
|
3526
|
+
if (this.check("LBRACE")) this.skipBraces();
|
|
3527
|
+
while (!this.check("RBRACE") && !this.check("EOF") && !this.check("NEWLINE")) {
|
|
3528
|
+
this.advance();
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
} else {
|
|
3532
|
+
this.advance();
|
|
3533
|
+
}
|
|
3534
|
+
this.skipNewlines();
|
|
3535
|
+
}
|
|
3536
|
+
this.expect("RBRACE", "Expected }");
|
|
3537
|
+
return result;
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* Parse environment block with lighting directives
|
|
3541
|
+
*/
|
|
3542
|
+
parseEnvironmentBlock() {
|
|
3543
|
+
const properties = {};
|
|
3544
|
+
const directives = [];
|
|
3545
|
+
if (!this.check("LBRACE")) {
|
|
3546
|
+
return { properties, directives };
|
|
3547
|
+
}
|
|
3548
|
+
this.advance();
|
|
3549
|
+
this.skipNewlines();
|
|
3550
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3551
|
+
this.skipNewlines();
|
|
3552
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3553
|
+
if (this.check("AT")) {
|
|
3554
|
+
const directive = this.parseDirective();
|
|
3555
|
+
if (directive) {
|
|
3556
|
+
directives.push(directive);
|
|
3557
|
+
}
|
|
3558
|
+
} else if (this.check("IDENTIFIER")) {
|
|
3559
|
+
const key = this.advance().value;
|
|
3560
|
+
if (this.check("COLON") || this.check("EQUALS")) {
|
|
3561
|
+
this.advance();
|
|
3562
|
+
properties[key] = this.parseValue();
|
|
3563
|
+
} else {
|
|
3564
|
+
properties[key] = true;
|
|
3565
|
+
}
|
|
3566
|
+
} else {
|
|
3567
|
+
this.advance();
|
|
3568
|
+
}
|
|
3569
|
+
this.skipNewlines();
|
|
3570
|
+
}
|
|
3571
|
+
this.expect("RBRACE", "Expected }");
|
|
3572
|
+
return { properties, directives };
|
|
3573
|
+
}
|
|
3574
|
+
parsePropsBlock() {
|
|
3575
|
+
this.skipNewlines();
|
|
3576
|
+
const props = {};
|
|
3577
|
+
if (this.check("LBRACE")) {
|
|
3578
|
+
this.advance();
|
|
3579
|
+
this.skipNewlines();
|
|
3580
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3581
|
+
if (this.check("SPREAD")) {
|
|
3582
|
+
this.advance();
|
|
3583
|
+
const val = this.parseValue();
|
|
3584
|
+
const spreadKey = `__spread_${Object.keys(props).length}`;
|
|
3585
|
+
props[spreadKey] = { type: "spread", argument: val };
|
|
3586
|
+
this.skipNewlines();
|
|
3587
|
+
if (this.check("COMMA")) this.advance();
|
|
3588
|
+
this.skipNewlines();
|
|
3589
|
+
continue;
|
|
3590
|
+
}
|
|
3591
|
+
const key = this.expect("IDENTIFIER", "Expected property name").value;
|
|
3592
|
+
if (this.check("COLON") || this.check("EQUALS")) {
|
|
3593
|
+
this.advance();
|
|
3594
|
+
props[key] = this.parseValue();
|
|
3595
|
+
} else {
|
|
3596
|
+
props[key] = true;
|
|
3597
|
+
}
|
|
3598
|
+
this.skipNewlines();
|
|
3599
|
+
if (this.check("COMMA")) this.advance();
|
|
3600
|
+
this.skipNewlines();
|
|
3601
|
+
}
|
|
3602
|
+
this.expect("RBRACE", "Expected }");
|
|
3603
|
+
}
|
|
3604
|
+
return props;
|
|
3605
|
+
}
|
|
3606
|
+
parseDialogBlock() {
|
|
3607
|
+
this.skipNewlines();
|
|
3608
|
+
const props = {};
|
|
3609
|
+
const options = [];
|
|
3610
|
+
if (this.check("LBRACE")) {
|
|
3611
|
+
this.advance();
|
|
3612
|
+
this.skipNewlines();
|
|
3613
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3614
|
+
if (this.check("IDENTIFIER") && this.current().value === "option") {
|
|
3615
|
+
this.advance();
|
|
3616
|
+
const text = this.expect("STRING", "Expected option text").value;
|
|
3617
|
+
this.expect("ARROW", "Expected ->");
|
|
3618
|
+
let target;
|
|
3619
|
+
if (this.check("AT")) {
|
|
3620
|
+
const d = this.parseDirective();
|
|
3621
|
+
target = { type: "directive", value: d };
|
|
3622
|
+
} else {
|
|
3623
|
+
target = this.expect("STRING", "Expected target ID").value;
|
|
3624
|
+
}
|
|
3625
|
+
options.push({ text, target });
|
|
3626
|
+
} else {
|
|
3627
|
+
const key = this.expect("IDENTIFIER", "Expected property name").value;
|
|
3628
|
+
if (this.check("COLON") || this.check("EQUALS")) {
|
|
3629
|
+
this.advance();
|
|
3630
|
+
props[key] = this.parseValue();
|
|
3631
|
+
} else {
|
|
3632
|
+
props[key] = true;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
this.skipNewlines();
|
|
3636
|
+
}
|
|
3637
|
+
this.expect("RBRACE", "Expected }");
|
|
3638
|
+
}
|
|
3639
|
+
return { props, options };
|
|
3640
|
+
}
|
|
3641
|
+
parseTraitConfig() {
|
|
3642
|
+
const config = {};
|
|
3643
|
+
if (this.check("LPAREN")) {
|
|
3644
|
+
this.advance();
|
|
3645
|
+
this.skipNewlines();
|
|
3646
|
+
if (this.check("STRING") && (this.peek(1).type === "RPAREN" || this.peek(1).type === "COMMA")) {
|
|
3647
|
+
config["default"] = this.advance().value;
|
|
3648
|
+
if (this.check("COMMA")) this.advance();
|
|
3649
|
+
this.skipNewlines();
|
|
3650
|
+
}
|
|
3651
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
3652
|
+
this.skipNewlines();
|
|
3653
|
+
if (this.check("RPAREN") || this.check("EOF")) break;
|
|
3654
|
+
if (!this.check("IDENTIFIER")) {
|
|
3655
|
+
this.advance();
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3658
|
+
const key = this.expect("IDENTIFIER", "Expected property name").value;
|
|
3659
|
+
if (this.check("COLON") || this.check("EQUALS")) {
|
|
3660
|
+
this.advance();
|
|
3661
|
+
config[key] = this.parseValue();
|
|
3662
|
+
} else {
|
|
3663
|
+
config[key] = true;
|
|
3664
|
+
}
|
|
3665
|
+
if (this.check("COMMA")) this.advance();
|
|
3666
|
+
this.skipNewlines();
|
|
3667
|
+
}
|
|
3668
|
+
this.expect("RPAREN", "Expected )");
|
|
3669
|
+
}
|
|
3670
|
+
return config;
|
|
3671
|
+
}
|
|
3672
|
+
parseStateBlock() {
|
|
3673
|
+
const state = {};
|
|
3674
|
+
if (this.check("LBRACE")) {
|
|
3675
|
+
this.advance();
|
|
3676
|
+
this.skipNewlines();
|
|
3677
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3678
|
+
this.skipNewlines();
|
|
3679
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3680
|
+
if (this.check("SPREAD")) {
|
|
3681
|
+
this.advance();
|
|
3682
|
+
const spreadArg = this.parseValue();
|
|
3683
|
+
if (spreadArg === null) {
|
|
3684
|
+
this.error("Expected expression after spread operator (...) in state block", "HSP300");
|
|
3685
|
+
} else {
|
|
3686
|
+
const spreadKey = `__spread_${Object.keys(state).length}`;
|
|
3687
|
+
state[spreadKey] = { type: "spread", argument: spreadArg };
|
|
3688
|
+
}
|
|
3689
|
+
this.skipNewlines();
|
|
3690
|
+
continue;
|
|
3691
|
+
}
|
|
3692
|
+
const key = this.expect("IDENTIFIER", "Expected state variable name").value;
|
|
3693
|
+
if (this.check("COLON") || this.check("EQUALS")) {
|
|
3694
|
+
this.advance();
|
|
3695
|
+
state[key] = this.parseValue();
|
|
3696
|
+
} else {
|
|
3697
|
+
state[key] = true;
|
|
3698
|
+
}
|
|
3699
|
+
this.skipNewlines();
|
|
3700
|
+
}
|
|
3701
|
+
this.expect("RBRACE", "Expected }");
|
|
3702
|
+
}
|
|
3703
|
+
return state;
|
|
3704
|
+
}
|
|
3705
|
+
/**
|
|
3706
|
+
* Parse spatial state machine (Phase 13)
|
|
3707
|
+
*/
|
|
3708
|
+
parseStateMachine() {
|
|
3709
|
+
const startToken = this.current();
|
|
3710
|
+
if (startToken.type === "STATE_MACHINE") {
|
|
3711
|
+
this.advance();
|
|
3712
|
+
}
|
|
3713
|
+
const name = this.expect("IDENTIFIER", "Expected state machine name").value;
|
|
3714
|
+
const states = [];
|
|
3715
|
+
const transitions = [];
|
|
3716
|
+
let initialState = "";
|
|
3717
|
+
this.expect("LBRACE", "Expected { after state machine name");
|
|
3718
|
+
this.skipNewlines();
|
|
3719
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3720
|
+
this.skipNewlines();
|
|
3721
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3722
|
+
const current = this.current();
|
|
3723
|
+
if (current.type === "INITIAL" || current.value === "initial") {
|
|
3724
|
+
this.advance();
|
|
3725
|
+
this.expect("COLON", "Expected : after initial");
|
|
3726
|
+
initialState = this.expect("STRING", "Expected initial state name").value;
|
|
3727
|
+
} else if (current.type === "STATE" || current.value === "state") {
|
|
3728
|
+
states.push(this.parseStateNode());
|
|
3729
|
+
} else if (current.type === "TRANSITION" || current.value === "transitions") {
|
|
3730
|
+
transitions.push(...this.parseTransitionsBlock());
|
|
3731
|
+
} else {
|
|
3732
|
+
this.advance();
|
|
3733
|
+
}
|
|
3734
|
+
this.skipNewlines();
|
|
3735
|
+
}
|
|
3736
|
+
this.expect("RBRACE", "Expected } at end of state machine");
|
|
3737
|
+
return {
|
|
3738
|
+
type: "state-machine",
|
|
3739
|
+
name,
|
|
3740
|
+
initialState,
|
|
3741
|
+
states,
|
|
3742
|
+
transitions,
|
|
3743
|
+
loc: {
|
|
3744
|
+
start: { line: startToken.line, column: startToken.column },
|
|
3745
|
+
end: { line: this.current().line, column: this.current().column }
|
|
3746
|
+
}
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
parseStateNode() {
|
|
3750
|
+
this.advance();
|
|
3751
|
+
const name = this.expect("IDENTIFIER", "Expected state name").value;
|
|
3752
|
+
let onEntry;
|
|
3753
|
+
let onExit;
|
|
3754
|
+
this.expect("LBRACE", "Expected { after state name");
|
|
3755
|
+
this.skipNewlines();
|
|
3756
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3757
|
+
this.skipNewlines();
|
|
3758
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3759
|
+
const current = this.current();
|
|
3760
|
+
if (current.type === "ON_ENTRY" || current.value === "on_entry") {
|
|
3761
|
+
this.advance();
|
|
3762
|
+
onEntry = this.parseCodeBlock();
|
|
3763
|
+
} else if (current.type === "ON_EXIT" || current.value === "on_exit") {
|
|
3764
|
+
this.advance();
|
|
3765
|
+
onExit = this.parseCodeBlock();
|
|
3766
|
+
} else {
|
|
3767
|
+
this.advance();
|
|
3768
|
+
}
|
|
3769
|
+
this.skipNewlines();
|
|
3770
|
+
}
|
|
3771
|
+
this.expect("RBRACE", "Expected }");
|
|
3772
|
+
return { name, onEntry, onExit };
|
|
3773
|
+
}
|
|
3774
|
+
parseOnErrorNode() {
|
|
3775
|
+
const startToken = this.previous();
|
|
3776
|
+
const body = this.parseControlFlowBody();
|
|
3777
|
+
return {
|
|
3778
|
+
type: "on_error",
|
|
3779
|
+
body,
|
|
3780
|
+
loc: {
|
|
3781
|
+
start: { line: startToken.line, column: startToken.column },
|
|
3782
|
+
end: { line: this.current().line, column: this.current().column }
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
parseAssertNode() {
|
|
3787
|
+
const startToken = this.previous();
|
|
3788
|
+
this.expect("LPAREN", "Expected (");
|
|
3789
|
+
const condition = this.parseExpression();
|
|
3790
|
+
let message = "";
|
|
3791
|
+
if (this.match(["COMMA"])) {
|
|
3792
|
+
message = String(this.parseValue());
|
|
3793
|
+
}
|
|
3794
|
+
this.expect("RPAREN", "Expected )");
|
|
3795
|
+
return {
|
|
3796
|
+
type: "assert",
|
|
3797
|
+
condition,
|
|
3798
|
+
message,
|
|
3799
|
+
loc: {
|
|
3800
|
+
start: { line: startToken.line, column: startToken.column },
|
|
3801
|
+
end: { line: this.current().line, column: this.current().column }
|
|
3802
|
+
}
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
parseTransitionsBlock() {
|
|
3806
|
+
const transitions = [];
|
|
3807
|
+
this.advance();
|
|
3808
|
+
this.expect("LBRACE", "Expected {");
|
|
3809
|
+
this.skipNewlines();
|
|
3810
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3811
|
+
this.skipNewlines();
|
|
3812
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3813
|
+
const from = this.expect("IDENTIFIER", "Expected source state").value;
|
|
3814
|
+
this.expect("ARROW", "Expected ->");
|
|
3815
|
+
const to = this.expect("IDENTIFIER", "Expected target state").value;
|
|
3816
|
+
this.expect("COLON", "Expected :");
|
|
3817
|
+
const event = this.expect("IDENTIFIER", "Expected event name").value;
|
|
3818
|
+
transitions.push({ from, to, event });
|
|
3819
|
+
this.skipNewlines();
|
|
3820
|
+
}
|
|
3821
|
+
this.expect("RBRACE", "Expected }");
|
|
3822
|
+
return transitions;
|
|
3823
|
+
}
|
|
3824
|
+
parseControlFlowBody() {
|
|
3825
|
+
const nodes = [];
|
|
3826
|
+
if (this.check("LBRACE")) {
|
|
3827
|
+
this.advance();
|
|
3828
|
+
this.skipNewlines();
|
|
3829
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
3830
|
+
this.skipNewlines();
|
|
3831
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
3832
|
+
if (this.check("AT")) {
|
|
3833
|
+
const directive = this.parseDirective();
|
|
3834
|
+
if (directive) {
|
|
3835
|
+
if (directive.type === "for" || directive.type === "while" || directive.type === "if" || directive.type === "forEach") {
|
|
3836
|
+
nodes.push({
|
|
3837
|
+
type: "fragment",
|
|
3838
|
+
directives: [directive],
|
|
3839
|
+
children: [],
|
|
3840
|
+
traits: /* @__PURE__ */ new Map(),
|
|
3841
|
+
properties: {}
|
|
3842
|
+
});
|
|
3843
|
+
} else if (directive.type === "trait") {
|
|
3844
|
+
this.warn(`Standalone trait @${directive.name} in block`);
|
|
3845
|
+
} else {
|
|
3846
|
+
nodes.push({
|
|
3847
|
+
type: "fragment",
|
|
3848
|
+
directives: [directive],
|
|
3849
|
+
children: [],
|
|
3850
|
+
traits: /* @__PURE__ */ new Map(),
|
|
3851
|
+
properties: {}
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
} else if (this.check("IDENTIFIER")) {
|
|
3856
|
+
nodes.push(this.parseNode());
|
|
3857
|
+
} else {
|
|
3858
|
+
this.advance();
|
|
3859
|
+
}
|
|
3860
|
+
this.skipNewlines();
|
|
3861
|
+
}
|
|
3862
|
+
this.expect("RBRACE", "Expected }");
|
|
3863
|
+
}
|
|
3864
|
+
return nodes;
|
|
3865
|
+
}
|
|
3866
|
+
parseCodeBlock() {
|
|
3867
|
+
let code = "";
|
|
3868
|
+
let braceDepth = 0;
|
|
3869
|
+
if (this.check("LBRACE")) {
|
|
3870
|
+
this.advance();
|
|
3871
|
+
braceDepth = 1;
|
|
3872
|
+
try {
|
|
3873
|
+
while (braceDepth > 0 && !this.check("EOF")) {
|
|
3874
|
+
const token = this.advance();
|
|
3875
|
+
if (token.type === "LBRACE") {
|
|
3876
|
+
braceDepth++;
|
|
3877
|
+
code += "{";
|
|
3878
|
+
} else if (token.type === "RBRACE") {
|
|
3879
|
+
braceDepth--;
|
|
3880
|
+
if (braceDepth > 0) {
|
|
3881
|
+
code += "}";
|
|
3882
|
+
}
|
|
3883
|
+
} else if (token.type === "STRING") {
|
|
3884
|
+
code += `"${token.value}"`;
|
|
3885
|
+
code += " ";
|
|
3886
|
+
} else {
|
|
3887
|
+
code += token.value;
|
|
3888
|
+
if (token.type === "NEWLINE") {
|
|
3889
|
+
code += "\n";
|
|
3890
|
+
} else {
|
|
3891
|
+
code += " ";
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
} catch (e) {
|
|
3896
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3897
|
+
if (errorMessage !== "ParseError") console.error(e);
|
|
3898
|
+
this.synchronize();
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
return code.trim();
|
|
3902
|
+
}
|
|
3903
|
+
parseInlineExpression() {
|
|
3904
|
+
let expr = "";
|
|
3905
|
+
while (!this.check("NEWLINE") && !this.check("LBRACE") && !this.check("EOF")) {
|
|
3906
|
+
const token = this.advance();
|
|
3907
|
+
expr += token.value + " ";
|
|
3908
|
+
}
|
|
3909
|
+
return expr.trim();
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* Parse any value or expression (Entry Point)
|
|
3913
|
+
* Handles operators like ?? and unary operators like ...
|
|
3914
|
+
*/
|
|
3915
|
+
parseValue() {
|
|
3916
|
+
return this.parseAssignment();
|
|
3917
|
+
}
|
|
3918
|
+
/**
|
|
3919
|
+
* Parse assignment expressions (??=)
|
|
3920
|
+
* Much lower precedence than ternary/null-coalesce
|
|
3921
|
+
* Example: x ??= value → x = x ?? value
|
|
3922
|
+
*/
|
|
3923
|
+
parseAssignment() {
|
|
3924
|
+
const expr = this.parseExpression();
|
|
3925
|
+
if (this.check("NULL_COALESCE_ASSIGN")) {
|
|
3926
|
+
const isAssignable = typeof expr === "string" || typeof expr === "object" && expr && "__ref" in expr || // identifier or member expression
|
|
3927
|
+
typeof expr === "object" && expr && "type" in expr && expr.type === "member";
|
|
3928
|
+
if (isAssignable) {
|
|
3929
|
+
this.advance();
|
|
3930
|
+
const value = this.parseExpression();
|
|
3931
|
+
return {
|
|
3932
|
+
type: "nullCoalescingAssignment",
|
|
3933
|
+
target: expr,
|
|
3934
|
+
value
|
|
3935
|
+
};
|
|
3936
|
+
} else {
|
|
3937
|
+
throw new Error(
|
|
3938
|
+
`Cannot use ??= on non-assignable expression at line ${this.current().line}`
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
return expr;
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Parse expression (Entry Point for operators)
|
|
3946
|
+
* Handles Ternary Operators: cond ? true : false
|
|
3947
|
+
*/
|
|
3948
|
+
parseExpression() {
|
|
3949
|
+
const condition = this.parseNullCoalesce();
|
|
3950
|
+
if (this.check("QUESTION")) {
|
|
3951
|
+
this.advance();
|
|
3952
|
+
const trueValue = this.parseExpression();
|
|
3953
|
+
this.expect("COLON", "Expected : in ternary operator");
|
|
3954
|
+
const falseValue = this.parseExpression();
|
|
3955
|
+
return { type: "ternary", condition, trueValue, falseValue };
|
|
3956
|
+
}
|
|
3957
|
+
return condition;
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* Parse binary expressions: ?? (null coalesce) — lower precedence than logical OR
|
|
3961
|
+
*/
|
|
3962
|
+
parseNullCoalesce() {
|
|
3963
|
+
let left = this.parseLogicalOr();
|
|
3964
|
+
while (this.check("NULL_COALESCE")) {
|
|
3965
|
+
this.advance();
|
|
3966
|
+
const right = this.parseLogicalOr();
|
|
3967
|
+
left = { type: "binary", operator: "??", left, right };
|
|
3968
|
+
}
|
|
3969
|
+
return left;
|
|
3970
|
+
}
|
|
3971
|
+
/** Parse logical OR: a || b */
|
|
3972
|
+
parseLogicalOr() {
|
|
3973
|
+
let left = this.parseLogicalAnd();
|
|
3974
|
+
while (this.check("OR")) {
|
|
3975
|
+
this.advance();
|
|
3976
|
+
const right = this.parseLogicalAnd();
|
|
3977
|
+
left = { type: "binary", operator: "||", left, right };
|
|
3978
|
+
}
|
|
3979
|
+
return left;
|
|
3980
|
+
}
|
|
3981
|
+
/** Parse logical AND: a && b */
|
|
3982
|
+
parseLogicalAnd() {
|
|
3983
|
+
let left = this.parseEquality();
|
|
3984
|
+
while (this.check("AND")) {
|
|
3985
|
+
this.advance();
|
|
3986
|
+
const right = this.parseEquality();
|
|
3987
|
+
left = { type: "binary", operator: "&&", left, right };
|
|
3988
|
+
}
|
|
3989
|
+
return left;
|
|
3990
|
+
}
|
|
3991
|
+
/** Parse equality: a == b, a != b */
|
|
3992
|
+
parseEquality() {
|
|
3993
|
+
let left = this.parseComparison();
|
|
3994
|
+
while (this.check("DOUBLE_EQUALS") || this.check("NOT_EQUALS")) {
|
|
3995
|
+
const operator = this.current().value;
|
|
3996
|
+
this.advance();
|
|
3997
|
+
const right = this.parseComparison();
|
|
3998
|
+
left = { type: "binary", operator, left, right };
|
|
3999
|
+
}
|
|
4000
|
+
return left;
|
|
4001
|
+
}
|
|
4002
|
+
/** Parse comparison: a < b, a > b, a <= b, a >= b */
|
|
4003
|
+
parseComparison() {
|
|
4004
|
+
let left = this.parseAdditive();
|
|
4005
|
+
while (this.check("LESS_THAN") || this.check("GREATER_THAN") || this.check("LESS_EQUAL") || this.check("GREATER_EQUAL")) {
|
|
4006
|
+
const operator = this.current().value;
|
|
4007
|
+
this.advance();
|
|
4008
|
+
const right = this.parseAdditive();
|
|
4009
|
+
left = { type: "binary", operator, left, right };
|
|
4010
|
+
}
|
|
4011
|
+
return left;
|
|
4012
|
+
}
|
|
4013
|
+
/** Parse additive: a + b, a - b */
|
|
4014
|
+
parseAdditive() {
|
|
4015
|
+
let left = this.parseMultiplicative();
|
|
4016
|
+
while (this.check("PLUS") || this.check("MINUS")) {
|
|
4017
|
+
const operator = this.current().value;
|
|
4018
|
+
this.advance();
|
|
4019
|
+
const right = this.parseMultiplicative();
|
|
4020
|
+
left = { type: "binary", operator, left, right };
|
|
4021
|
+
}
|
|
4022
|
+
return left;
|
|
4023
|
+
}
|
|
4024
|
+
/** Parse multiplicative: a * b, a / b, a % b */
|
|
4025
|
+
parseMultiplicative() {
|
|
4026
|
+
let left = this.parseUnary();
|
|
4027
|
+
while (this.check("ASTERISK") || this.check("SLASH") || this.check("PERCENT")) {
|
|
4028
|
+
const operator = this.current().value;
|
|
4029
|
+
this.advance();
|
|
4030
|
+
const right = this.parseUnary();
|
|
4031
|
+
left = { type: "binary", operator, left, right };
|
|
4032
|
+
}
|
|
4033
|
+
return left;
|
|
4034
|
+
}
|
|
4035
|
+
/**
|
|
4036
|
+
* Parse unary prefix expressions (!, spread, unary minus, unary plus)
|
|
4037
|
+
*/
|
|
4038
|
+
parseUnary() {
|
|
4039
|
+
if (this.check("SPREAD")) {
|
|
4040
|
+
this.advance();
|
|
4041
|
+
const arg = this.parseUnary();
|
|
4042
|
+
return { type: "spread", argument: arg };
|
|
4043
|
+
}
|
|
4044
|
+
if (this.check("EXCLAMATION")) {
|
|
4045
|
+
this.advance();
|
|
4046
|
+
const arg = this.parseUnary();
|
|
4047
|
+
return { type: "unary", operator: "!", argument: arg };
|
|
4048
|
+
}
|
|
4049
|
+
if (this.check("MINUS") || this.check("PLUS")) {
|
|
4050
|
+
const operator = this.current().type === "MINUS" ? "-" : "+";
|
|
4051
|
+
this.advance();
|
|
4052
|
+
const arg = this.parseUnary();
|
|
4053
|
+
if (typeof arg === "number") {
|
|
4054
|
+
return operator === "-" ? -arg : arg;
|
|
4055
|
+
}
|
|
4056
|
+
return { type: "unary", operator, argument: arg };
|
|
4057
|
+
}
|
|
4058
|
+
return this.parsePrimary();
|
|
4059
|
+
}
|
|
4060
|
+
/**
|
|
4061
|
+
* Parse a primary value (literal, identifier, parenthesis, etc.)
|
|
4062
|
+
*/
|
|
4063
|
+
parsePrimary() {
|
|
4064
|
+
const token = this.current();
|
|
4065
|
+
if (token.type === "STRING") {
|
|
4066
|
+
this.advance();
|
|
4067
|
+
return token.value;
|
|
4068
|
+
}
|
|
4069
|
+
if (token.type === "NUMBER") {
|
|
4070
|
+
this.advance();
|
|
4071
|
+
const match = token.value.match(/^(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)(.*)?$/i);
|
|
4072
|
+
if (match) {
|
|
4073
|
+
const num = parseFloat(match[1]);
|
|
4074
|
+
const unit = match[2];
|
|
4075
|
+
if (unit) {
|
|
4076
|
+
return `${num}${unit}`;
|
|
4077
|
+
}
|
|
4078
|
+
return num;
|
|
4079
|
+
}
|
|
4080
|
+
return parseFloat(token.value);
|
|
4081
|
+
}
|
|
4082
|
+
if (token.type === "BOOLEAN") {
|
|
4083
|
+
this.advance();
|
|
4084
|
+
return token.value === "true";
|
|
4085
|
+
}
|
|
4086
|
+
if (token.type === "NULL") {
|
|
4087
|
+
this.advance();
|
|
4088
|
+
return null;
|
|
4089
|
+
}
|
|
4090
|
+
if (token.type === "EXPRESSION") {
|
|
4091
|
+
this.advance();
|
|
4092
|
+
const exprId = `expr_${this.compiledExpressions.size}`;
|
|
4093
|
+
this.compiledExpressions.set(exprId, token.value);
|
|
4094
|
+
return { __expr: exprId, __raw: token.value };
|
|
4095
|
+
}
|
|
4096
|
+
if (token.type === "TEMPLATE_STRING") {
|
|
4097
|
+
this.advance();
|
|
4098
|
+
return { type: "templateLiteral", value: token.value };
|
|
4099
|
+
}
|
|
4100
|
+
if (token.type === "LBRACKET") {
|
|
4101
|
+
return this.parseArray();
|
|
4102
|
+
}
|
|
4103
|
+
if (token.type === "LBRACE") {
|
|
4104
|
+
return this.parseBlockContent();
|
|
4105
|
+
}
|
|
4106
|
+
if (token.type === "HASH") {
|
|
4107
|
+
this.advance();
|
|
4108
|
+
const nextToken = this.current();
|
|
4109
|
+
if (nextToken.type === "IDENTIFIER" || nextToken.type === "NUMBER") {
|
|
4110
|
+
const hexValue = this.advance().value;
|
|
4111
|
+
return `#${hexValue}`;
|
|
4112
|
+
}
|
|
4113
|
+
return `#`;
|
|
4114
|
+
}
|
|
4115
|
+
if (token.type === "THIS") {
|
|
4116
|
+
this.advance();
|
|
4117
|
+
let ref = "this";
|
|
4118
|
+
while (this.check("DOT")) {
|
|
4119
|
+
this.advance();
|
|
4120
|
+
const part = this.expect("IDENTIFIER", "Expected property name").value;
|
|
4121
|
+
ref += "." + part;
|
|
4122
|
+
}
|
|
4123
|
+
return { __ref: ref };
|
|
4124
|
+
}
|
|
4125
|
+
if (token.type === "LPAREN") {
|
|
4126
|
+
let isArrow = false;
|
|
4127
|
+
let i = 1;
|
|
4128
|
+
let parenDepth = 1;
|
|
4129
|
+
while (this.peek(i).type !== "EOF") {
|
|
4130
|
+
const t = this.peek(i);
|
|
4131
|
+
if (t.type === "LPAREN") parenDepth++;
|
|
4132
|
+
if (t.type === "RPAREN") {
|
|
4133
|
+
parenDepth--;
|
|
4134
|
+
if (parenDepth === 0) {
|
|
4135
|
+
const next = this.peek(i + 1);
|
|
4136
|
+
if (next.type === "ARROW") isArrow = true;
|
|
4137
|
+
if (next.type === "COLON") {
|
|
4138
|
+
let j = i + 2;
|
|
4139
|
+
while (this.peek(j).type === "IDENTIFIER" || this.peek(j).type === "LBRACKET" || this.peek(j).type === "RBRACKET")
|
|
4140
|
+
j++;
|
|
4141
|
+
if (this.peek(j).type === "ARROW") isArrow = true;
|
|
4142
|
+
}
|
|
4143
|
+
break;
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
i++;
|
|
4147
|
+
}
|
|
4148
|
+
if (isArrow) {
|
|
4149
|
+
return this.parseArrowFunction();
|
|
4150
|
+
}
|
|
4151
|
+
return this.parseParenExpression();
|
|
4152
|
+
}
|
|
4153
|
+
if (token.type === "MATCH") {
|
|
4154
|
+
return this.parseMatchExpression();
|
|
4155
|
+
}
|
|
4156
|
+
const isIdentifierLike = token.type === "IDENTIFIER" || token.type === "STATE" || token.type === "INITIAL" || token.type === "STATE_MACHINE" || token.type === "TRANSITION" || token.type === "ON_ENTRY" || token.type === "ON_EXIT";
|
|
4157
|
+
if (isIdentifierLike) {
|
|
4158
|
+
if (this.peek(1).type === "ARROW") {
|
|
4159
|
+
return this.parseArrowFunction();
|
|
4160
|
+
}
|
|
4161
|
+
this.advance();
|
|
4162
|
+
let ref = token.value;
|
|
4163
|
+
while (this.check("DOT") || this.check("OPTIONAL_DOT") || this.check("LBRACKET")) {
|
|
4164
|
+
if (this.check("OPTIONAL_DOT")) {
|
|
4165
|
+
this.advance();
|
|
4166
|
+
const nextToken = this.current();
|
|
4167
|
+
const isPropertyName = nextToken.type === "IDENTIFIER" || nextToken.type === "STATE" || nextToken.type === "INITIAL" || nextToken.type === "STATE_MACHINE" || nextToken.type === "TRANSITION" || nextToken.type === "ON_ENTRY" || nextToken.type === "ON_EXIT";
|
|
4168
|
+
if (isPropertyName) {
|
|
4169
|
+
ref += "?." + this.advance().value;
|
|
4170
|
+
} else {
|
|
4171
|
+
this.error("Expected property name after optional chaining (?.)", "HSP002");
|
|
4172
|
+
}
|
|
4173
|
+
} else if (this.check("LBRACKET")) {
|
|
4174
|
+
this.advance();
|
|
4175
|
+
const indexExpr = this.parseValue();
|
|
4176
|
+
this.expect("RBRACKET", "Expected ]");
|
|
4177
|
+
const base = ref;
|
|
4178
|
+
return { type: "computedMember", object: { __ref: base }, property: indexExpr };
|
|
4179
|
+
} else {
|
|
4180
|
+
this.advance();
|
|
4181
|
+
const nextToken = this.current();
|
|
4182
|
+
const isPropertyName = nextToken.type === "IDENTIFIER" || nextToken.type === "STATE" || nextToken.type === "INITIAL" || nextToken.type === "STATE_MACHINE" || nextToken.type === "TRANSITION" || nextToken.type === "ON_ENTRY" || nextToken.type === "ON_EXIT";
|
|
4183
|
+
if (isPropertyName) {
|
|
4184
|
+
ref += "." + this.advance().value;
|
|
4185
|
+
} else {
|
|
4186
|
+
this.error("Expected property name after dot (.)", "HSP002");
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
if (this.check("LPAREN")) {
|
|
4191
|
+
const args = this.parseParenExpression();
|
|
4192
|
+
return { type: "call", callee: ref, args };
|
|
4193
|
+
}
|
|
4194
|
+
return { __ref: ref };
|
|
4195
|
+
}
|
|
4196
|
+
if (this.check("AT")) {
|
|
4197
|
+
const dir = this.parseDirective();
|
|
4198
|
+
return dir;
|
|
4199
|
+
}
|
|
4200
|
+
this.error(
|
|
4201
|
+
`Unexpected token in expression: ${token.type} "${token.value}". Expected value, identifier, or expression`,
|
|
4202
|
+
"HSP300"
|
|
4203
|
+
);
|
|
4204
|
+
const err = new Error("ParseError");
|
|
4205
|
+
err.message = "ParseError";
|
|
4206
|
+
throw err;
|
|
4207
|
+
}
|
|
4208
|
+
parseParenExpression() {
|
|
4209
|
+
this.expect("LPAREN", "Expected (");
|
|
4210
|
+
const items = [];
|
|
4211
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
4212
|
+
items.push(this.parseValue());
|
|
4213
|
+
if (this.check("COMMA")) this.advance();
|
|
4214
|
+
}
|
|
4215
|
+
this.expect("RPAREN", "Expected )");
|
|
4216
|
+
return items.length === 1 ? items[0] : items;
|
|
4217
|
+
}
|
|
4218
|
+
parseArrowFunction() {
|
|
4219
|
+
const params = [];
|
|
4220
|
+
if (this.check("LPAREN")) {
|
|
4221
|
+
this.advance();
|
|
4222
|
+
while (!this.check("RPAREN") && !this.check("EOF")) {
|
|
4223
|
+
if (this.check("SPREAD")) {
|
|
4224
|
+
this.advance();
|
|
4225
|
+
const name2 = this.expect("IDENTIFIER", "Expected parameter name after ...").value;
|
|
4226
|
+
let type2 = null;
|
|
4227
|
+
if (this.check("COLON")) {
|
|
4228
|
+
this.advance();
|
|
4229
|
+
type2 = this.expect("IDENTIFIER", "Expected type").value;
|
|
4230
|
+
}
|
|
4231
|
+
params.push({ name: name2, type: type2, rest: true });
|
|
4232
|
+
if (this.check("COMMA")) this.advance();
|
|
4233
|
+
break;
|
|
4234
|
+
}
|
|
4235
|
+
const name = this.expect("IDENTIFIER", "Expected parameter name").value;
|
|
4236
|
+
let type = null;
|
|
4237
|
+
if (this.check("COLON")) {
|
|
4238
|
+
this.advance();
|
|
4239
|
+
type = this.expect("IDENTIFIER", "Expected type").value;
|
|
4240
|
+
}
|
|
4241
|
+
params.push({ name, type });
|
|
4242
|
+
if (this.check("COMMA")) this.advance();
|
|
4243
|
+
}
|
|
4244
|
+
this.expect("RPAREN", "Expected )");
|
|
4245
|
+
} else {
|
|
4246
|
+
if (this.check("SPREAD")) {
|
|
4247
|
+
this.advance();
|
|
4248
|
+
const name = this.expect("IDENTIFIER", "Expected parameter name after ...").value;
|
|
4249
|
+
params.push({ name, type: null, rest: true });
|
|
4250
|
+
} else {
|
|
4251
|
+
const name = this.expect("IDENTIFIER", "Expected parameter name").value;
|
|
4252
|
+
params.push({ name, type: null });
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
let returnType = null;
|
|
4256
|
+
if (this.check("COLON")) {
|
|
4257
|
+
this.advance();
|
|
4258
|
+
returnType = this.expect("IDENTIFIER", "Expected return type").value;
|
|
4259
|
+
}
|
|
4260
|
+
this.expect("ARROW", "Expected =>");
|
|
4261
|
+
let body;
|
|
4262
|
+
if (this.check("LBRACE")) {
|
|
4263
|
+
body = this.parseCodeBlock();
|
|
4264
|
+
} else {
|
|
4265
|
+
body = this.parseInlineExpression();
|
|
4266
|
+
}
|
|
4267
|
+
return { type: "arrow_function", params, returnType, body };
|
|
4268
|
+
}
|
|
4269
|
+
parseArray() {
|
|
4270
|
+
const arr = [];
|
|
4271
|
+
this.expect("LBRACKET", "Expected [");
|
|
4272
|
+
this.skipNewlines();
|
|
4273
|
+
while (!this.check("RBRACKET") && !this.check("EOF")) {
|
|
4274
|
+
const beforePos = this.pos;
|
|
4275
|
+
this.skipNewlines();
|
|
4276
|
+
if (this.check("RBRACKET") || this.check("EOF")) break;
|
|
4277
|
+
if (this.check("SPREAD")) {
|
|
4278
|
+
this.advance();
|
|
4279
|
+
const spreadArg = this.parseValue();
|
|
4280
|
+
if (spreadArg === null) {
|
|
4281
|
+
this.error(
|
|
4282
|
+
"Expected expression after spread operator (...) in array. Example: [...items]",
|
|
4283
|
+
"HSP300"
|
|
4284
|
+
);
|
|
4285
|
+
} else {
|
|
4286
|
+
arr.push({ type: "spread", argument: spreadArg });
|
|
4287
|
+
}
|
|
4288
|
+
} else {
|
|
4289
|
+
const value = this.parseValue();
|
|
4290
|
+
if (value !== null) {
|
|
4291
|
+
arr.push(value);
|
|
4292
|
+
} else if (this.pos === beforePos) {
|
|
4293
|
+
this.advance();
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
if (this.check("COMMA")) this.advance();
|
|
4297
|
+
this.skipNewlines();
|
|
4298
|
+
}
|
|
4299
|
+
this.expect("RBRACKET", "Expected ]");
|
|
4300
|
+
return arr;
|
|
4301
|
+
}
|
|
4302
|
+
parseObject() {
|
|
4303
|
+
const obj = {};
|
|
4304
|
+
this.expect("LBRACE", "Expected {");
|
|
4305
|
+
this.skipNewlines();
|
|
4306
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
4307
|
+
this.skipNewlines();
|
|
4308
|
+
if (this.check("RBRACE") || this.check("EOF")) break;
|
|
4309
|
+
if (this.check("SPREAD")) {
|
|
4310
|
+
this.advance();
|
|
4311
|
+
const spreadArg = this.parseValue();
|
|
4312
|
+
if (spreadArg === null) {
|
|
4313
|
+
this.error(
|
|
4314
|
+
"Expected expression after spread operator (...) in object. Example: {...template}",
|
|
4315
|
+
"HSP300"
|
|
4316
|
+
);
|
|
4317
|
+
} else {
|
|
4318
|
+
const spreadKey = `__spread_${Object.keys(obj).length}`;
|
|
4319
|
+
obj[spreadKey] = { type: "spread", argument: spreadArg };
|
|
4320
|
+
}
|
|
4321
|
+
} else if (this.check("IDENTIFIER")) {
|
|
4322
|
+
const key = this.advance().value;
|
|
4323
|
+
if (this.check("COLON") || this.check("EQUALS")) {
|
|
4324
|
+
this.advance();
|
|
4325
|
+
obj[key] = this.parseValue();
|
|
4326
|
+
} else {
|
|
4327
|
+
obj[key] = true;
|
|
4328
|
+
}
|
|
4329
|
+
} else {
|
|
4330
|
+
this.advance();
|
|
4331
|
+
continue;
|
|
4332
|
+
}
|
|
4333
|
+
if (this.check("COMMA")) this.advance();
|
|
4334
|
+
this.skipNewlines();
|
|
4335
|
+
}
|
|
4336
|
+
this.expect("RBRACE", "Expected }");
|
|
4337
|
+
return obj;
|
|
4338
|
+
}
|
|
4339
|
+
/**
|
|
4340
|
+
* Parse a match expression: match subject { pattern => body, ... }
|
|
4341
|
+
* Supports:
|
|
4342
|
+
* - Literal patterns: "idle", 42, true
|
|
4343
|
+
* - Wildcard pattern: _
|
|
4344
|
+
* - Binding patterns: x (captures value)
|
|
4345
|
+
* - Guard clauses: pattern if condition => body
|
|
4346
|
+
*/
|
|
4347
|
+
parseMatchExpression() {
|
|
4348
|
+
const startToken = this.current();
|
|
4349
|
+
this.expect("MATCH", "Expected match keyword");
|
|
4350
|
+
const subject = this.parseValue();
|
|
4351
|
+
if (subject === null) {
|
|
4352
|
+
this.error("Expected expression after match keyword", "HSP300");
|
|
4353
|
+
return null;
|
|
4354
|
+
}
|
|
4355
|
+
this.expect("LBRACE", "Expected { after match subject");
|
|
4356
|
+
this.skipNewlines();
|
|
4357
|
+
const cases = [];
|
|
4358
|
+
let hasWildcard = false;
|
|
4359
|
+
while (!this.check("RBRACE") && !this.check("EOF")) {
|
|
4360
|
+
this.skipNewlines();
|
|
4361
|
+
if (this.check("RBRACE")) break;
|
|
4362
|
+
const caseNode = this.parseMatchCase();
|
|
4363
|
+
if (caseNode) {
|
|
4364
|
+
cases.push(caseNode);
|
|
4365
|
+
if (caseNode.pattern && caseNode.pattern.type === "wildcard-pattern") {
|
|
4366
|
+
hasWildcard = true;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
if (this.check("COMMA")) this.advance();
|
|
4370
|
+
this.skipNewlines();
|
|
4371
|
+
}
|
|
4372
|
+
this.expect("RBRACE", "Expected } to close match expression");
|
|
4373
|
+
return {
|
|
4374
|
+
type: "match",
|
|
4375
|
+
subject,
|
|
4376
|
+
cases,
|
|
4377
|
+
hasWildcard,
|
|
4378
|
+
sourceLocation: {
|
|
4379
|
+
line: startToken.line,
|
|
4380
|
+
column: startToken.column
|
|
4381
|
+
}
|
|
4382
|
+
};
|
|
4383
|
+
}
|
|
4384
|
+
/**
|
|
4385
|
+
* Parse a single match case: pattern [if guard] => body
|
|
4386
|
+
*/
|
|
4387
|
+
parseMatchCase() {
|
|
4388
|
+
const pattern = this.parseMatchPattern();
|
|
4389
|
+
if (!pattern) {
|
|
4390
|
+
return null;
|
|
4391
|
+
}
|
|
4392
|
+
let guard;
|
|
4393
|
+
if (this.check("IDENTIFIER") && this.current().value === "if") {
|
|
4394
|
+
this.advance();
|
|
4395
|
+
let guardExpr = "";
|
|
4396
|
+
while (!this.check("ARROW") && !this.check("EOF") && !this.check("RBRACE")) {
|
|
4397
|
+
guardExpr += this.current().value + " ";
|
|
4398
|
+
this.advance();
|
|
4399
|
+
}
|
|
4400
|
+
guard = guardExpr.trim();
|
|
4401
|
+
}
|
|
4402
|
+
this.expect("ARROW", "Expected => after match pattern");
|
|
4403
|
+
let body;
|
|
4404
|
+
if (this.check("LBRACE")) {
|
|
4405
|
+
body = this.parseBlockContent();
|
|
4406
|
+
} else {
|
|
4407
|
+
body = this.parseValue();
|
|
4408
|
+
}
|
|
4409
|
+
return {
|
|
4410
|
+
type: "match-case",
|
|
4411
|
+
pattern,
|
|
4412
|
+
body,
|
|
4413
|
+
guard
|
|
4414
|
+
};
|
|
4415
|
+
}
|
|
4416
|
+
/**
|
|
4417
|
+
* Parse a match pattern:
|
|
4418
|
+
* - Literal: "string", 42, true
|
|
4419
|
+
* - Wildcard: _
|
|
4420
|
+
* - Binding: identifier
|
|
4421
|
+
*/
|
|
4422
|
+
parseMatchPattern() {
|
|
4423
|
+
const token = this.current();
|
|
4424
|
+
if (token.type === "UNDERSCORE" || token.type === "IDENTIFIER" && token.value === "_") {
|
|
4425
|
+
this.advance();
|
|
4426
|
+
return {
|
|
4427
|
+
type: "wildcard-pattern",
|
|
4428
|
+
symbol: "_"
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
if (token.type === "STRING") {
|
|
4432
|
+
this.advance();
|
|
4433
|
+
return {
|
|
4434
|
+
type: "literal-pattern",
|
|
4435
|
+
value: token.value
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4438
|
+
if (token.type === "NUMBER") {
|
|
4439
|
+
this.advance();
|
|
4440
|
+
return {
|
|
4441
|
+
type: "literal-pattern",
|
|
4442
|
+
value: parseFloat(token.value)
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
if (token.type === "BOOLEAN") {
|
|
4446
|
+
this.advance();
|
|
4447
|
+
return {
|
|
4448
|
+
type: "literal-pattern",
|
|
4449
|
+
value: token.value === "true"
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
if (token.type === "IDENTIFIER") {
|
|
4453
|
+
this.advance();
|
|
4454
|
+
return {
|
|
4455
|
+
type: "binding-pattern",
|
|
4456
|
+
name: token.value
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
if (token.type === "NULL") {
|
|
4460
|
+
this.advance();
|
|
4461
|
+
return {
|
|
4462
|
+
type: "literal-pattern",
|
|
4463
|
+
value: null
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
this.error(`Expected match pattern (literal, identifier, or _), got ${token.type}`, "HSP300");
|
|
4467
|
+
return null;
|
|
4468
|
+
}
|
|
4469
|
+
current() {
|
|
4470
|
+
return this.tokens[this.pos] || { type: "EOF", value: "", line: 0, column: 0 };
|
|
4471
|
+
}
|
|
4472
|
+
peek(offset = 0) {
|
|
4473
|
+
return this.tokens[this.pos + offset] || { type: "EOF", value: "", line: 0, column: 0 };
|
|
4474
|
+
}
|
|
4475
|
+
check(type) {
|
|
4476
|
+
return this.current().type === type;
|
|
4477
|
+
}
|
|
4478
|
+
advance() {
|
|
4479
|
+
const token = this.current();
|
|
4480
|
+
if (this.pos < this.tokens.length) {
|
|
4481
|
+
this.pos++;
|
|
4482
|
+
}
|
|
4483
|
+
return token;
|
|
4484
|
+
}
|
|
4485
|
+
previous() {
|
|
4486
|
+
return this.tokens[this.pos - 1] || this.current();
|
|
4487
|
+
}
|
|
4488
|
+
match(types) {
|
|
4489
|
+
for (const type of types) {
|
|
4490
|
+
if (this.check(type)) {
|
|
4491
|
+
return this.advance();
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
return null;
|
|
4495
|
+
}
|
|
4496
|
+
expect(type, message) {
|
|
4497
|
+
if (!this.check(type)) {
|
|
4498
|
+
const current = this.current();
|
|
4499
|
+
const fullMessage = `${message}. Got ${current.type} "${current.value}"`;
|
|
4500
|
+
let errorCode = "HSP001";
|
|
4501
|
+
let suggestion;
|
|
4502
|
+
switch (type) {
|
|
4503
|
+
case "RBRACE":
|
|
4504
|
+
errorCode = "HSP004";
|
|
4505
|
+
suggestion = "Check for matching opening brace { and ensure all blocks are properly closed";
|
|
4506
|
+
break;
|
|
4507
|
+
case "RBRACKET":
|
|
4508
|
+
errorCode = "HSP005";
|
|
4509
|
+
suggestion = "Check for matching opening bracket [ and ensure all arrays are properly closed";
|
|
4510
|
+
break;
|
|
4511
|
+
case "RPAREN":
|
|
4512
|
+
errorCode = "HSP006";
|
|
4513
|
+
suggestion = "Check for matching opening parenthesis ( and ensure all expressions are properly closed";
|
|
4514
|
+
break;
|
|
4515
|
+
case "COLON":
|
|
4516
|
+
errorCode = "HSP009";
|
|
4517
|
+
suggestion = "Properties use colon syntax: propertyName: value";
|
|
4518
|
+
break;
|
|
4519
|
+
case "IDENTIFIER":
|
|
4520
|
+
errorCode = "HSP002";
|
|
4521
|
+
if (current.type === "IDENTIFIER") {
|
|
4522
|
+
const similar = findSimilarKeyword(current.value);
|
|
4523
|
+
if (similar) {
|
|
4524
|
+
suggestion = `Did you mean '${similar}'?`;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
break;
|
|
4528
|
+
case "LBRACE":
|
|
4529
|
+
errorCode = "HSP100";
|
|
4530
|
+
suggestion = "Expected opening brace { to start block";
|
|
4531
|
+
break;
|
|
4532
|
+
default:
|
|
4533
|
+
errorCode = "HSP001";
|
|
4534
|
+
}
|
|
4535
|
+
if (suggestion) {
|
|
4536
|
+
this.errorWithSuggestion(fullMessage, suggestion, errorCode);
|
|
4537
|
+
} else {
|
|
4538
|
+
this.error(fullMessage, errorCode);
|
|
4539
|
+
}
|
|
4540
|
+
if (type === "RBRACE" || type === "LBRACE" || type === "IDENTIFIER") {
|
|
4541
|
+
this.synchronize();
|
|
4542
|
+
}
|
|
4543
|
+
return current;
|
|
4544
|
+
}
|
|
4545
|
+
return this.advance();
|
|
4546
|
+
}
|
|
4547
|
+
skipNewlines() {
|
|
4548
|
+
while (this.check("NEWLINE") || this.check("INDENT") || this.check("DEDENT")) {
|
|
4549
|
+
this.advance();
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
/** Skip a balanced parenthesised list ( ... ) including nested parens */
|
|
4553
|
+
skipParens() {
|
|
4554
|
+
if (!this.check("LPAREN")) return;
|
|
4555
|
+
this.advance();
|
|
4556
|
+
let depth = 1;
|
|
4557
|
+
while (depth > 0 && !this.check("EOF")) {
|
|
4558
|
+
if (this.check("LPAREN")) depth++;
|
|
4559
|
+
if (this.check("RPAREN")) depth--;
|
|
4560
|
+
this.advance();
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
skipBraces() {
|
|
4564
|
+
if (!this.check("LBRACE")) return;
|
|
4565
|
+
this.advance();
|
|
4566
|
+
let depth = 1;
|
|
4567
|
+
while (depth > 0 && !this.check("EOF")) {
|
|
4568
|
+
if (this.check("LBRACE")) depth++;
|
|
4569
|
+
if (this.check("RBRACE")) depth--;
|
|
4570
|
+
this.advance();
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
error(message, code = "HSP001") {
|
|
4574
|
+
const token = this.current();
|
|
4575
|
+
const line = token.line;
|
|
4576
|
+
const column = token.column;
|
|
4577
|
+
let suggestion;
|
|
4578
|
+
if (token.type === "IDENTIFIER") {
|
|
4579
|
+
const similar = findSimilarKeyword(token.value);
|
|
4580
|
+
if (similar) {
|
|
4581
|
+
suggestion = `Did you mean '${similar}'?`;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
this.errors.push(
|
|
4585
|
+
createRichError(code, message, line, column, {
|
|
4586
|
+
source: this.source,
|
|
4587
|
+
suggestion,
|
|
4588
|
+
severity: "error"
|
|
4589
|
+
})
|
|
4590
|
+
);
|
|
4591
|
+
}
|
|
4592
|
+
errorWithSuggestion(message, suggestion, code = "HSP001") {
|
|
4593
|
+
const token = this.current();
|
|
4594
|
+
this.errors.push(
|
|
4595
|
+
createRichError(code, message, token.line, token.column, {
|
|
4596
|
+
source: this.source,
|
|
4597
|
+
suggestion,
|
|
4598
|
+
severity: "error"
|
|
4599
|
+
})
|
|
4600
|
+
);
|
|
4601
|
+
}
|
|
4602
|
+
traitError(traitName) {
|
|
4603
|
+
const token = this.current();
|
|
4604
|
+
this.errors.push(createTraitError(traitName, token.line, token.column, this.source));
|
|
4605
|
+
}
|
|
4606
|
+
warn(message, code = "HSP001") {
|
|
4607
|
+
const token = this.current();
|
|
4608
|
+
this.warnings.push(
|
|
4609
|
+
createRichError(code, message, token.line, token.column, {
|
|
4610
|
+
source: this.source,
|
|
4611
|
+
severity: "warning"
|
|
4612
|
+
})
|
|
4613
|
+
);
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Detect common mistakes and provide context-aware error messages
|
|
4617
|
+
*/
|
|
4618
|
+
detectCommonMistake() {
|
|
4619
|
+
const current = this.current();
|
|
4620
|
+
const prev = this.pos > 0 ? this.tokens[this.pos - 1] : null;
|
|
4621
|
+
const next = this.pos + 1 < this.tokens.length ? this.tokens[this.pos + 1] : null;
|
|
4622
|
+
if (current.type === "EQUALS" && prev?.type === "IDENTIFIER") {
|
|
4623
|
+
return {
|
|
4624
|
+
message: `Unexpected '=' after property name '${prev.value}'`,
|
|
4625
|
+
suggestion: "Use ':' instead of '=' for property assignment. Example: propertyName: value",
|
|
4626
|
+
code: "HSP009"
|
|
4627
|
+
};
|
|
4628
|
+
}
|
|
4629
|
+
if (current.value === ";") {
|
|
4630
|
+
return {
|
|
4631
|
+
message: "Unexpected semicolon",
|
|
4632
|
+
suggestion: "HoloScript does not require semicolons. Remove the semicolon to continue.",
|
|
4633
|
+
code: "HSP001"
|
|
4634
|
+
};
|
|
4635
|
+
}
|
|
4636
|
+
if (current.type === "IDENTIFIER" && VR_TRAITS.includes(current.value)) {
|
|
4637
|
+
if (prev?.type !== "AT") {
|
|
4638
|
+
return {
|
|
4639
|
+
message: `'${current.value}' is a trait and requires '@' prefix`,
|
|
4640
|
+
suggestion: `Use '@${current.value}' to apply this trait`,
|
|
4641
|
+
code: "HSP200"
|
|
4642
|
+
};
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
if (current.type === "STRING" && next?.type === "COLON") {
|
|
4646
|
+
return {
|
|
4647
|
+
message: "Property names should not be quoted in HoloScript",
|
|
4648
|
+
suggestion: `Remove quotes: use ${current.value.replace(/['"]/g, "")}: instead of "${current.value}":`,
|
|
4649
|
+
code: "HSP101"
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4652
|
+
if (current.value === "function" && prev?.type === "COLON") {
|
|
4653
|
+
return {
|
|
4654
|
+
message: "Unexpected 'function' keyword",
|
|
4655
|
+
suggestion: "Use arrow function syntax: handler: (args) => { ... }",
|
|
4656
|
+
code: "HSP300"
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
if (current.type === "SPREAD" && next && (next.type === "RBRACE" || next.type === "RBRACKET" || next.type === "COMMA" || next.type === "NEWLINE")) {
|
|
4660
|
+
return {
|
|
4661
|
+
message: "Spread operator (...) requires a target expression",
|
|
4662
|
+
suggestion: "Provide an expression to spread: ...targetObject or ...targetArray",
|
|
4663
|
+
code: "HSP002"
|
|
4664
|
+
};
|
|
4665
|
+
}
|
|
4666
|
+
return null;
|
|
4667
|
+
}
|
|
4668
|
+
/**
|
|
4669
|
+
* Enhanced error reporting with common mistake detection
|
|
4670
|
+
*/
|
|
4671
|
+
errorWithContext(message, code = "HSP001") {
|
|
4672
|
+
const commonMistake = this.detectCommonMistake();
|
|
4673
|
+
if (commonMistake) {
|
|
4674
|
+
this.errorWithSuggestion(commonMistake.message, commonMistake.suggestion, commonMistake.code);
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
this.error(message, code);
|
|
4678
|
+
}
|
|
4679
|
+
/**
|
|
4680
|
+
* Synchronize parser state after an error
|
|
4681
|
+
* Skips tokens until a potential recovery point (newline followed by keyword/directive)
|
|
4682
|
+
*/
|
|
4683
|
+
synchronize() {
|
|
4684
|
+
this.advance();
|
|
4685
|
+
while (!this.check("EOF")) {
|
|
4686
|
+
if (this.check("RBRACE")) {
|
|
4687
|
+
return;
|
|
4688
|
+
}
|
|
4689
|
+
if (this.check("IDENTIFIER") && ["orb", "template", "logic", "object", "composition", "scene", "group"].includes(
|
|
4690
|
+
this.current().value
|
|
4691
|
+
)) {
|
|
4692
|
+
return;
|
|
4693
|
+
}
|
|
4694
|
+
if (this.check("AT")) {
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4697
|
+
this.advance();
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
/**
|
|
4701
|
+
* Mini-sync for property lists: skips until next line or comma
|
|
4702
|
+
*/
|
|
4703
|
+
synchronizeProperty() {
|
|
4704
|
+
this.advance();
|
|
4705
|
+
while (!this.check("EOF")) {
|
|
4706
|
+
if (this.check("RBRACE") || this.check("LBRACE")) {
|
|
4707
|
+
return;
|
|
4708
|
+
}
|
|
4709
|
+
if (this.check("IDENTIFIER") || this.check("STRING") || this.check("SPREAD") || this.check("AT")) {
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
if (this.check("NEWLINE")) {
|
|
4713
|
+
this.advance();
|
|
4714
|
+
this.skipNewlines();
|
|
4715
|
+
if (this.check("IDENTIFIER") || this.check("STRING") || this.check("SPREAD") || this.check("AT")) {
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
if (this.check("COMMA")) {
|
|
4720
|
+
this.advance();
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4723
|
+
this.advance();
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
/**
|
|
4727
|
+
* Synchronize for array context: skip to next valid array element or closing bracket
|
|
4728
|
+
*/
|
|
4729
|
+
synchronizeArray() {
|
|
4730
|
+
while (!this.check("EOF")) {
|
|
4731
|
+
if (this.check("RBRACKET")) {
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
if (this.check("COMMA")) {
|
|
4735
|
+
this.advance();
|
|
4736
|
+
this.skipNewlines();
|
|
4737
|
+
return;
|
|
4738
|
+
}
|
|
4739
|
+
if (this.check("NEWLINE")) {
|
|
4740
|
+
this.advance();
|
|
4741
|
+
this.skipNewlines();
|
|
4742
|
+
if (this.check("RBRACKET") || this.isLikelyValue(this.current()) || this.check("SPREAD")) {
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
this.advance();
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
/**
|
|
4750
|
+
* Synchronize for expression context: skip to end of expression
|
|
4751
|
+
*/
|
|
4752
|
+
synchronizeExpression() {
|
|
4753
|
+
let parenDepth = 0;
|
|
4754
|
+
let braceDepth = 0;
|
|
4755
|
+
let bracketDepth = 0;
|
|
4756
|
+
while (!this.check("EOF")) {
|
|
4757
|
+
const token = this.current();
|
|
4758
|
+
if (token.type === "LPAREN") parenDepth++;
|
|
4759
|
+
else if (token.type === "RPAREN") {
|
|
4760
|
+
if (parenDepth === 0) return;
|
|
4761
|
+
parenDepth--;
|
|
4762
|
+
} else if (token.type === "LBRACE") braceDepth++;
|
|
4763
|
+
else if (token.type === "RBRACE") {
|
|
4764
|
+
if (braceDepth === 0) return;
|
|
4765
|
+
braceDepth--;
|
|
4766
|
+
} else if (token.type === "LBRACKET") bracketDepth++;
|
|
4767
|
+
else if (token.type === "RBRACKET") {
|
|
4768
|
+
if (bracketDepth === 0) return;
|
|
4769
|
+
bracketDepth--;
|
|
4770
|
+
}
|
|
4771
|
+
if (parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
|
|
4772
|
+
if (this.check("NEWLINE") || this.check("COMMA")) {
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
this.advance();
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
/**
|
|
4780
|
+
* Attempt to recover from missing colon by looking ahead
|
|
4781
|
+
* Returns true if recovery was successful
|
|
4782
|
+
*/
|
|
4783
|
+
tryRecoverMissingColon() {
|
|
4784
|
+
if (this.check("IDENTIFIER")) {
|
|
4785
|
+
const nextPos = this.pos + 1;
|
|
4786
|
+
if (nextPos < this.tokens.length) {
|
|
4787
|
+
const nextToken = this.tokens[nextPos];
|
|
4788
|
+
if (this.isLikelyValue(nextToken) || nextToken.type === "IDENTIFIER") {
|
|
4789
|
+
this.warn(
|
|
4790
|
+
`Missing colon after property "${this.current().value}". Consider adding ':' between property name and value`,
|
|
4791
|
+
"HSP009"
|
|
4792
|
+
);
|
|
4793
|
+
return true;
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
return false;
|
|
4798
|
+
}
|
|
4799
|
+
/**
|
|
4800
|
+
* Helper to check if current token is a natural boundary after a value
|
|
4801
|
+
*/
|
|
4802
|
+
isValueBoundary() {
|
|
4803
|
+
return this.check("NEWLINE") || this.check("RBRACE") || this.check("COMMA") || this.check("IDENTIFIER") || this.check("SPREAD") || this.check("AT") || this.check("EOF");
|
|
4804
|
+
}
|
|
4805
|
+
/**
|
|
4806
|
+
* Helper to check if a token is likely the start of a value
|
|
4807
|
+
*/
|
|
4808
|
+
isLikelyValue(token) {
|
|
4809
|
+
return token.type === "STRING" || token.type === "NUMBER" || token.type === "LBRACKET" || token.type === "LBRACE" || token.type === "BOOLEAN" || token.type === "NULL" || token.type === "TEMPLATE_STRING";
|
|
4810
|
+
}
|
|
4811
|
+
/**
|
|
4812
|
+
* Parse a block of raw code (balanced braces)
|
|
4813
|
+
*/
|
|
4814
|
+
parseRawBlock() {
|
|
4815
|
+
const startToken = this.expect("LBRACE", "Expected {");
|
|
4816
|
+
const startOffset = startToken.offset + 1;
|
|
4817
|
+
let braceDepth = 1;
|
|
4818
|
+
let endOffset = startOffset;
|
|
4819
|
+
while (braceDepth > 0 && this.pos < this.tokens.length) {
|
|
4820
|
+
const token = this.current();
|
|
4821
|
+
if (token.type === "EOF") break;
|
|
4822
|
+
if (token.type === "LBRACE") {
|
|
4823
|
+
braceDepth++;
|
|
4824
|
+
} else if (token.type === "RBRACE") {
|
|
4825
|
+
braceDepth--;
|
|
4826
|
+
if (braceDepth === 0) {
|
|
4827
|
+
endOffset = token.offset;
|
|
4828
|
+
break;
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
this.advance();
|
|
4832
|
+
}
|
|
4833
|
+
this.expect("RBRACE", "Expected }");
|
|
4834
|
+
return this.source.substring(startOffset, endOffset).trim();
|
|
4835
|
+
}
|
|
4836
|
+
};
|
|
4837
|
+
function createParser(options) {
|
|
4838
|
+
return new HoloScriptPlusParser(options);
|
|
4839
|
+
}
|
|
4840
|
+
function parse(source, options) {
|
|
4841
|
+
const parser = createParser(options);
|
|
4842
|
+
return parser.parse(source);
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
export { ChunkBasedIncrementalParser, HSPLUS_ERROR_CODES, HoloScriptPlusParser, ParseCache, createKeywordError, createParser, createRichError, createTraitError, findSimilarKeyword, findSimilarTrait, formatRichError, formatRichErrors, getErrorCodeDocumentation, getSourceContext, globalParseCache, parse, parseIncrementalChunks };
|
|
4846
|
+
//# sourceMappingURL=chunk-YIMN575O.js.map
|
|
4847
|
+
//# sourceMappingURL=chunk-YIMN575O.js.map
|