@holoscript/core 1.0.0-alpha.2 → 2.0.1

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 (74) hide show
  1. package/package.json +2 -2
  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 +17 -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
@@ -67,6 +67,23 @@ export class HoloScriptCodeParser {
67
67
  private warnings: string[] = [];
68
68
  private tokens: Token[] = [];
69
69
  private position: number = 0;
70
+ private keywordSet: Set<string>;
71
+
72
+ constructor() {
73
+ // Pre-compute keyword set for O(1) lookup instead of O(n) array search
74
+ this.keywordSet = new Set([
75
+ 'orb', 'function', 'connect', 'to', 'as', 'gate', 'stream', 'from', 'through', 'return',
76
+ 'if', 'else', 'nexus', 'building', 'pillar', 'foundation',
77
+ 'for', 'while', 'forEach', 'in', 'of', 'break', 'continue',
78
+ 'import', 'export', 'module', 'use',
79
+ 'type', 'interface', 'extends', 'implements',
80
+ 'async', 'await', 'spawn', 'parallel',
81
+ 'class', 'new', 'this', 'super', 'static', 'private', 'public',
82
+ 'try', 'catch', 'finally', 'throw',
83
+ 'const', 'let', 'var',
84
+ 'animate', 'modify', 'pulse', 'move', 'show', 'hide',
85
+ ]);
86
+ }
70
87
 
71
88
  /**
72
89
  * Parse HoloScript code string into AST
@@ -132,25 +149,6 @@ export class HoloScriptCodeParser {
132
149
  let column = 1;
133
150
  let i = 0;
134
151
 
135
- const keywords = [
136
- 'orb', 'function', 'connect', 'to', 'as', 'gate', 'stream', 'from', 'through', 'return',
137
- 'if', 'else', 'nexus', 'building', 'pillar', 'foundation',
138
- // Phase 2: Loop constructs
139
- 'for', 'while', 'forEach', 'in', 'of', 'break', 'continue',
140
- // Phase 2: Module system
141
- 'import', 'export', 'module', 'use',
142
- // Phase 2: Type system
143
- 'type', 'interface', 'extends', 'implements',
144
- // Phase 2: Async
145
- 'async', 'await', 'spawn', 'parallel',
146
- // Phase 2: Object-oriented
147
- 'class', 'new', 'this', 'super', 'static', 'private', 'public',
148
- // Phase 2: Error handling
149
- 'try', 'catch', 'finally', 'throw',
150
- // Phase 2: Constants
151
- 'const', 'let', 'var',
152
- ];
153
-
154
152
  while (i < code.length) {
155
153
  const char = code[i];
156
154
 
@@ -231,7 +229,7 @@ export class HoloScriptCodeParser {
231
229
  column++;
232
230
  }
233
231
 
234
- const isKeyword = keywords.includes(ident.toLowerCase());
232
+ const isKeyword = this.keywordSet.has(ident.toLowerCase());
235
233
  tokens.push({
236
234
  type: isKeyword ? 'keyword' : 'identifier',
237
235
  value: ident,
@@ -341,10 +339,24 @@ export class HoloScriptCodeParser {
341
339
  case 'export':
342
340
  return this.parseExport();
343
341
  // Phase 2: Variable declarations
342
+ // UI Extensions
343
+ case 'ui2d':
344
+ case 'card':
345
+ case 'metric':
346
+ case 'button':
347
+ case 'row':
348
+ case 'col':
349
+ case 'text':
350
+ return this.parseUIElement();
344
351
  case 'const':
345
352
  case 'let':
346
353
  case 'var':
347
354
  return this.parseVariableDeclaration();
355
+ // DSL-first commands (Phase 54)
356
+ case 'animate':
357
+ return this.parseAnimate();
358
+ case 'modify':
359
+ return this.parseModify();
348
360
  default:
349
361
  this.advance();
350
362
  return null;
@@ -1055,6 +1067,119 @@ export class HoloScriptCodeParser {
1055
1067
  return null;
1056
1068
  }
1057
1069
 
1070
+ /**
1071
+ * Parse animate command: animate target property: "..." from: 0 to: 1 duration: 1000
1072
+ */
1073
+ private parseAnimate(): ASTNode | null {
1074
+ this.expect('keyword', 'animate');
1075
+ const target = this.expectIdentifier();
1076
+ if (!target) return null;
1077
+
1078
+ const properties: Record<string, unknown> = {};
1079
+
1080
+ // Parse inline properties
1081
+ while (this.position < this.tokens.length) {
1082
+ this.skipNewlines();
1083
+ const t = this.currentToken();
1084
+ if (!t || t.type === 'newline' || (t.type === 'keyword' && this.keywordSet.has(t.value.toLowerCase()))) break;
1085
+
1086
+ const prop = this.parseProperty();
1087
+ if (prop) {
1088
+ properties[prop.key] = prop.value;
1089
+ } else {
1090
+ break;
1091
+ }
1092
+ }
1093
+
1094
+ return {
1095
+ type: 'expression-statement',
1096
+ expression: `animate("${target}", ${JSON.stringify(properties)})`,
1097
+ position: { x: 0, y: 0, z: 0 },
1098
+ } as ASTNode;
1099
+ }
1100
+
1101
+ /**
1102
+ * Parse modify command: modify target { prop: value }
1103
+ */
1104
+ private parseModify(): ASTNode | null {
1105
+ this.expect('keyword', 'modify');
1106
+ const target = this.expectIdentifier();
1107
+ if (!target) return null;
1108
+
1109
+ const properties: Record<string, unknown> = {};
1110
+
1111
+ if (this.check('punctuation', '{')) {
1112
+ this.advance();
1113
+ while (!this.check('punctuation', '}') && this.position < this.tokens.length) {
1114
+ this.skipNewlines();
1115
+ if (this.check('punctuation', '}')) break;
1116
+
1117
+ const prop = this.parseProperty();
1118
+ if (prop) {
1119
+ properties[prop.key] = prop.value;
1120
+ }
1121
+ this.skipNewlines();
1122
+ }
1123
+ this.expect('punctuation', '}');
1124
+ }
1125
+
1126
+ return {
1127
+ type: 'expression-statement',
1128
+ expression: `modify("${target}", ${JSON.stringify(properties)})`,
1129
+ position: { x: 0, y: 0, z: 0 },
1130
+ } as ASTNode;
1131
+ }
1132
+
1133
+ /**
1134
+ * Parse UI Element: ui2d dashboard#id { ... }
1135
+ */
1136
+ private parseUIElement(): ASTNode | null {
1137
+ const typeToken = this.currentToken();
1138
+ if (!typeToken) return null;
1139
+
1140
+ const elementType = typeToken.value;
1141
+ this.advance();
1142
+
1143
+ let elementId = `${elementType}_${Date.now()}`;
1144
+
1145
+ // Check for ID syntax
1146
+ if (this.currentToken()?.type === 'punctuation' && this.currentToken()?.value === '#') {
1147
+ this.advance();
1148
+ const idToken = this.currentToken();
1149
+ if (idToken) {
1150
+ elementId = idToken.value;
1151
+ this.advance();
1152
+ }
1153
+ } else if (this.currentToken()?.type === 'identifier' && this.currentToken()?.value.startsWith('#')) {
1154
+ elementId = this.currentToken()?.value.slice(1) || elementId;
1155
+ this.advance();
1156
+ }
1157
+
1158
+ const properties: Record<string, any> = {};
1159
+
1160
+ if (this.check('punctuation', '{')) {
1161
+ this.advance();
1162
+ while (!this.check('punctuation', '}') && this.position < this.tokens.length) {
1163
+ this.skipNewlines();
1164
+ if (this.check('punctuation', '}')) break;
1165
+
1166
+ const prop = this.parseProperty();
1167
+ if (prop) {
1168
+ properties[prop.key] = prop.value;
1169
+ }
1170
+ this.skipNewlines();
1171
+ }
1172
+ this.expect('punctuation', '}');
1173
+ }
1174
+
1175
+ return {
1176
+ type: 'ui2d',
1177
+ name: elementType,
1178
+ properties: { id: elementId, ...properties },
1179
+ position: { x: 0, y: 0, z: 0 }
1180
+ } as ASTNode;
1181
+ }
1182
+
1058
1183
  private skipNewlines(): void {
1059
1184
  while (this.currentToken()?.type === 'newline') {
1060
1185
  this.advance();
@@ -0,0 +1,458 @@
1
+ /**
2
+ * HoloScript Debugger
3
+ *
4
+ * Step-through debugging with breakpoints, call stack inspection,
5
+ * and variable watch capabilities.
6
+ */
7
+ import { HoloScriptRuntime } from './HoloScriptRuntime';
8
+ import { HoloScriptCodeParser } from './HoloScriptCodeParser';
9
+ /**
10
+ * HoloScript Debugger with breakpoints and step-through execution
11
+ */
12
+ export class HoloScriptDebugger {
13
+ constructor(runtime) {
14
+ this.breakpoints = new Map();
15
+ this.callStack = [];
16
+ this.currentAST = [];
17
+ this.currentNodeIndex = 0;
18
+ this.frameIdCounter = 0;
19
+ this.breakpointIdCounter = 0;
20
+ this.eventHandlers = new Map();
21
+ this.state = {
22
+ status: 'stopped',
23
+ currentLine: 0,
24
+ currentColumn: 0,
25
+ currentNode: null,
26
+ callStack: [],
27
+ breakpoints: [],
28
+ };
29
+ this.runtime = runtime || new HoloScriptRuntime();
30
+ this.parser = new HoloScriptCodeParser();
31
+ }
32
+ /**
33
+ * Load source code for debugging
34
+ */
35
+ loadSource(code, file) {
36
+ const parseResult = this.parser.parse(code);
37
+ if (!parseResult.success) {
38
+ return {
39
+ success: false,
40
+ errors: parseResult.errors.map(e => `Line ${e.line}: ${e.message}`),
41
+ };
42
+ }
43
+ this.currentAST = parseResult.ast;
44
+ this.currentNodeIndex = 0;
45
+ this.state.status = 'stopped';
46
+ this.callStack = [];
47
+ // Store file reference in nodes
48
+ if (file) {
49
+ for (const node of this.currentAST) {
50
+ node.file = file;
51
+ }
52
+ }
53
+ return { success: true };
54
+ }
55
+ /**
56
+ * Set a breakpoint at a line
57
+ */
58
+ setBreakpoint(line, options = {}) {
59
+ const id = `bp_${++this.breakpointIdCounter}`;
60
+ const breakpoint = {
61
+ id,
62
+ line,
63
+ column: options.column,
64
+ condition: options.condition,
65
+ hitCount: 0,
66
+ enabled: options.enabled !== false,
67
+ file: options.file,
68
+ };
69
+ this.breakpoints.set(id, breakpoint);
70
+ this.updateBreakpointList();
71
+ return breakpoint;
72
+ }
73
+ /**
74
+ * Remove a breakpoint by ID
75
+ */
76
+ removeBreakpoint(id) {
77
+ const removed = this.breakpoints.delete(id);
78
+ if (removed) {
79
+ this.updateBreakpointList();
80
+ }
81
+ return removed;
82
+ }
83
+ /**
84
+ * Remove all breakpoints at a line
85
+ */
86
+ removeBreakpointsAtLine(line) {
87
+ let removed = 0;
88
+ for (const [id, bp] of this.breakpoints) {
89
+ if (bp.line === line) {
90
+ this.breakpoints.delete(id);
91
+ removed++;
92
+ }
93
+ }
94
+ if (removed > 0) {
95
+ this.updateBreakpointList();
96
+ }
97
+ return removed;
98
+ }
99
+ /**
100
+ * Toggle breakpoint enabled state
101
+ */
102
+ toggleBreakpoint(id) {
103
+ const bp = this.breakpoints.get(id);
104
+ if (bp) {
105
+ bp.enabled = !bp.enabled;
106
+ this.updateBreakpointList();
107
+ return bp.enabled;
108
+ }
109
+ return false;
110
+ }
111
+ /**
112
+ * Get all breakpoints
113
+ */
114
+ getBreakpoints() {
115
+ return Array.from(this.breakpoints.values());
116
+ }
117
+ /**
118
+ * Clear all breakpoints
119
+ */
120
+ clearBreakpoints() {
121
+ this.breakpoints.clear();
122
+ this.updateBreakpointList();
123
+ }
124
+ /**
125
+ * Start debugging from the beginning
126
+ */
127
+ async start() {
128
+ this.currentNodeIndex = 0;
129
+ this.callStack = [];
130
+ this.frameIdCounter = 0;
131
+ this.runtime.reset();
132
+ this.state.status = 'running';
133
+ await this.runUntilBreakpoint();
134
+ }
135
+ /**
136
+ * Continue execution until next breakpoint or end
137
+ */
138
+ async continue() {
139
+ if (this.state.status !== 'paused')
140
+ return;
141
+ this.state.status = 'running';
142
+ this.currentNodeIndex++;
143
+ await this.runUntilBreakpoint();
144
+ }
145
+ /**
146
+ * Step into the next node
147
+ */
148
+ async stepInto() {
149
+ if (this.state.status !== 'paused')
150
+ return;
151
+ this.state.status = 'stepping';
152
+ await this.executeStep('into');
153
+ }
154
+ /**
155
+ * Step over the current node
156
+ */
157
+ async stepOver() {
158
+ if (this.state.status !== 'paused')
159
+ return;
160
+ this.state.status = 'stepping';
161
+ await this.executeStep('over');
162
+ }
163
+ /**
164
+ * Step out of the current function
165
+ */
166
+ async stepOut() {
167
+ if (this.state.status !== 'paused')
168
+ return;
169
+ this.state.status = 'stepping';
170
+ await this.executeStep('out');
171
+ }
172
+ /**
173
+ * Stop debugging
174
+ */
175
+ stop() {
176
+ this.state.status = 'stopped';
177
+ this.callStack = [];
178
+ this.currentNodeIndex = 0;
179
+ this.emitEvent({ type: 'state-change', data: { status: 'stopped' } });
180
+ }
181
+ /**
182
+ * Pause execution
183
+ */
184
+ pause() {
185
+ if (this.state.status === 'running') {
186
+ this.state.status = 'paused';
187
+ this.emitEvent({ type: 'state-change', data: { status: 'paused' } });
188
+ }
189
+ }
190
+ /**
191
+ * Get current debug state
192
+ */
193
+ getState() {
194
+ return { ...this.state };
195
+ }
196
+ /**
197
+ * Get call stack
198
+ */
199
+ getCallStack() {
200
+ return [...this.callStack];
201
+ }
202
+ /**
203
+ * Get variables at a specific stack frame
204
+ */
205
+ getVariables(frameId) {
206
+ if (frameId !== undefined) {
207
+ const frame = this.callStack.find(f => f.id === frameId);
208
+ return frame?.variables || new Map();
209
+ }
210
+ // Return all variables from runtime context
211
+ return new Map(this.runtime.getContext().variables);
212
+ }
213
+ /**
214
+ * Evaluate an expression in the current context
215
+ */
216
+ async evaluate(expression) {
217
+ try {
218
+ // Try to evaluate as a variable lookup first
219
+ const varValue = this.runtime.getVariable(expression);
220
+ if (varValue !== undefined) {
221
+ return { value: varValue };
222
+ }
223
+ // Try to parse and execute as an expression
224
+ const parseResult = this.parser.parse(expression);
225
+ if (!parseResult.success) {
226
+ return { value: undefined, error: parseResult.errors[0]?.message };
227
+ }
228
+ // Execute the expression
229
+ const results = await this.runtime.executeProgram(parseResult.ast);
230
+ const lastResult = results[results.length - 1];
231
+ return { value: lastResult?.output };
232
+ }
233
+ catch (error) {
234
+ return { value: undefined, error: String(error) };
235
+ }
236
+ }
237
+ /**
238
+ * Set a watch expression
239
+ */
240
+ watch(expression) {
241
+ const id = `watch_${Date.now()}`;
242
+ return { id, expression };
243
+ }
244
+ /**
245
+ * Register an event handler
246
+ */
247
+ on(event, handler) {
248
+ if (!this.eventHandlers.has(event)) {
249
+ this.eventHandlers.set(event, []);
250
+ }
251
+ this.eventHandlers.get(event).push(handler);
252
+ }
253
+ /**
254
+ * Remove an event handler
255
+ */
256
+ off(event, handler) {
257
+ const handlers = this.eventHandlers.get(event);
258
+ if (handlers) {
259
+ const index = handlers.indexOf(handler);
260
+ if (index !== -1) {
261
+ handlers.splice(index, 1);
262
+ }
263
+ }
264
+ }
265
+ /**
266
+ * Get the underlying runtime
267
+ */
268
+ getRuntime() {
269
+ return this.runtime;
270
+ }
271
+ // Private methods
272
+ async runUntilBreakpoint() {
273
+ while (this.currentNodeIndex < this.currentAST.length) {
274
+ if (this.state.status === 'stopped')
275
+ break;
276
+ const node = this.currentAST[this.currentNodeIndex];
277
+ // Check for breakpoint
278
+ if (this.shouldBreakAt(node)) {
279
+ this.state.status = 'paused';
280
+ this.updateCurrentState(node);
281
+ const bp = this.findBreakpointAtLine(node.line ?? 0);
282
+ if (bp) {
283
+ bp.hitCount++;
284
+ this.emitEvent({
285
+ type: 'breakpoint-hit',
286
+ data: { breakpoint: bp, node, line: node.line ?? 0 },
287
+ });
288
+ }
289
+ return;
290
+ }
291
+ // Execute the node
292
+ await this.executeNode(node);
293
+ this.currentNodeIndex++;
294
+ }
295
+ // Execution complete
296
+ this.state.status = 'stopped';
297
+ this.emitEvent({ type: 'state-change', data: { status: 'stopped', reason: 'complete' } });
298
+ }
299
+ async executeStep(mode) {
300
+ const startStackDepth = this.callStack.length;
301
+ if (this.currentNodeIndex >= this.currentAST.length) {
302
+ this.state.status = 'stopped';
303
+ return;
304
+ }
305
+ const node = this.currentAST[this.currentNodeIndex];
306
+ switch (mode) {
307
+ case 'into':
308
+ // Execute one node, stepping into function calls
309
+ await this.executeNode(node);
310
+ this.currentNodeIndex++;
311
+ break;
312
+ case 'over':
313
+ // Execute one node, treating function calls as single steps
314
+ await this.executeNode(node);
315
+ this.currentNodeIndex++;
316
+ break;
317
+ case 'out':
318
+ // Execute until we leave the current stack frame
319
+ while (this.currentNodeIndex < this.currentAST.length) {
320
+ await this.executeNode(this.currentAST[this.currentNodeIndex]);
321
+ this.currentNodeIndex++;
322
+ if (this.callStack.length < startStackDepth) {
323
+ break;
324
+ }
325
+ }
326
+ break;
327
+ }
328
+ // Update state after step
329
+ if (this.currentNodeIndex < this.currentAST.length) {
330
+ this.state.status = 'paused';
331
+ this.updateCurrentState(this.currentAST[this.currentNodeIndex]);
332
+ this.emitEvent({
333
+ type: 'step-complete',
334
+ data: { mode, node: this.currentAST[this.currentNodeIndex] },
335
+ });
336
+ }
337
+ else {
338
+ this.state.status = 'stopped';
339
+ this.emitEvent({ type: 'state-change', data: { status: 'stopped', reason: 'complete' } });
340
+ }
341
+ }
342
+ async executeNode(node) {
343
+ // Push stack frame for functions
344
+ if (node.type === 'function') {
345
+ const funcNode = node;
346
+ this.pushStackFrame(funcNode.name, node);
347
+ }
348
+ try {
349
+ // Execute via runtime
350
+ const result = await this.runtime.executeNode(node);
351
+ if (!result.success && result.error) {
352
+ this.emitEvent({
353
+ type: 'exception',
354
+ data: { error: result.error, node, line: node.line },
355
+ });
356
+ }
357
+ if (result.output !== undefined) {
358
+ this.emitEvent({
359
+ type: 'output',
360
+ data: { output: result.output, node },
361
+ });
362
+ }
363
+ }
364
+ catch (error) {
365
+ this.emitEvent({
366
+ type: 'exception',
367
+ data: { error: String(error), node, line: node.line },
368
+ });
369
+ }
370
+ // Pop stack frame when function completes
371
+ if (node.type === 'function') {
372
+ this.popStackFrame();
373
+ }
374
+ }
375
+ shouldBreakAt(node) {
376
+ const line = node.line;
377
+ for (const bp of this.breakpoints.values()) {
378
+ if (!bp.enabled)
379
+ continue;
380
+ if (bp.line !== line)
381
+ continue;
382
+ // Check condition if present
383
+ if (bp.condition) {
384
+ try {
385
+ const value = this.runtime.getVariable(bp.condition);
386
+ if (!value)
387
+ continue;
388
+ }
389
+ catch {
390
+ continue;
391
+ }
392
+ }
393
+ return true;
394
+ }
395
+ return false;
396
+ }
397
+ findBreakpointAtLine(line) {
398
+ for (const bp of this.breakpoints.values()) {
399
+ if (bp.line === line && bp.enabled) {
400
+ return bp;
401
+ }
402
+ }
403
+ return null;
404
+ }
405
+ pushStackFrame(name, node) {
406
+ const frame = {
407
+ id: ++this.frameIdCounter,
408
+ name,
409
+ file: node.file,
410
+ line: node.line ?? 0,
411
+ column: node.column ?? 0,
412
+ variables: new Map(this.runtime.getContext().variables),
413
+ node,
414
+ };
415
+ this.callStack.push(frame);
416
+ this.state.callStack = [...this.callStack];
417
+ }
418
+ popStackFrame() {
419
+ const frame = this.callStack.pop();
420
+ this.state.callStack = [...this.callStack];
421
+ return frame;
422
+ }
423
+ updateCurrentState(node) {
424
+ this.state.currentLine = node.line ?? 0;
425
+ this.state.currentColumn = node.column ?? 0;
426
+ this.state.currentNode = node;
427
+ }
428
+ updateBreakpointList() {
429
+ this.state.breakpoints = Array.from(this.breakpoints.values());
430
+ }
431
+ emitEvent(event) {
432
+ const handlers = this.eventHandlers.get(event.type) || [];
433
+ for (const handler of handlers) {
434
+ try {
435
+ handler(event);
436
+ }
437
+ catch (error) {
438
+ console.error('Debug event handler error:', error);
439
+ }
440
+ }
441
+ // Also emit to 'all' handlers
442
+ const allHandlers = this.eventHandlers.get('all') || [];
443
+ for (const handler of allHandlers) {
444
+ try {
445
+ handler(event);
446
+ }
447
+ catch (error) {
448
+ console.error('Debug event handler error:', error);
449
+ }
450
+ }
451
+ }
452
+ }
453
+ /**
454
+ * Create a debugger instance
455
+ */
456
+ export function createDebugger(runtime) {
457
+ return new HoloScriptDebugger(runtime);
458
+ }