@holoscript/core 1.0.0-alpha.1 → 2.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.
Files changed (127) hide show
  1. package/package.json +10 -9
  2. package/src/HoloScript2DParser.js +227 -0
  3. package/src/HoloScript2DParser.ts +5 -0
  4. package/src/HoloScriptCodeParser.js +1102 -0
  5. package/src/HoloScriptCodeParser.ts +145 -20
  6. package/src/HoloScriptDebugger.js +458 -0
  7. package/src/HoloScriptParser.js +338 -0
  8. package/src/HoloScriptPlusParser.js +371 -0
  9. package/src/HoloScriptPlusParser.ts +543 -0
  10. package/src/HoloScriptRuntime.js +1399 -0
  11. package/src/HoloScriptRuntime.test.js +351 -0
  12. package/src/HoloScriptRuntime.ts +257 -3
  13. package/src/HoloScriptTypeChecker.js +356 -0
  14. package/src/__tests__/GraphicsServices.test.js +357 -0
  15. package/src/__tests__/GraphicsServices.test.ts +427 -0
  16. package/src/__tests__/HoloScriptPlusParser.test.js +317 -0
  17. package/src/__tests__/HoloScriptPlusParser.test.ts +392 -0
  18. package/src/__tests__/integration.test.js +336 -0
  19. package/src/__tests__/performance.bench.js +218 -0
  20. package/src/__tests__/type-checker.test.js +60 -0
  21. package/src/__tests__/type-checker.test.ts +73 -0
  22. package/src/index.js +217 -0
  23. package/src/index.ts +158 -18
  24. package/src/interop/Interoperability.js +413 -0
  25. package/src/interop/Interoperability.ts +494 -0
  26. package/src/logger.js +42 -0
  27. package/src/parser/EnhancedParser.js +205 -0
  28. package/src/parser/EnhancedParser.ts +251 -0
  29. package/src/parser/HoloScriptPlusParser.js +928 -0
  30. package/src/parser/HoloScriptPlusParser.ts +1089 -0
  31. package/src/runtime/HoloScriptPlusRuntime.js +674 -0
  32. package/src/runtime/HoloScriptPlusRuntime.ts +861 -0
  33. package/src/runtime/PerformanceTelemetry.js +323 -0
  34. package/src/runtime/PerformanceTelemetry.ts +467 -0
  35. package/src/runtime/RuntimeOptimization.js +361 -0
  36. package/src/runtime/RuntimeOptimization.ts +416 -0
  37. package/src/services/HololandGraphicsPipelineService.js +506 -0
  38. package/src/services/HololandGraphicsPipelineService.ts +662 -0
  39. package/src/services/PlatformPerformanceOptimizer.js +356 -0
  40. package/src/services/PlatformPerformanceOptimizer.ts +503 -0
  41. package/src/state/ReactiveState.js +427 -0
  42. package/src/state/ReactiveState.ts +572 -0
  43. package/src/tools/DeveloperExperience.js +376 -0
  44. package/src/tools/DeveloperExperience.ts +438 -0
  45. package/src/traits/AIDriverTrait.js +322 -0
  46. package/src/traits/AIDriverTrait.test.js +329 -0
  47. package/src/traits/AIDriverTrait.test.ts +357 -0
  48. package/src/traits/AIDriverTrait.ts +474 -0
  49. package/src/traits/LightingTrait.js +313 -0
  50. package/src/traits/LightingTrait.test.js +410 -0
  51. package/src/traits/LightingTrait.test.ts +462 -0
  52. package/src/traits/LightingTrait.ts +505 -0
  53. package/src/traits/MaterialTrait.js +194 -0
  54. package/src/traits/MaterialTrait.test.js +286 -0
  55. package/src/traits/MaterialTrait.test.ts +329 -0
  56. package/src/traits/MaterialTrait.ts +324 -0
  57. package/src/traits/RenderingTrait.js +356 -0
  58. package/src/traits/RenderingTrait.test.js +363 -0
  59. package/src/traits/RenderingTrait.test.ts +427 -0
  60. package/src/traits/RenderingTrait.ts +555 -0
  61. package/src/traits/VRTraitSystem.js +740 -0
  62. package/src/traits/VRTraitSystem.ts +1040 -0
  63. package/src/traits/VoiceInputTrait.js +284 -0
  64. package/src/traits/VoiceInputTrait.test.js +226 -0
  65. package/src/traits/VoiceInputTrait.test.ts +252 -0
  66. package/src/traits/VoiceInputTrait.ts +401 -0
  67. package/src/types/AdvancedTypeSystem.js +226 -0
  68. package/src/types/AdvancedTypeSystem.ts +494 -0
  69. package/src/types/HoloScriptPlus.d.ts +853 -0
  70. package/src/types.js +6 -0
  71. package/src/types.ts +96 -1
  72. package/tsconfig.json +1 -1
  73. package/tsup.config.d.ts +2 -0
  74. package/tsup.config.js +18 -0
  75. package/LICENSE +0 -21
  76. package/dist/chunk-3X2EGU7Z.cjs +0 -52
  77. package/dist/chunk-3X2EGU7Z.cjs.map +0 -1
  78. package/dist/chunk-723TPVHD.js +0 -1074
  79. package/dist/chunk-723TPVHD.js.map +0 -1
  80. package/dist/chunk-EOKNAVDO.cjs +0 -424
  81. package/dist/chunk-EOKNAVDO.cjs.map +0 -1
  82. package/dist/chunk-HQZ3HUMY.js +0 -1087
  83. package/dist/chunk-HQZ3HUMY.js.map +0 -1
  84. package/dist/chunk-KWYIVRIH.js +0 -344
  85. package/dist/chunk-KWYIVRIH.js.map +0 -1
  86. package/dist/chunk-LKH4ZAN6.js +0 -421
  87. package/dist/chunk-LKH4ZAN6.js.map +0 -1
  88. package/dist/chunk-SATNCODL.js +0 -45
  89. package/dist/chunk-SATNCODL.js.map +0 -1
  90. package/dist/chunk-VMZN4EVR.cjs +0 -347
  91. package/dist/chunk-VMZN4EVR.cjs.map +0 -1
  92. package/dist/chunk-VV3UUUYP.cjs +0 -1089
  93. package/dist/chunk-VV3UUUYP.cjs.map +0 -1
  94. package/dist/chunk-XRYTSQHZ.cjs +0 -1076
  95. package/dist/chunk-XRYTSQHZ.cjs.map +0 -1
  96. package/dist/debugger.cjs +0 -19
  97. package/dist/debugger.cjs.map +0 -1
  98. package/dist/debugger.d.cts +0 -171
  99. package/dist/debugger.d.ts +0 -171
  100. package/dist/debugger.js +0 -6
  101. package/dist/debugger.js.map +0 -1
  102. package/dist/index.cjs +0 -755
  103. package/dist/index.cjs.map +0 -1
  104. package/dist/index.d.cts +0 -169
  105. package/dist/index.d.ts +0 -169
  106. package/dist/index.js +0 -699
  107. package/dist/index.js.map +0 -1
  108. package/dist/parser.cjs +0 -13
  109. package/dist/parser.cjs.map +0 -1
  110. package/dist/parser.d.cts +0 -154
  111. package/dist/parser.d.ts +0 -154
  112. package/dist/parser.js +0 -4
  113. package/dist/parser.js.map +0 -1
  114. package/dist/runtime.cjs +0 -13
  115. package/dist/runtime.cjs.map +0 -1
  116. package/dist/runtime.d.cts +0 -147
  117. package/dist/runtime.d.ts +0 -147
  118. package/dist/runtime.js +0 -4
  119. package/dist/runtime.js.map +0 -1
  120. package/dist/type-checker.cjs +0 -16
  121. package/dist/type-checker.cjs.map +0 -1
  122. package/dist/type-checker.d.cts +0 -105
  123. package/dist/type-checker.d.ts +0 -105
  124. package/dist/type-checker.js +0 -3
  125. package/dist/type-checker.js.map +0 -1
  126. package/dist/types-WQSk1Qs2.d.cts +0 -238
  127. package/dist/types-WQSk1Qs2.d.ts +0 -238
