novac 1.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.
@@ -0,0 +1,1005 @@
1
+ const {
2
+ NovaValue,
3
+ NovaNumber,
4
+ NovaString,
5
+ NovaArray,
6
+ NovaObject,
7
+ NovaFunction,
8
+ NovaTemplateString,
9
+ NovaPointer,
10
+ NovaBool,
11
+ NovaNull,
12
+ } = require("./types.js");
13
+ const { CustomError, formatError, NovaException } = require("./error");
14
+ class Scope {
15
+ constructor(kind = "block", parent = null, globalScope = null) {
16
+ this.kind = kind;
17
+ this.parent = parent;
18
+ this.globalScope = globalScope;
19
+ this.variables = {
20
+ scope: this,
21
+ array: {
22
+ toCallable: () => (values) => new NovaArray(values)
23
+ }
24
+ };
25
+ this.consts = {};
26
+ this.prototypes = []; // array of other Scopes or JS objects
27
+ }
28
+
29
+ get(name, prop = null) {
30
+ let value = null;
31
+ if (name in this.variables) value = this.variables[name];
32
+
33
+ // Look through prototypes
34
+ for (const proto of this.prototypes) {
35
+ if (proto instanceof Scope) {
36
+ // Look up properties directly in the variables of the prototype scope
37
+ if (proto.parent || proto.globalScope) {
38
+ const val = proto.get(name);
39
+ if (val !== undefined) value = val;
40
+ }
41
+ } else if (proto && name in proto) {
42
+ value = proto[name];
43
+ }
44
+ }
45
+
46
+ if (!value && this.globalScope) value = this.globalScope.get(name);
47
+ if (!value && this.parent) value = this.parent.get(name);
48
+
49
+ if (value?.read) return value?.read?.();
50
+
51
+ return prop
52
+ ? value instanceof Scope
53
+ ? value.get(prop)
54
+ : value[prop]
55
+ : value;
56
+ }
57
+
58
+ delete(name) {
59
+ if (name in this.variables) delete this.variables[name];
60
+
61
+ if (this.parent) this.parent.delete(name);
62
+ if (this.globalScope) this.globalScope.delete(name);
63
+
64
+ return undefined;
65
+ }
66
+
67
+ set(name, value, isConst) {
68
+ if (this.variables?.[name]?.write) {
69
+ return (this.variables?.[name]?.write)(value);
70
+ }
71
+ if (
72
+ this.consts.hasOwnProperty(name) &&
73
+ this.variables.hasOwnProperty(name)
74
+ ) {
75
+ throw "Cannot reassign a const.";
76
+ }
77
+ if (isConst) {
78
+ this.consts[name] = value;
79
+ this.variables[name] = value;
80
+ } else this.variables[name] = value;
81
+ return value;
82
+ }
83
+
84
+ has(name) {
85
+ return name in this.variables;
86
+ }
87
+
88
+ toString() {
89
+ return `${this.kind.toUpperCase()}`;
90
+ }
91
+ }
92
+
93
+ class Executor {
94
+ // In Executor constructor:
95
+ constructor(source, stdlib = {}) {
96
+ this.rawsrc = source;
97
+ this.taskQueue = [];
98
+ this.isProcessing = false;
99
+ this.globalScope = new Scope("global");
100
+ this.globalScope.variables.std = stdlib;
101
+
102
+ // Register setTimeout here
103
+ const self = this;
104
+ this.globalScope.set("setTimeout", (fn, delay) => {
105
+ // fn: The function to call (either native JS or a Nova function node)
106
+ // delay: The time in ms
107
+
108
+ if (typeof fn === "function") {
109
+ // Case 1: Native JS function (like an arrowfunc or standard JS built-in)
110
+ setTimeout(fn, delay);
111
+ } else if (fn && fn.kind === "function" && fn.body) {
112
+ // Case 2: Nova AST function node
113
+ // The Nova function node carries its definition scope implicitly, but
114
+ // here we just use the global scope for simplicity of a scheduled task.
115
+ // A more robust solution would require passing the function's closure/scope.
116
+ setTimeout(() => {
117
+ // Execute the function body using the helper method.
118
+ // We use the globalScope as the execution environment for the task.
119
+ self.runFunctionNode(fn, self.globalScope);
120
+ }, delay);
121
+ } else {
122
+ // You may want to throw an error here: 'setTimeout requires a function'
123
+ }
124
+ });
125
+
126
+ // existing core library addition
127
+ this.globalScope.set("core", {
128
+ test: {
129
+ object: { foo: 42 },
130
+ },
131
+ getAst: () => this.ast,
132
+ json: JSON,
133
+ print: (value) => {
134
+ let result = this.stringify(value);
135
+ process.stdout.write(result + "\n");
136
+ return "";
137
+ },
138
+ register: (fn) => {
139
+ let fnScope = new Scope("function");
140
+ let func = function(...args) { self.runFunctionNode(fn, self.globalScope, ...args); };
141
+ func.registered = true;
142
+ return func;
143
+ },
144
+ vars: self.globalScope.variables,
145
+ });
146
+ this.globalScope.set("k", (a) => a * 1000);
147
+ this.globalScope.set("m", (a) => a * 1000000);
148
+ this.globalScope.set("h", (a) => a * 100);
149
+ this.globalScope.set("b", (a) => a * 1000000000);
150
+ }
151
+
152
+ // ===== executor.js (Updated stringify method) =====
153
+ stringify(value) {
154
+ if (value instanceof Scope) return value.toString();
155
+ if (value === null) return '[-Null{null}-]'
156
+ // Handle all NovaValue wrappers
157
+ if (value instanceof NovaValue) {
158
+ if (value instanceof NovaString) return value.valueOf();
159
+ if (value instanceof NovaNumber) return value.valueOf();
160
+ if (value instanceof NovaArray) return `Array [${value.length}]`;
161
+ if (value instanceof NovaObject)
162
+ return `Object [${Object.keys(value.inner).length}]`;
163
+ if (value instanceof NovaNull) return "null"; // 🔥 NEW: NovaNull handling
164
+ if (value instanceof NovaBool) return value.valueOf() ? "true" : "false"; // 🔥 NEW: NovaBool handling
165
+ return value.toString();
166
+ }
167
+
168
+ // Handle Nova AST/Function Kinds
169
+ switch (value?.kind) {
170
+ case "function":
171
+ return `Function [${value.name}]`;
172
+ case "class":
173
+ return `Class [${value.node.name}]`;
174
+ default:
175
+ // Handle raw JavaScript types (for external/stdlib values)
176
+ switch (typeof value) {
177
+ case "number":
178
+ return isNaN(value) ? "NaN" : value;
179
+ case "string":
180
+ return value;
181
+ case "object":
182
+ return `Object [${value}]`;
183
+ case "function":
184
+ return `Function [${value.registered ? "Registered" : "NativeJs"}]`;
185
+ }
186
+ }
187
+ }
188
+
189
+ // Add this method to the Executor class
190
+ runFunctionNode(fn, scope, args = []) {
191
+ const localScope = new Scope("function", scope, this.globalScope); // Parent scope is the definition scope
192
+ localScope.set("this", scope);
193
+
194
+ fn.args.forEach((argName, i) => localScope.set(argName, args[i]));
195
+
196
+ let result;
197
+ try {
198
+ for (const stmt of fn.body) {
199
+ result = this.execute(stmt, localScope);
200
+ }
201
+ } catch (e) {
202
+ if (e && "__return" in e) {
203
+ return e.__return;
204
+ } else {
205
+ throw e;
206
+ }
207
+ }
208
+
209
+ return localScope.get("__return") ?? result;
210
+ }
211
+
212
+ error(msg, node) {
213
+ throw new new CustomError("RuntimeError")(
214
+ formatError("ExecError", msg, node.line, node.column, this.rawsrc),
215
+ );
216
+ }
217
+
218
+ run(ast) {
219
+ this.ast = ast;
220
+ if (!ast) return;
221
+ const statements = Array.isArray(ast) ? ast : ast.nodes;
222
+ let result;
223
+ for (const node of statements) {
224
+ result = this.execute(node, this.globalScope);
225
+ }
226
+ return result; // Return the result (may be a Promise)
227
+ }
228
+ checkAsync(cond) {
229
+ setTimeout(() => {
230
+ if (!cond()) this.checkAsync(cond);
231
+ }, 10);
232
+ }
233
+ execute(node, scope = this.globalScope) {
234
+ if (!node) return undefined;
235
+
236
+ switch (node.kind) {
237
+ case "EOF":
238
+ return undefined;
239
+
240
+ case "declare": {
241
+ if (node.destructure) {
242
+ const val = this.evaluate(node.value, scope);
243
+ if (node.destructure.kind === "objpattern") {
244
+ for (const { key, alias } of node.destructure.props) {
245
+ if (val && Object.prototype.hasOwnProperty.call(val, key)) {
246
+ scope.set(alias, val[key], node.isConst);
247
+ } else {
248
+ this.error(
249
+ `Missing property '${key}' in destructured object`,
250
+ node,
251
+ );
252
+ }
253
+ }
254
+ } else if (node.destructure.kind === "arrpattern") {
255
+ node.destructure.elements.forEach((name, i) => {
256
+ scope.set(name, val?.[i], node.isConst);
257
+ });
258
+ } else {
259
+ this.error("Invalid destructuring pattern", node);
260
+ }
261
+ return val;
262
+ }
263
+
264
+ scope.set(node.name, undefined);
265
+ let val = this.evaluate(node.value, scope);
266
+ // 🔥 NEW: Check for isPointer and wrap value
267
+ if (node.isPointer) {
268
+ val = new NovaPointer(
269
+ val,
270
+ (v) => v, // simple read
271
+ (newVal, address) => {
272
+ // Update the pointer's inner value when written to
273
+ val.inner = newVal;
274
+ },
275
+ );
276
+ }
277
+
278
+ return scope.set(node.name, val, node.isConst);
279
+ }
280
+
281
+ case "branch": {
282
+ let current = node;
283
+ let executed = false;
284
+
285
+ while (current && !executed) {
286
+ switch (current.type) {
287
+ // ───── Conditional ─────
288
+ case "if": {
289
+ const condition = this.evaluate(current.args, scope);
290
+ if (condition) {
291
+ this.run(current.body);
292
+ executed = true; // stop chaining
293
+ } else {
294
+ current = current.next; // try next branch
295
+ }
296
+ break;
297
+ }
298
+
299
+ case "else": {
300
+ this.run(current.body);
301
+ executed = true;
302
+ break;
303
+ }
304
+
305
+ case "unless": {
306
+ // like `if !condition`
307
+ const condition = this.evaluate(current.args, scope);
308
+ if (!condition) {
309
+ this.run(current.body);
310
+ }
311
+ executed = true;
312
+ break;
313
+ }
314
+
315
+ // ───── Loops ─────
316
+ case "while": {
317
+ while (
318
+ this.evaluate(
319
+ Array.isArray(current.args) ? current.args[0] : current.args,
320
+ scope,
321
+ )
322
+ ) {
323
+ this.run(current.body);
324
+ }
325
+ executed = true;
326
+ break;
327
+ }
328
+
329
+ case "until": {
330
+ while (
331
+ !this.evaluate(
332
+ Array.isArray(current.args) ? current.args[0] : current.args,
333
+ scope,
334
+ )
335
+ ) {
336
+ this.run(current.body);
337
+ }
338
+ executed = true;
339
+ break;
340
+ }
341
+
342
+ case "do": {
343
+ do {
344
+ this.run(current.body);
345
+ } while (
346
+ this.evaluate(
347
+ Array.isArray(current.args) ? current.args[0] : current.args,
348
+ scope,
349
+ )
350
+ );
351
+ executed = true;
352
+ break;
353
+ }
354
+
355
+ case "repeat": {
356
+ // args[0] = count
357
+ const times = this.evaluate(
358
+ Array.isArray(current.args) ? current.args[0] : current.args,
359
+ );
360
+ for (let i = 0; i < times; i++) {
361
+ this.run(current.body);
362
+ }
363
+ executed = true;
364
+ break;
365
+ }
366
+
367
+ case "for": {
368
+ // for loops: args = [init, condition, update]
369
+ const [initNode, condNode, updateNode] = current.args;
370
+ if (initNode) this.execute(initNode, scope);
371
+ while (!condNode || this.evaluate(condNode, scope)) {
372
+ this.run(current.body);
373
+ if (updateNode) this.execute(updateNode, scope);
374
+ }
375
+ executed = true;
376
+ break;
377
+ }
378
+
379
+ default:
380
+ this.error(`Unknown branch type '${current.type}'`, current);
381
+ }
382
+ }
383
+ break;
384
+ }
385
+ case "function": {
386
+ scope.set(node.name, undefined); // placeholder first
387
+ const val = node;
388
+ return scope.set(node.name, val);
389
+ }
390
+
391
+ case "class": {
392
+ const cls = {};
393
+ for (const m of node.members) {
394
+ if (m.kind === "declare") cls[m.name] = m.value;
395
+ if (m.kind === "function") cls[m.name] = m;
396
+ }
397
+
398
+ let SuperClassConstructor = null;
399
+ let SuperClassPrototype = null; // Holds the instance Scope of the parent
400
+
401
+ if (node.superClass) {
402
+ const superRef = this.evaluate(node.superClass, scope);
403
+ if (typeof superRef !== "function") {
404
+ this.error(`Superclass must be a callable class/constructor`, node);
405
+ }
406
+ SuperClassConstructor = superRef;
407
+
408
+ SuperClassPrototype = SuperClassConstructor();
409
+ }
410
+
411
+ const constructor = (...args) => {
412
+ const instanceScope = new Scope("instance", scope, this.globalScope);
413
+ instanceScope.set("this", instanceScope);
414
+
415
+ if (SuperClassPrototype) {
416
+ instanceScope.prototypes.push(SuperClassPrototype);
417
+ }
418
+
419
+ // Initialize fields and methods (remains the same)
420
+ for (const key in cls) {
421
+ const val = cls[key];
422
+ if (val && val.args && val.body) {
423
+ instanceScope.set(key, val);
424
+ } else {
425
+ instanceScope.set(key, this.evaluate(val, instanceScope));
426
+ }
427
+ }
428
+
429
+ if (SuperClassConstructor) {
430
+ const superFunc = function (...superArgs) {
431
+ // 1. Check if 'super' has already been called (optional, but good for safety)
432
+ if (instanceScope.get("__super_called__")) {
433
+ self.error("Super constructor already called", node);
434
+ }
435
+ // 2. Call the SuperClassConstructor, binding 'this' to the current instanceScope
436
+ // We use runFunctionNode if it's a Nova function, or apply if it's a native JS function.
437
+ if (
438
+ SuperClassConstructor &&
439
+ SuperClassConstructor.args &&
440
+ SuperClassConstructor.body
441
+ ) {
442
+ // If the super constructor is a Nova AST function node:
443
+ self.runFunctionNode(
444
+ SuperClassConstructor,
445
+ instanceScope,
446
+ superArgs,
447
+ );
448
+ } else if (typeof SuperClassConstructor === "function") {
449
+ // If it's a native JS constructor:
450
+ SuperClassConstructor.apply(instanceScope, superArgs);
451
+ }
452
+ // 3. Mark super as called
453
+ instanceScope.set("__super_called__", true);
454
+ };
455
+ // Set the 'super' function within the instance scope
456
+ instanceScope.set("super", superFunc, true); // Set as const
457
+ }
458
+
459
+ if (SuperClassPrototype) {
460
+ instanceScope.prototypes.push(SuperClassPrototype);
461
+ }
462
+
463
+ return instanceScope;
464
+ };
465
+
466
+ // Define a key on the constructor function to hold the SuperClass reference
467
+ constructor.SuperClassConstructor = SuperClassConstructor;
468
+ constructor.SuperClassPrototype = SuperClassPrototype;
469
+ constructor.definitionScope = scope;
470
+ constructor.node = node;
471
+ constructor.kind = "class";
472
+
473
+ return scope.set(node.name, constructor);
474
+ }
475
+
476
+ case "return": {
477
+ const val = this.evaluate(node.value, scope);
478
+ scope.set("__return", val);
479
+ if (node.terminate) throw { __return: val }; // signal function end
480
+ return val;
481
+ }
482
+
483
+ case "exec":
484
+ return this.evaluate(node.expr, scope);
485
+
486
+ case "throw": {
487
+ const value = this.evaluate(node.value, scope);
488
+ const err = new NovaException(value);
489
+ err.payload = value;
490
+ err.node = node;
491
+ throw err;
492
+ }
493
+
494
+ case "try": {
495
+ let thrownValue = null; // The Nova value to be passed to the 'catch' block
496
+
497
+ try {
498
+ // 1. Run try block
499
+ for (const stmt of node.tryBody) {
500
+ this.execute(stmt, scope);
501
+ }
502
+ } catch (e) {
503
+ // Non-local exit: Propagate return/give immediately
504
+ if (e && "__return" in e) {
505
+ throw e;
506
+ }
507
+
508
+ // Nova-level exception (thrown from 'throw')
509
+ else if (e instanceof NovaException) {
510
+ thrownValue = e.payload; // Retrieve the actual thrown Nova value
511
+ }
512
+
513
+ // Native JS errors (like division by zero). We re-throw for external handling.
514
+ else {
515
+ throw e;
516
+ }
517
+ } finally {
518
+ // 3. Run finally block (always runs, and any exit *must* propagate)
519
+ if (node.finallyBody) {
520
+ try {
521
+ for (const stmt of node.finallyBody) {
522
+ this.execute(stmt, scope);
523
+ }
524
+ } catch (e) {
525
+ // An exit (return, give, or error) in 'finally' takes precedence
526
+ throw e;
527
+ }
528
+ }
529
+ }
530
+
531
+ // 2. Run catch block (only runs if a NovaException was caught)
532
+ if (thrownValue !== null && node.catchBody) {
533
+ // The catch block itself can throw or return, so wrap it
534
+ try {
535
+ const catchScope = new Scope("catch", scope, this.globalScope);
536
+ // Bind the thrown value to the catch variable (e.g., 'err' in catch(err))
537
+ catchScope.set(node.catchName, thrownValue);
538
+
539
+ // Execute statements inside the catch block
540
+ for (const stmt of node.catchBody) {
541
+ this.execute(stmt, catchScope);
542
+ }
543
+ } catch (e) {
544
+ // Propagate any exit (return, give, or error) from catch
545
+ throw e;
546
+ }
547
+ }
548
+
549
+ return undefined;
550
+ }
551
+
552
+ default:
553
+ this.error(`Unknown node kind: ${node.kind}`, node);
554
+ }
555
+ }
556
+
557
+ evaluate(expr, scope) {
558
+ if (!expr || !expr.kind) return undefined;
559
+
560
+ switch (expr.kind) {
561
+ case "EOF":
562
+ return undefined;
563
+
564
+ case "deref": {
565
+ const ptr = this.evaluate(expr.operand, scope, false);
566
+ if (ptr instanceof NovaPointer) {
567
+ // Returning the pointer itself allows it to be the LHS of an assignment
568
+ return ptr;
569
+ }
570
+ this.error("Attempt to dereference a non-pointer value", expr);
571
+ }
572
+
573
+ case "value":
574
+ const rawValue = expr.value;
575
+ switch (typeof rawValue) {
576
+ case "string":
577
+ case "number":
578
+ return rawValue;
579
+ default:
580
+ // 🔥 NEW: Check for Lexer symbols and wrap them
581
+ if (rawValue === Symbol("NOVA_TRUE")) return new NovaBool(true);
582
+ if (rawValue === Symbol("NOVA_FALSE")) return new NovaBool(false);
583
+ if (rawValue === Symbol("NOVA_NULL")) return new NovaNull();
584
+
585
+ return rawValue; // Pass through any other raw JS values
586
+ }
587
+
588
+ case "template": {
589
+ const parts = [];
590
+ for (const part of expr.parts) {
591
+ // 1. If the part is a simple string, use its raw value
592
+ if (part.kind === "value" && typeof part.value === "string") {
593
+ parts.push(part.value);
594
+ } else {
595
+ // 2. If the part is an expression, evaluate it and stringify
596
+ const evaluated = this.evaluate(part, scope);
597
+ parts.push(this.stringify(evaluated));
598
+ }
599
+ }
600
+ // Return the wrapper
601
+ return new NovaTemplateString(parts);
602
+ }
603
+
604
+ case "ref":
605
+ return scope.get(expr.name);
606
+
607
+ case "array": {
608
+ const elements = expr.elements.map((el) => this.evaluate(el, scope));
609
+ // 🔥 NEW: Use NovaArray wrapper
610
+ return new NovaArray(elements);
611
+ }
612
+
613
+ case "assign": {
614
+ const val = this.evaluate(expr.value, scope);
615
+
616
+ // 🔥 NEW: Handle dereference assignment: *ptr = val
617
+ if (expr.name.kind === "deref") {
618
+ const ptr = this.evaluate(expr.name.operand, scope);
619
+ if (ptr instanceof NovaPointer) {
620
+ ptr.write(val);
621
+ return val;
622
+ }
623
+ this.error(
624
+ "Attempt to assign to a dereferenced non-pointer value",
625
+ expr,
626
+ );
627
+ }
628
+
629
+ if (expr.name.kind === "prop") {
630
+ const obj = this.evaluate(expr.name.object, scope);
631
+ const propName = expr.name.name;
632
+
633
+ // 🔥 NEW: Check for NovaObject/NovaArray and use their set() method
634
+ if (obj instanceof NovaObject || obj instanceof NovaArray) {
635
+ obj.set(propName, val);
636
+ } else {
637
+ // Existing logic for Scope or raw JS object
638
+ obj instanceof Scope
639
+ ? obj.set(propName, val)
640
+ : (obj[propName] = val);
641
+ }
642
+ return val;
643
+ }
644
+
645
+ if (expr.name.kind === "subscript") {
646
+ const obj = this.evaluate(expr.name.object, scope);
647
+ const index = this.evaluate(expr.name.index, scope);
648
+
649
+ // 🔥 NEW: Check for NovaObject/NovaArray and use their set() method
650
+ if (obj instanceof NovaObject || obj instanceof NovaArray) {
651
+ obj.set(index, val);
652
+ } else {
653
+ // Existing logic for Scope or raw JS object
654
+ obj instanceof Scope ? obj.set(index, val) : (obj[index] = val);
655
+ }
656
+ return val;
657
+ }
658
+ // normal variable assignment
659
+ return scope.set(expr.name.name, val);
660
+ }
661
+
662
+ case "subscript": {
663
+ const obj = this.evaluate(expr.object, scope);
664
+ const index = this.evaluate(expr.index, scope);
665
+ if (obj == null) this.error("Cannot subscript null or undefined", expr);
666
+
667
+ // 🔥 NEW: Check for NovaObject/NovaArray and use their get() method
668
+ if (obj instanceof NovaObject || obj instanceof NovaArray) {
669
+ return obj.get(index);
670
+ }
671
+
672
+ // Existing logic for Scope or raw JS object
673
+ return obj instanceof Scope ? obj.get(index) : obj[index];
674
+ }
675
+
676
+ case "arrowfunc": {
677
+ // Create a callable JS function bound to lexical scope
678
+ const fn = (...args) => {
679
+ const localScope = new Scope("function", scope, this.globalScope);
680
+ fn.args.forEach((argName, i) => localScope.set(argName, args[i]));
681
+ let result;
682
+ try {
683
+ for (const stmt of expr.body) {
684
+ result = this.execute(stmt, localScope);
685
+ }
686
+ } catch (e) {
687
+ if (e && "__return" in e) return e.__return;
688
+ else throw e;
689
+ }
690
+ return localScope.get("__return") ?? result;
691
+ };
692
+ fn.args = expr.args;
693
+ fn.body = expr.body;
694
+ return fn;
695
+ }
696
+
697
+ case "object": {
698
+ const innerObj = {};
699
+ for (const [k, v] of Object.entries(expr.props)) {
700
+ innerObj[k] = this.evaluate(v, scope);
701
+ }
702
+ // 🔥 NEW: Use NovaObject wrapper
703
+ return new NovaObject(innerObj);
704
+ }
705
+
706
+ case "currency": {
707
+ let value = this.evaluate(expr.operand, scope);
708
+ let multiplier = scope.get(expr.name) || expr.name;
709
+ return (!isNaN(multiplier)) ? multiplier * value : multiplier?.(value);
710
+ }
711
+
712
+ case "construct": {
713
+ let Construct = this.evaluate(expr.left, scope);
714
+ let Values = [];
715
+ for (let node of expr.args) {
716
+ Values.push(this.evaluate(node, scope));
717
+ }
718
+ return Construct.toCallable()(Values);
719
+ }
720
+
721
+ case "binary": {
722
+ const left = this.evaluate(expr.left, scope);
723
+ const right = this.evaluate(expr.right, scope);
724
+ switch (expr.operator) {
725
+ case "+":
726
+ return left + right;
727
+ case "-":
728
+ return left - right;
729
+ case "*":
730
+ return left * right;
731
+ case "/":
732
+ return left / right;
733
+ case "%":
734
+ return left % right;
735
+ case "==":
736
+ return left == right;
737
+ case "!=":
738
+ return left != right;
739
+ case "<":
740
+ return left < right;
741
+ case "<=":
742
+ return left <= right;
743
+ case ">":
744
+ return left > right;
745
+ case ">=":
746
+ return left >= right;
747
+ case "&&":
748
+ return left && right;
749
+ case "||":
750
+ return left || right;
751
+ default:
752
+ if (expr.operator.custom) return this.evaluate({ type: "call", args: [left, right], name: expr.operator.left });
753
+ this.error(`Unknown binary operator '${expr.operator}'`, expr);
754
+ }
755
+ }
756
+
757
+ case "unary": {
758
+ switch (expr.operator) {
759
+ case "-":
760
+ return -this.evaluate(expr.operand, scope);
761
+ case "!":
762
+ return !this.evaluate(expr.operand, scope);
763
+ case "delete": {
764
+ const operand = expr.operand;
765
+
766
+ // ───── Case 1: Variable reference ─────
767
+ if (operand.kind === "ref") {
768
+ return scope.delete(operand.name);
769
+ }
770
+
771
+ // ───── Case 2: Object property access (obj.prop) ─────
772
+ if (operand.kind === "prop") {
773
+ const obj = this.evaluate(operand.object, scope);
774
+ const propName = operand.name;
775
+
776
+ // 🔥 NEW: Check for NovaObject/NovaArray and use their delete() method
777
+ if (obj instanceof NovaObject || obj instanceof NovaArray) {
778
+ obj.delete(propName);
779
+ } else if (obj instanceof Scope) {
780
+ obj.delete(propName);
781
+ } else if (obj && typeof obj === "object") {
782
+ delete obj[propName];
783
+ } else {
784
+ this.error(
785
+ `Cannot delete property '${propName}' of non-object`,
786
+ expr,
787
+ );
788
+ }
789
+ return true;
790
+ }
791
+
792
+ // ───── Case 3: Subscript (obj[key]) ─────
793
+ if (operand.kind === "subscript") {
794
+ const obj = this.evaluate(operand.object, scope);
795
+ const index = this.evaluate(operand.index, scope);
796
+
797
+ // 🔥 NEW: Check for NovaObject/NovaArray and use their delete() method
798
+ if (obj instanceof NovaObject || obj instanceof NovaArray) {
799
+ obj.delete(index);
800
+ } else if (obj instanceof Scope) {
801
+ obj.delete(index);
802
+ } else if (obj && typeof obj === "object") {
803
+ delete obj[index];
804
+ } else {
805
+ this.error(
806
+ `Cannot delete index '${index}' of non-object`,
807
+ expr,
808
+ );
809
+ }
810
+ return true;
811
+ }
812
+
813
+ this.error(`Invalid operand for delete`, expr);
814
+ }
815
+ default:
816
+ this.error(`Unknown unary operator '${expr.operator}'`, expr);
817
+ }
818
+ }
819
+
820
+ // ────────────── Property access ──────────────
821
+ case "prop": {
822
+ const obj = this.evaluate(expr.object, scope);
823
+ const propName = expr.name;
824
+
825
+ // 🔥 NEW: Check for NovaObject/NovaArray and use their get() method
826
+ if (obj instanceof NovaObject || obj instanceof NovaArray) {
827
+ return obj.get(propName);
828
+ }
829
+
830
+ // Existing logic for Scope or raw JS object
831
+ return obj instanceof Scope ? obj.get(propName) : obj[propName];
832
+ }
833
+
834
+ // ────────────── Function / Method call ──────────────
835
+ case "call": {
836
+ let fn;
837
+ let thisBinding = scope;
838
+
839
+ if (typeof expr.name === "string") {
840
+ // normal function
841
+ fn = scope.get(expr.name);
842
+ } else if (expr.name.kind === "prop") {
843
+ // method call like obj.method()
844
+ const obj = this.evaluate(expr.name.object, scope);
845
+ thisBinding = obj;
846
+
847
+ // if obj is a Scope, use get(); otherwise use JS property
848
+ if (obj instanceof Scope) fn = obj.get(expr.name.name);
849
+ else if (
850
+ obj &&
851
+ (typeof obj === "object" || typeof obj === "function")
852
+ )
853
+ fn = obj[expr.name.name];
854
+ else
855
+ this.error(`Cannot call '${expr.name.name}' of non-object`, expr);
856
+ } else {
857
+ fn = this.evaluate(expr.name, scope);
858
+ }
859
+
860
+ const args = expr.args.map((a) => this.evaluate(a, scope));
861
+
862
+ // Native JS function
863
+ if (typeof fn === "function") {
864
+ const isClass =
865
+ fn.prototype &&
866
+ Object.getOwnPropertyNames(fn.prototype).length > 1;
867
+
868
+ if (isClass) {
869
+ // Class constructor — no binding, just instantiate
870
+ return new fn(...args);
871
+ } else {
872
+ // Normal function — apply with thisBinding
873
+ return fn.apply(thisBinding, args);
874
+ }
875
+ }
876
+
877
+ // AST function
878
+ if (fn && fn.args && fn.body) {
879
+ return this.runFunctionNode(fn, thisBinding, args);
880
+ }
881
+
882
+ if (typeof fn === "number") return fn * args[0];
883
+
884
+ this.error(`${fn} is not callable`, expr);
885
+ }
886
+
887
+ case "postfix": {
888
+ // Evaluate operand reference
889
+ const operand = expr.operand;
890
+
891
+ // Handle variable refs (like i++)
892
+ if (operand.kind === "ref") {
893
+ const name = operand.name;
894
+ const oldVal = scope.get(name);
895
+ let newVal;
896
+
897
+ switch (expr.operator) {
898
+ case "++":
899
+ newVal = oldVal + 1;
900
+ break;
901
+ case "--":
902
+ newVal = oldVal - 1;
903
+ break;
904
+ default:
905
+ this.error(`Unknown postfix operator '${expr.operator}'`, expr);
906
+ }
907
+
908
+ // Update the variable in its actual scope
909
+ // (walk up parent chain until found)
910
+ let targetScope = scope;
911
+ while (targetScope && !targetScope.has(name)) {
912
+ targetScope = targetScope.parent;
913
+ }
914
+ if (targetScope) targetScope.set(name, newVal);
915
+ else scope.set(name, newVal); // define globally if not found
916
+
917
+ // Return *old* value, just like JS
918
+ return oldVal;
919
+ }
920
+
921
+ if (operand.kind === "prop") {
922
+ const obj = this.evaluate(operand.object, scope);
923
+ const propName = operand.name;
924
+
925
+ // 🔥 NEW: Get the old value using NovaObject/NovaArray get()
926
+ const oldVal =
927
+ obj instanceof NovaObject || obj instanceof NovaArray
928
+ ? obj.get(propName)
929
+ : obj instanceof Scope
930
+ ? obj.get(propName)
931
+ : obj[propName];
932
+
933
+ let newVal;
934
+ switch (expr.operator) {
935
+ case "++":
936
+ newVal = oldVal + 1;
937
+ break;
938
+ case "--":
939
+ newVal = oldVal - 1;
940
+ break;
941
+ default:
942
+ this.error(`Unknown postfix operator '${expr.operator}'`, expr);
943
+ }
944
+
945
+ // 🔥 NEW: Set the new value using NovaObject/NovaArray set()
946
+ if (obj instanceof NovaObject || obj instanceof NovaArray) {
947
+ obj.set(propName, newVal);
948
+ } else {
949
+ let val = newVal;
950
+ obj instanceof Scope
951
+ ? obj.set(propName, val)
952
+ : (obj[propName] = val);
953
+ }
954
+ return oldVal;
955
+ }
956
+
957
+ // Handle subscripts like obj[key]++
958
+ if (operand.kind === "subscript") {
959
+ const obj = this.evaluate(operand.object, scope);
960
+ const index = this.evaluate(operand.index, scope);
961
+
962
+ // 🔥 NEW: Get the old value using NovaObject/NovaArray get()
963
+ const oldVal =
964
+ obj instanceof NovaObject || obj instanceof NovaArray
965
+ ? obj.get(index)
966
+ : obj instanceof Scope
967
+ ? obj.get(index)
968
+ : obj[index];
969
+
970
+ let newVal;
971
+
972
+ switch (expr.operator) {
973
+ case "++":
974
+ newVal = oldVal + 1;
975
+ break;
976
+ case "--":
977
+ newVal = oldVal - 1;
978
+ break;
979
+ default:
980
+ this.error(`Unknown postfix operator '${expr.operator}'`, expr);
981
+ }
982
+
983
+ // 🔥 NEW: Set the new value using NovaObject/NovaArray set()
984
+ if (obj instanceof NovaObject || obj instanceof NovaArray) {
985
+ obj.set(index, newVal);
986
+ } else {
987
+ let val = newVal;
988
+ obj instanceof Scope ? obj.set(index, val) : (obj[index] = val);
989
+ }
990
+ return oldVal;
991
+ }
992
+
993
+ this.error("Invalid operand for postfix operator", expr);
994
+ }
995
+
996
+ default:
997
+ this.error("RuntimeError")(
998
+ `Unknown expression kind: ${expr.kind}`,
999
+ expr,
1000
+ );
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ module.exports = { Executor, Scope };