@@ -0,0 +1,1089 @@
1
+ /**
2
+ * HoloScript+ Parser
3
+ *
4
+ * Parses HoloScript+ source code into an AST with support for:
5
+ * - Standard HoloScript syntax (backward compatible)
6
+ * - @ directive parsing for VR traits, state, control flow
7
+ * - Expression interpolation with ${...}
8
+ * - TypeScript companion imports
9
+ *
10
+ * @version 1.0.0
11
+ */
12
+
13
+ import type {
14
+ HSPlusAST,
15
+ HSPlusNode,
16
+ HSPlusDirective,
17
+ HSPlusCompileResult,
18
+ HSPlusParserOptions,
19
+ VRTraitName,
20
+ } from '../types/AdvancedTypeSystem';
21
+
22
+ // =============================================================================
23
+ // TOKEN TYPES
24
+ // =============================================================================
25
+
26
+ type TokenType =
27
+ | 'IDENTIFIER'
28
+ | 'STRING'
29
+ | 'NUMBER'
30
+ | 'BOOLEAN'
31
+ | 'NULL'
32
+ | 'LBRACE'
33
+ | 'RBRACE'
34
+ | 'LBRACKET'
35
+ | 'RBRACKET'
36
+ | 'LPAREN'
37
+ | 'RPAREN'
38
+ | 'COLON'
39
+ | 'COMMA'
40
+ | 'AT'
41
+ | 'HASH'
42
+ | 'DOT'
43
+ | 'EQUALS'
44
+ | 'ARROW'
45
+ | 'PIPE'
46
+ | 'EXPRESSION'
47
+ | 'COMMENT'
48
+ | 'NEWLINE'
49
+ | 'INDENT'
50
+ | 'DEDENT'
51
+ | 'EOF';
52
+
53
+ interface Token {
54
+ type: TokenType;
55
+ value: string;
56
+ line: number;
57
+ column: number;
58
+ }
59
+
60
+ // =============================================================================
61
+ // VR TRAITS
62
+ // =============================================================================
63
+
64
+ const VR_TRAITS: VRTraitName[] = [
65
+ 'grabbable',
66
+ 'throwable',
67
+ 'pointable',
68
+ 'hoverable',
69
+ 'scalable',
70
+ 'rotatable',
71
+ 'stackable',
72
+ 'snappable',
73
+ 'breakable',
74
+ ];
75
+
76
+ // =============================================================================
77
+ // LIFECYCLE HOOKS
78
+ // =============================================================================
79
+
80
+ type HookName = string;
81
+
82
+ const LIFECYCLE_HOOKS: HookName[] = [
83
+ // Standard lifecycle
84
+ 'on_mount',
85
+ 'on_unmount',
86
+ 'on_update',
87
+ 'on_data_update',
88
+ // VR lifecycle
89
+ 'on_grab',
90
+ 'on_release',
91
+ 'on_hover_enter',
92
+ 'on_hover_exit',
93
+ 'on_point_enter',
94
+ 'on_point_exit',
95
+ 'on_collision',
96
+ 'on_trigger_enter',
97
+ 'on_trigger_exit',
98
+ 'on_click',
99
+ 'on_double_click',
100
+ // Controller hooks
101
+ 'on_controller_button',
102
+ 'on_trigger_hold',
103
+ 'on_trigger_release',
104
+ 'on_grip_hold',
105
+ 'on_grip_release',
106
+ ];
107
+
108
+ // =============================================================================
109
+ // LEXER
110
+ // =============================================================================
111
+
112
+ class Lexer {
113
+ private source: string;
114
+ private pos: number = 0;
115
+ private line: number = 1;
116
+ private column: number = 1;
117
+ private indentStack: number[] = [0];
118
+ private tokens: Token[] = [];
119
+ private pendingDedents: number = 0;
120
+
121
+ constructor(source: string) {
122
+ this.source = source;
123
+ }
124
+
125
+ tokenize(): Token[] {
126
+ while (this.pos < this.source.length) {
127
+ // Handle pending dedents
128
+ while (this.pendingDedents > 0) {
129
+ this.tokens.push(this.createToken('DEDENT', ''));
130
+ this.pendingDedents--;
131
+ }
132
+
133
+ const char = this.source[this.pos];
134
+
135
+ // Skip whitespace (but track indentation at line start)
136
+ if (char === ' ' || char === '\t') {
137
+ if (this.column === 1) {
138
+ this.handleIndentation();
139
+ } else {
140
+ this.advance();
141
+ }
142
+ continue;
143
+ }
144
+
145
+ // Comments
146
+ if (char === '/' && this.peek(1) === '/') {
147
+ this.skipLineComment();
148
+ continue;
149
+ }
150
+ if (char === '/' && this.peek(1) === '*') {
151
+ this.skipBlockComment();
152
+ continue;
153
+ }
154
+ if (char === '#' && this.peek(1) !== '#') {
155
+ if (this.peek(1) === '#') {
156
+ this.skipLineComment();
157
+ continue;
158
+ }
159
+ }
160
+
161
+ // Newlines
162
+ if (char === '\n') {
163
+ this.tokens.push(this.createToken('NEWLINE', '\n'));
164
+ this.advance();
165
+ this.line++;
166
+ this.column = 1;
167
+ continue;
168
+ }
169
+ if (char === '\r') {
170
+ this.advance();
171
+ if (this.peek() === '\n') {
172
+ this.advance();
173
+ }
174
+ this.tokens.push(this.createToken('NEWLINE', '\n'));
175
+ this.line++;
176
+ this.column = 1;
177
+ continue;
178
+ }
179
+
180
+ // Symbols
181
+ if (char === '{') {
182
+ this.tokens.push(this.createToken('LBRACE', '{'));
183
+ this.advance();
184
+ continue;
185
+ }
186
+ if (char === '}') {
187
+ this.tokens.push(this.createToken('RBRACE', '}'));
188
+ this.advance();
189
+ continue;
190
+ }
191
+ if (char === '[') {
192
+ this.tokens.push(this.createToken('LBRACKET', '['));
193
+ this.advance();
194
+ continue;
195
+ }
196
+ if (char === ']') {
197
+ this.tokens.push(this.createToken('RBRACKET', ']'));
198
+ this.advance();
199
+ continue;
200
+ }
201
+ if (char === '(') {
202
+ this.tokens.push(this.createToken('LPAREN', '('));
203
+ this.advance();
204
+ continue;
205
+ }
206
+ if (char === ')') {
207
+ this.tokens.push(this.createToken('RPAREN', ')'));
208
+ this.advance();
209
+ continue;
210
+ }
211
+ if (char === ':') {
212
+ this.tokens.push(this.createToken('COLON', ':'));
213
+ this.advance();
214
+ continue;
215
+ }
216
+ if (char === ',') {
217
+ this.tokens.push(this.createToken('COMMA', ','));
218
+ this.advance();
219
+ continue;
220
+ }
221
+ if (char === '@') {
222
+ this.tokens.push(this.createToken('AT', '@'));
223
+ this.advance();
224
+ continue;
225
+ }
226
+ if (char === '#') {
227
+ this.tokens.push(this.createToken('HASH', '#'));
228
+ this.advance();
229
+ continue;
230
+ }
231
+ if (char === '.') {
232
+ this.tokens.push(this.createToken('DOT', '.'));
233
+ this.advance();
234
+ continue;
235
+ }
236
+ if (char === '=') {
237
+ if (this.peek(1) === '>') {
238
+ this.tokens.push(this.createToken('ARROW', '=>'));
239
+ this.advance();
240
+ this.advance();
241
+ continue;
242
+ }
243
+ this.tokens.push(this.createToken('EQUALS', '='));
244
+ this.advance();
245
+ continue;
246
+ }
247
+ if (char === '|') {
248
+ this.tokens.push(this.createToken('PIPE', '|'));
249
+ this.advance();
250
+ continue;
251
+ }
252
+
253
+ // Strings
254
+ if (char === '"' || char === "'") {
255
+ this.tokens.push(this.readString(char));
256
+ continue;
257
+ }
258
+
259
+ // Numbers
260
+ if (this.isDigit(char) || (char === '-' && this.isDigit(this.peek(1)))) {
261
+ this.tokens.push(this.readNumber());
262
+ continue;
263
+ }
264
+
265
+ // Expression interpolation ${...}
266
+ if (char === '$' && this.peek(1) === '{') {
267
+ this.tokens.push(this.readExpression());
268
+ continue;
269
+ }
270
+
271
+ // Identifiers and keywords
272
+ if (this.isIdentifierStart(char)) {
273
+ this.tokens.push(this.readIdentifier());
274
+ continue;
275
+ }
276
+
277
+ // Unknown character - skip
278
+ this.advance();
279
+ }
280
+
281
+ // Handle remaining dedents
282
+ while (this.indentStack.length > 1) {
283
+ this.tokens.push(this.createToken('DEDENT', ''));
284
+ this.indentStack.pop();
285
+ }
286
+
287
+ this.tokens.push(this.createToken('EOF', ''));
288
+ return this.tokens;
289
+ }
290
+
291
+ private advance(): string {
292
+ const char = this.source[this.pos];
293
+ this.pos++;
294
+ this.column++;
295
+ return char;
296
+ }
297
+
298
+ private peek(offset: number = 0): string {
299
+ const pos = this.pos + offset;
300
+ return pos < this.source.length ? this.source[pos] : '';
301
+ }
302
+
303
+ private createToken(type: TokenType, value: string): Token {
304
+ return {
305
+ type,
306
+ value,
307
+ line: this.line,
308
+ column: this.column - value.length,
309
+ };
310
+ }
311
+
312
+ private handleIndentation(): void {
313
+ let indent = 0;
314
+ while (this.peek() === ' ' || this.peek() === '\t') {
315
+ indent += this.peek() === '\t' ? 4 : 1;
316
+ this.advance();
317
+ }
318
+
319
+ if (this.peek() === '\n' || this.peek() === '\r') {
320
+ return;
321
+ }
322
+
323
+ const currentIndent = this.indentStack[this.indentStack.length - 1];
324
+
325
+ if (indent > currentIndent) {
326
+ this.indentStack.push(indent);
327
+ this.tokens.push(this.createToken('INDENT', ''));
328
+ } else if (indent < currentIndent) {
329
+ while (
330
+ this.indentStack.length > 1 &&
331
+ indent < this.indentStack[this.indentStack.length - 1]
332
+ ) {
333
+ this.indentStack.pop();
334
+ this.pendingDedents++;
335
+ }
336
+ }
337
+ }
338
+
339
+ private skipLineComment(): void {
340
+ while (this.peek() !== '\n' && this.pos < this.source.length) {
341
+ this.advance();
342
+ }
343
+ }
344
+
345
+ private skipBlockComment(): void {
346
+ this.advance(); // /
347
+ this.advance(); // *
348
+ while (this.pos < this.source.length) {
349
+ if (this.peek() === '*' && this.peek(1) === '/') {
350
+ this.advance();
351
+ this.advance();
352
+ break;
353
+ }
354
+ if (this.peek() === '\n') {
355
+ this.line++;
356
+ this.column = 0;
357
+ }
358
+ this.advance();
359
+ }
360
+ }
361
+
362
+ private readString(quote: string): Token {
363
+ const startLine = this.line;
364
+ const startColumn = this.column;
365
+ this.advance(); // Opening quote
366
+ let value = '';
367
+
368
+ while (this.peek() !== quote && this.pos < this.source.length) {
369
+ if (this.peek() === '\\') {
370
+ this.advance();
371
+ const escaped = this.advance();
372
+ switch (escaped) {
373
+ case 'n':
374
+ value += '\n';
375
+ break;
376
+ case 't':
377
+ value += '\t';
378
+ break;
379
+ case 'r':
380
+ value += '\r';
381
+ break;
382
+ case '\\':
383
+ value += '\\';
384
+ break;
385
+ case '"':
386
+ value += '"';
387
+ break;
388
+ case "'":
389
+ value += "'";
390
+ break;
391
+ default:
392
+ value += escaped;
393
+ }
394
+ } else if (this.peek() === '\n') {
395
+ this.line++;
396
+ this.column = 0;
397
+ value += this.advance();
398
+ } else {
399
+ value += this.advance();
400
+ }
401
+ }
402
+
403
+ this.advance(); // Closing quote
404
+ return {
405
+ type: 'STRING',
406
+ value,
407
+ line: startLine,
408
+ column: startColumn,
409
+ };
410
+ }
411
+
412
+ private readNumber(): Token {
413
+ const startColumn = this.column;
414
+ let value = '';
415
+
416
+ if (this.peek() === '-') {
417
+ value += this.advance();
418
+ }
419
+
420
+ while (this.isDigit(this.peek())) {
421
+ value += this.advance();
422
+ }
423
+
424
+ if (this.peek() === '.' && this.isDigit(this.peek(1))) {
425
+ value += this.advance(); // .
426
+ while (this.isDigit(this.peek())) {
427
+ value += this.advance();
428
+ }
429
+ }
430
+
431
+ // Scientific notation
432
+ if (this.peek() === 'e' || this.peek() === 'E') {
433
+ value += this.advance();
434
+ if (this.peek() === '+' || this.peek() === '-') {
435
+ value += this.advance();
436
+ }
437
+ while (this.isDigit(this.peek())) {
438
+ value += this.advance();
439
+ }
440
+ }
441
+
442
+ // Unit suffix
443
+ while (this.isAlpha(this.peek()) || this.peek() === '%') {
444
+ value += this.advance();
445
+ }
446
+
447
+ return {
448
+ type: 'NUMBER',
449
+ value,
450
+ line: this.line,
451
+ column: startColumn,
452
+ };
453
+ }
454
+
455
+ private readExpression(): Token {
456
+ const startLine = this.line;
457
+ const startColumn = this.column;
458
+ this.advance(); // $
459
+ this.advance(); // {
460
+
461
+ let value = '';
462
+ let braceDepth = 1;
463
+
464
+ while (braceDepth > 0 && this.pos < this.source.length) {
465
+ if (this.peek() === '{') {
466
+ braceDepth++;
467
+ } else if (this.peek() === '}') {
468
+ braceDepth--;
469
+ if (braceDepth === 0) {
470
+ break;
471
+ }
472
+ }
473
+ if (this.peek() === '\n') {
474
+ this.line++;
475
+ this.column = 0;
476
+ }
477
+ value += this.advance();
478
+ }
479
+
480
+ this.advance(); // Closing }
481
+
482
+ return {
483
+ type: 'EXPRESSION',
484
+ value: value.trim(),
485
+ line: startLine,
486
+ column: startColumn,
487
+ };
488
+ }
489
+
490
+ private readIdentifier(): Token {
491
+ const startColumn = this.column;
492
+ let value = '';
493
+
494
+ while (this.isIdentifierPart(this.peek())) {
495
+ value += this.advance();
496
+ }
497
+
498
+ if (value === 'true' || value === 'false') {
499
+ return {
500
+ type: 'BOOLEAN',
501
+ value,
502
+ line: this.line,
503
+ column: startColumn,
504
+ };
505
+ }
506
+ if (value === 'null' || value === 'none') {
507
+ return {
508
+ type: 'NULL',
509
+ value,
510
+ line: this.line,
511
+ column: startColumn,
512
+ };
513
+ }
514
+
515
+ return {
516
+ type: 'IDENTIFIER',
517
+ value,
518
+ line: this.line,
519
+ column: startColumn,
520
+ };
521
+ }
522
+
523
+ private isDigit(char: string): boolean {
524
+ return char >= '0' && char <= '9';
525
+ }
526
+
527
+ private isAlpha(char: string): boolean {
528
+ return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z');
529
+ }
530
+
531
+ private isIdentifierStart(char: string): boolean {
532
+ return this.isAlpha(char) || char === '_';
533
+ }
534
+
535
+ private isIdentifierPart(char: string): boolean {
536
+ return this.isIdentifierStart(char) || this.isDigit(char) || char === '-';
537
+ }
538
+ }
539
+
540
+ // =============================================================================
541
+ // PARSER
542
+ // =============================================================================
543
+
544
+ export class HoloScriptPlusParser {
545
+ private tokens: Token[] = [];
546
+ private pos: number = 0;
547
+ private options: HSPlusParserOptions;
548
+ private errors: Array<{ message: string; line: number; column: number }> = [];
549
+ private warnings: Array<{ message: string; line: number; column: number }> = [];
550
+ private imports: Array<{ path: string; alias: string }> = [];
551
+ private hasState: boolean = false;
552
+ private hasVRTraits: boolean = false;
553
+ private hasControlFlow: boolean = false;
554
+ private compiledExpressions: Map<string, string> = new Map();
555
+
556
+ constructor(options: HSPlusParserOptions = {}) {
557
+ this.options = {
558
+ enableVRTraits: true,
559
+ enableTypeScriptImports: true,
560
+ strict: false,
561
+ ...options,
562
+ };
563
+ }
564
+
565
+ parse(source: string): HSPlusCompileResult {
566
+ // Reset state
567
+ this.errors = [];
568
+ this.warnings = [];
569
+ this.imports = [];
570
+ this.hasState = false;
571
+ this.hasVRTraits = false;
572
+ this.hasControlFlow = false;
573
+ this.compiledExpressions = new Map();
574
+ this.pos = 0;
575
+
576
+ // Tokenize
577
+ const lexer = new Lexer(source);
578
+ this.tokens = lexer.tokenize();
579
+
580
+ // Parse root node
581
+ const root = this.parseDocument();
582
+
583
+ // Build AST
584
+ const ast: HSPlusAST = {
585
+ type: 'Program',
586
+ body: root.directives || [],
587
+ version: '1.0',
588
+ root,
589
+ imports: this.imports,
590
+ hasState: this.hasState,
591
+ hasVRTraits: this.hasVRTraits,
592
+ hasControlFlow: this.hasControlFlow,
593
+ };
594
+
595
+ return {
596
+ success: true,
597
+ ast,
598
+ compiledExpressions: this.compiledExpressions,
599
+ requiredCompanions: this.imports.map((i) => i.path),
600
+ features: {
601
+ state: this.hasState,
602
+ vrTraits: this.hasVRTraits,
603
+ loops: this.hasControlFlow,
604
+ conditionals: this.hasControlFlow,
605
+ lifecycleHooks: root.directives.some((d: any) => d.type === 'lifecycle'),
606
+ },
607
+ warnings: this.warnings,
608
+ errors: this.errors,
609
+ };
610
+ }
611
+
612
+ private parseDocument(): HSPlusNode {
613
+ this.skipNewlines();
614
+
615
+ const directives: HSPlusDirective[] = [];
616
+ while (this.check('AT')) {
617
+ const directive = this.parseDirective();
618
+ if (directive) {
619
+ directives.push(directive);
620
+ }
621
+ this.skipNewlines();
622
+ }
623
+
624
+ const root = this.parseNode();
625
+ root.directives = [...directives, ...root.directives];
626
+
627
+ return root;
628
+ }
629
+
630
+ private parseNode(): HSPlusNode {
631
+ const startToken = this.current();
632
+
633
+ const type = this.expect('IDENTIFIER', 'Expected element type').value;
634
+
635
+ let id: string | undefined;
636
+ if (this.check('HASH')) {
637
+ this.advance();
638
+ id = this.expect('IDENTIFIER', 'Expected ID after #').value;
639
+ }
640
+
641
+ const properties: Record<string, unknown> = {};
642
+ const directives: HSPlusDirective[] = [];
643
+ const traits = new Map<VRTraitName, unknown>();
644
+
645
+ while (!this.check('LBRACE') && !this.check('NEWLINE') && !this.check('EOF')) {
646
+ if (this.check('AT')) {
647
+ const directive = this.parseDirective();
648
+ if (directive) {
649
+ if (directive.type === 'trait') {
650
+ traits.set(directive.name as VRTraitName, (directive as any).config);
651
+ this.hasVRTraits = true;
652
+ } else {
653
+ directives.push(directive);
654
+ }
655
+ }
656
+ } else if (this.check('IDENTIFIER')) {
657
+ const key = this.advance().value;
658
+ if (this.check('COLON') || this.check('EQUALS')) {
659
+ this.advance();
660
+ properties[key] = this.parseValue();
661
+ } else {
662
+ properties[key] = true;
663
+ }
664
+ } else {
665
+ break;
666
+ }
667
+ }
668
+
669
+ const children: HSPlusNode[] = [];
670
+ if (this.check('LBRACE')) {
671
+ this.advance();
672
+ this.skipNewlines();
673
+
674
+ while (!this.check('RBRACE') && !this.check('EOF')) {
675
+ if (this.check('AT')) {
676
+ const directive = this.parseDirective();
677
+ if (directive) {
678
+ if (directive.type === 'trait') {
679
+ traits.set(directive.name as VRTraitName, (directive as any).config);
680
+ this.hasVRTraits = true;
681
+ } else {
682
+ directives.push(directive);
683
+ }
684
+ }
685
+ } else if (this.check('IDENTIFIER')) {
686
+ const saved = this.pos;
687
+ const name = this.advance().value;
688
+
689
+ if (this.check('COLON') || this.check('EQUALS')) {
690
+ this.advance();
691
+ properties[name] = this.parseValue();
692
+ } else {
693
+ this.pos = saved;
694
+ children.push(this.parseNode());
695
+ }
696
+ } else {
697
+ this.skipNewlines();
698
+ if (this.check('RBRACE') || this.check('EOF')) break;
699
+ this.advance();
700
+ }
701
+ this.skipNewlines();
702
+ }
703
+
704
+ this.expect('RBRACE', 'Expected }');
705
+ }
706
+
707
+ return {
708
+ type: type as any,
709
+ id,
710
+ properties,
711
+ directives,
712
+ children,
713
+ traits,
714
+ loc: {
715
+ start: { line: startToken.line, column: startToken.column },
716
+ end: { line: this.current().line, column: this.current().column },
717
+ },
718
+ };
719
+ }
720
+
721
+ private parseDirective(): HSPlusDirective | null {
722
+ this.expect('AT', 'Expected @');
723
+ const name = this.expect('IDENTIFIER', 'Expected directive name').value;
724
+
725
+ if (VR_TRAITS.includes(name as VRTraitName)) {
726
+ if (!this.options.enableVRTraits) {
727
+ this.warn(`VR trait @${name} is disabled`);
728
+ return null;
729
+ }
730
+ const config = this.parseTraitConfig();
731
+ return { type: 'trait' as const, name: name as VRTraitName, config } as any;
732
+ }
733
+
734
+ if (LIFECYCLE_HOOKS.includes(name)) {
735
+ const params: string[] = [];
736
+ if (this.check('LPAREN')) {
737
+ this.advance();
738
+ while (!this.check('RPAREN') && !this.check('EOF')) {
739
+ params.push(this.expect('IDENTIFIER', 'Expected parameter name').value);
740
+ if (this.check('COMMA')) this.advance();
741
+ }
742
+ this.expect('RPAREN', 'Expected )');
743
+ }
744
+
745
+ let body = '';
746
+ if (this.check('ARROW')) {
747
+ this.advance();
748
+ body = this.parseInlineExpression();
749
+ } else if (this.check('LBRACE')) {
750
+ body = this.parseCodeBlock();
751
+ }
752
+
753
+ return {
754
+ type: 'lifecycle' as const,
755
+ hook: name,
756
+ params,
757
+ body,
758
+ } as any;
759
+ }
760
+
761
+ if (name === 'state') {
762
+ this.hasState = true;
763
+ const body = this.parseStateBlock();
764
+ return { type: 'state' as const, body } as any;
765
+ }
766
+
767
+ if (name === 'for') {
768
+ this.hasControlFlow = true;
769
+ const variable = this.expect('IDENTIFIER', 'Expected variable name').value;
770
+ this.expect('IDENTIFIER', 'Expected "in"');
771
+ const iterable = this.parseInlineExpression();
772
+ const body = this.parseControlFlowBody();
773
+ return { type: 'for' as const, variable, iterable, body } as any;
774
+ }
775
+
776
+ if (name === 'if') {
777
+ this.hasControlFlow = true;
778
+ const condition = this.parseInlineExpression();
779
+ const body = this.parseControlFlowBody();
780
+ let elseBody: HSPlusNode[] | undefined;
781
+
782
+ this.skipNewlines();
783
+ if (this.check('AT')) {
784
+ const saved = this.pos;
785
+ this.advance();
786
+ if (this.check('IDENTIFIER') && this.current().value === 'else') {
787
+ this.advance();
788
+ elseBody = this.parseControlFlowBody();
789
+ } else {
790
+ this.pos = saved;
791
+ }
792
+ }
793
+
794
+ return { type: 'if' as const, condition, body, else: elseBody } as any;
795
+ }
796
+
797
+ if (name === 'import') {
798
+ if (!this.options.enableTypeScriptImports) {
799
+ this.warn('@import is disabled');
800
+ return null;
801
+ }
802
+ const path = this.expect('STRING', 'Expected import path').value;
803
+ let alias = path.split('/').pop()?.replace(/\.[^.]+$/, '') || 'import';
804
+ if (this.check('IDENTIFIER') && this.current().value === 'as') {
805
+ this.advance();
806
+ alias = this.expect('IDENTIFIER', 'Expected alias').value;
807
+ }
808
+ this.imports.push({ path, alias });
809
+ return { type: 'import' as const, path, alias } as any;
810
+ }
811
+
812
+ if (this.options.strict) {
813
+ this.error(`Unknown directive @${name}`);
814
+ } else {
815
+ this.warn(`Unknown directive @${name}`);
816
+ }
817
+ return null;
818
+ }
819
+
820
+ private parseTraitConfig(): Record<string, unknown> {
821
+ const config: Record<string, unknown> = {};
822
+
823
+ if (this.check('LPAREN')) {
824
+ this.advance();
825
+ while (!this.check('RPAREN') && !this.check('EOF')) {
826
+ const key = this.expect('IDENTIFIER', 'Expected property name').value;
827
+ if (this.check('COLON') || this.check('EQUALS')) {
828
+ this.advance();
829
+ config[key] = this.parseValue();
830
+ } else {
831
+ config[key] = true;
832
+ }
833
+ if (this.check('COMMA')) this.advance();
834
+ }
835
+ this.expect('RPAREN', 'Expected )');
836
+ }
837
+
838
+ return config;
839
+ }
840
+
841
+ private parseStateBlock(): Record<string, any> {
842
+ const state: Record<string, any> = {};
843
+
844
+ if (this.check('LBRACE')) {
845
+ this.advance();
846
+ this.skipNewlines();
847
+
848
+ while (!this.check('RBRACE') && !this.check('EOF')) {
849
+ const key = this.expect('IDENTIFIER', 'Expected state variable name').value;
850
+ if (this.check('COLON') || this.check('EQUALS')) {
851
+ this.advance();
852
+ state[key] = this.parseValue();
853
+ } else {
854
+ state[key] = null;
855
+ }
856
+ this.skipNewlines();
857
+ }
858
+
859
+ this.expect('RBRACE', 'Expected }');
860
+ }
861
+
862
+ return state;
863
+ }
864
+
865
+ private parseControlFlowBody(): HSPlusNode[] {
866
+ const nodes: HSPlusNode[] = [];
867
+
868
+ if (this.check('LBRACE')) {
869
+ this.advance();
870
+ this.skipNewlines();
871
+
872
+ while (!this.check('RBRACE') && !this.check('EOF')) {
873
+ if (this.check('AT')) {
874
+ const directive = this.parseDirective();
875
+ if (directive && directive.type === 'for') {
876
+ nodes.push({
877
+ type: 'fragment',
878
+ properties: {},
879
+ directives: [directive],
880
+ children: [],
881
+ traits: new Map(),
882
+ });
883
+ }
884
+ } else if (this.check('IDENTIFIER')) {
885
+ nodes.push(this.parseNode());
886
+ }
887
+ this.skipNewlines();
888
+ }
889
+
890
+ this.expect('RBRACE', 'Expected }');
891
+ }
892
+
893
+ return nodes;
894
+ }
895
+
896
+ private parseCodeBlock(): string {
897
+ let code = '';
898
+ let braceDepth = 0;
899
+
900
+ if (this.check('LBRACE')) {
901
+ this.advance();
902
+ braceDepth = 1;
903
+
904
+ while (braceDepth > 0 && !this.check('EOF')) {
905
+ const token = this.advance();
906
+ if (token.type === 'LBRACE') {
907
+ braceDepth++;
908
+ code += '{';
909
+ } else if (token.type === 'RBRACE') {
910
+ braceDepth--;
911
+ if (braceDepth > 0) {
912
+ code += '}';
913
+ }
914
+ } else {
915
+ code += token.value;
916
+ if (token.type === 'NEWLINE') {
917
+ code += '\n';
918
+ } else {
919
+ code += ' ';
920
+ }
921
+ }
922
+ }
923
+ }
924
+
925
+ return code.trim();
926
+ }
927
+
928
+ private parseInlineExpression(): string {
929
+ let expr = '';
930
+
931
+ while (
932
+ !this.check('NEWLINE') &&
933
+ !this.check('LBRACE') &&
934
+ !this.check('EOF')
935
+ ) {
936
+ const token = this.advance();
937
+ expr += token.value + ' ';
938
+ }
939
+
940
+ return expr.trim();
941
+ }
942
+
943
+ private parseValue(): unknown {
944
+ const token = this.current();
945
+
946
+ if (token.type === 'STRING') {
947
+ this.advance();
948
+ return token.value;
949
+ }
950
+
951
+ if (token.type === 'NUMBER') {
952
+ this.advance();
953
+ const match = token.value.match(/^(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)(.*)?$/i);
954
+ if (match) {
955
+ const num = parseFloat(match[1]);
956
+ const unit = match[2];
957
+ if (unit) {
958
+ return `${num}${unit}`;
959
+ }
960
+ return num;
961
+ }
962
+ return parseFloat(token.value);
963
+ }
964
+
965
+ if (token.type === 'BOOLEAN') {
966
+ this.advance();
967
+ return token.value === 'true';
968
+ }
969
+
970
+ if (token.type === 'NULL') {
971
+ this.advance();
972
+ return null;
973
+ }
974
+
975
+ if (token.type === 'EXPRESSION') {
976
+ this.advance();
977
+ const exprId = `expr_${this.compiledExpressions.size}`;
978
+ this.compiledExpressions.set(exprId, token.value);
979
+ return { __expr: exprId, __raw: token.value };
980
+ }
981
+
982
+ if (token.type === 'LBRACKET') {
983
+ return this.parseArray();
984
+ }
985
+
986
+ if (token.type === 'LBRACE') {
987
+ return this.parseObject();
988
+ }
989
+
990
+ if (token.type === 'IDENTIFIER') {
991
+ this.advance();
992
+ return { __ref: token.value };
993
+ }
994
+
995
+ return null;
996
+ }
997
+
998
+ private parseArray(): unknown[] {
999
+ const arr: unknown[] = [];
1000
+ this.expect('LBRACKET', 'Expected [');
1001
+
1002
+ while (!this.check('RBRACKET') && !this.check('EOF')) {
1003
+ arr.push(this.parseValue());
1004
+ if (this.check('COMMA')) this.advance();
1005
+ this.skipNewlines();
1006
+ }
1007
+
1008
+ this.expect('RBRACKET', 'Expected ]');
1009
+ return arr;
1010
+ }
1011
+
1012
+ private parseObject(): Record<string, unknown> {
1013
+ const obj: Record<string, unknown> = {};
1014
+ this.expect('LBRACE', 'Expected {');
1015
+ this.skipNewlines();
1016
+
1017
+ while (!this.check('RBRACE') && !this.check('EOF')) {
1018
+ const key = this.expect('IDENTIFIER', 'Expected property name').value;
1019
+ if (this.check('COLON') || this.check('EQUALS')) {
1020
+ this.advance();
1021
+ obj[key] = this.parseValue();
1022
+ } else {
1023
+ obj[key] = true;
1024
+ }
1025
+ if (this.check('COMMA')) this.advance();
1026
+ this.skipNewlines();
1027
+ }
1028
+
1029
+ this.expect('RBRACE', 'Expected }');
1030
+ return obj;
1031
+ }
1032
+
1033
+ private current(): Token {
1034
+ return this.tokens[this.pos] || { type: 'EOF', value: '', line: 0, column: 0 };
1035
+ }
1036
+
1037
+ private check(type: TokenType): boolean {
1038
+ return this.current().type === type;
1039
+ }
1040
+
1041
+ private advance(): Token {
1042
+ const token = this.current();
1043
+ if (this.pos < this.tokens.length) {
1044
+ this.pos++;
1045
+ }
1046
+ return token;
1047
+ }
1048
+
1049
+ private expect(type: TokenType, message: string): Token {
1050
+ if (!this.check(type)) {
1051
+ this.error(`${message}. Got ${this.current().type} "${this.current().value}"`);
1052
+ return { type, value: '', line: this.current().line, column: this.current().column };
1053
+ }
1054
+ return this.advance();
1055
+ }
1056
+
1057
+ private skipNewlines(): void {
1058
+ while (this.check('NEWLINE') || this.check('INDENT') || this.check('DEDENT')) {
1059
+ this.advance();
1060
+ }
1061
+ }
1062
+
1063
+ private error(message: string): void {
1064
+ const token = this.current();
1065
+ this.errors.push({
1066
+ message,
1067
+ line: token.line,
1068
+ column: token.column,
1069
+ });
1070
+ }
1071
+
1072
+ private warn(message: string): void {
1073
+ const token = this.current();
1074
+ this.warnings.push({
1075
+ message,
1076
+ line: token.line,
1077
+ column: token.column,
1078
+ });
1079
+ }
1080
+ }
1081
+
1082
+ export function createParser(options?: HSPlusParserOptions): HoloScriptPlusParser {
1083
+ return new HoloScriptPlusParser(options);
1084
+ }
1085
+
1086
+ export function parse(source: string, options?: HSPlusParserOptions): HSPlusCompileResult {
1087
+ const parser = createParser(options);
1088
+ return parser.parse(source);
1089
+ }