novac 2.0.1 → 2.2.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 (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
@@ -1,9 +1,15 @@
1
1
  const {
2
2
  NovaValue, NovaNumber, NovaString, NovaArray, NovaObject,
3
3
  NovaFunction, NovaTemplateString, NovaPointer, NovaBool, NovaNull, NovaRange,
4
- NovaStruct, NovaEnum,
4
+ NovaStruct, NovaEnum, NOVA_META,
5
5
  } = require('./types.js');
6
6
  const { CustomError, formatError, NovaException } = require('./error');
7
+ const { atob } = require('buffer');
8
+
9
+ // ── Default sentinel ─────────────────────────────────────────────────────────
10
+ // When `default` is passed as a function argument, the parameter's declared
11
+ // default value is used instead (same effect as omitting the argument).
12
+ const DEFAULT_SENTINEL = Symbol('nova:default');
7
13
 
8
14
  // ══════════════ TypeRegistry ══════════════
9
15
  class TypeRegistry {
@@ -44,7 +50,21 @@ class TypeRegistry {
44
50
  if (this.enums.has(n)) return value && value.__enum_type__ === n;
45
51
  return true;
46
52
  }
47
- case 'union_type': return typeExpr.variants.some(v => this.check(value, v));
53
+ case 'value_type': {
54
+ // Literal value type: exact equality
55
+ const lv = typeExpr.value;
56
+ const raw = value instanceof NovaValue ? value.valueOf() : value;
57
+ if (lv === Symbol.for('NOVA_TRUE') || lv?.toString?.() === 'Symbol(NOVA_TRUE)') return raw === true;
58
+ if (lv === Symbol.for('NOVA_FALSE') || lv?.toString?.() === 'Symbol(NOVA_FALSE)') return raw === false;
59
+ if (lv === Symbol.for('NOVA_NULL') || lv?.toString?.() === 'Symbol(NOVA_NULL)') return raw == null;
60
+ return raw === lv;
61
+ }
62
+ case 'union_type': {
63
+ // If all variants are named_type with literal-looking names, treat as type union
64
+ // But if any variant is a value literal (STRING/NUMBER/BOOL from parser value node),
65
+ // treat as value union — checked in executor via modifiers
66
+ return typeExpr.variants.some(v => this.check(value, v));
67
+ }
48
68
  case 'array_type': return value instanceof NovaArray;
49
69
  case 'shape_type': return value instanceof NovaObject;
50
70
  case 'intersect_type': return typeExpr.parts.every(p => this.check(value, p));
@@ -85,8 +105,9 @@ class TypeRegistry {
85
105
  // ══════════════ Scope ══════════════
86
106
  class Scope {
87
107
  constructor(kind = 'block', parent = null, globalScope = null) {
108
+ const self = this;
88
109
  this.kind = kind; this.parent = parent; this.globalScope = globalScope;
89
- this.variables = { scope: this, array: { toCallable: () => (vals) => new NovaArray(vals) } };
110
+ this.variables = { globalScope, parent, scope: this, array: { toCallable: () => (vals) => new NovaArray(vals) } };
90
111
  this.consts = {}; this.prototypes = [];
91
112
  }
92
113
  get(name) {
@@ -105,14 +126,14 @@ class Scope {
105
126
  }
106
127
  }
107
128
  // Scope chain
108
- if (this.globalScope) {
109
- const gv = this.globalScope.get(name);
110
- if (gv !== null && gv !== undefined) return gv;
111
- }
112
129
  if (this.parent) {
113
130
  const pv = this.parent.get(name);
114
131
  if (pv !== null && pv !== undefined) return pv;
115
132
  }
133
+ if (this.globalScope) {
134
+ const gv = this.globalScope.get(name);
135
+ if (gv !== null && gv !== undefined) return gv;
136
+ }
116
137
  return null;
117
138
  }
118
139
  delete(name) {
@@ -144,6 +165,13 @@ class Scope {
144
165
  return value;
145
166
  }
146
167
  has(name) { return name in this.variables; }
168
+ // Always write to this scope's own variables — used by 'let'/'const' declarations
169
+ // so that re-declaring a name in a child scope shadows the parent instead of updating it.
170
+ setOwn(name, value, isConst) {
171
+ if (isConst) { this.consts[name] = value; }
172
+ this.variables[name] = value;
173
+ return value;
174
+ }
147
175
  toString() { return this.kind.toUpperCase(); }
148
176
  }
149
177
 
@@ -164,25 +192,84 @@ class Executor {
164
192
  this.resus = new Map();
165
193
  this.namespaces = new Map();
166
194
  this.novaServers = new Map(); // port -> routes[], for in-process dispatch
195
+ this.dot_commands = {
196
+ print: (...args) => {
197
+ console.log(...args.map(a => _self.evaluate(a)));
198
+ },
199
+ clear: () => process.stdout.write('\x1Bc'),
200
+ code: () => 0, // no-op, used for doc comments that should be ignored by the executor but visible to external tools
201
+ zap: (target) => {
202
+ const t = this.evaluate(target);
203
+ if (t instanceof NovaObject) {
204
+ for (const k of Object.keys(t.inner)) delete t.inner[k];
205
+ } else if (t instanceof NovaStruct) {
206
+ for (const k of Object.keys(t.inner)) delete t.inner[k];
207
+ } else if (t && typeof t === 'object') {
208
+ for (const k of Object.keys(t)) delete t[k];
209
+ } else {
210
+ throw new Error('.zap target must be an object or struct instance');
211
+ }
212
+ },
213
+ cookie() {
214
+ const asciiCookie = `(\\_/)
215
+ ( •_•)
216
+ / >🍪 Here's a cookie for you!`;
217
+ console.log(asciiCookie);
218
+ },
219
+ js: (code) => {
220
+ const codeStr = this.evaluate(code);
221
+ if (typeof codeStr === 'string') eval(codeStr);
222
+ else if (codeStr instanceof NovaString) eval(codeStr.value);
223
+ else throw new Error('.js command requires a string argument');
224
+ },
225
+ }
167
226
  this.moduleExportsStack = [];
168
- this.awaiting = false; // true while an await is resolving; blocks all scheduling
169
227
  this.options = {};
170
228
  this.optionDecls = {};
171
229
  this.descriptions = [];
172
230
  this.globalScope.variables.std = stdlib;
173
231
  const self = this;
174
232
 
175
- this.globalScope.set('setTimeout', (fn, delay) => {
176
- const wrapped = () => {
177
- if (self.awaiting) { setTimeout(wrapped, 0); return; } // re-queue until clear
178
- if (typeof fn === 'function') fn();
179
- else if (fn && fn.body) self.runFunctionNode(fn, self.globalScope, []);
180
- };
181
- setTimeout(wrapped, delay);
182
- });
233
+ this.globalScope.set('avaibleKits', (() => {
234
+ const kits = [];
235
+ const kitsDir = require('path').join(__dirname, '../..', 'kits');
236
+ try {
237
+ const fs = require('fs');
238
+ for (const kitName of fs.readdirSync(kitsDir)) {
239
+ kits.push(kitName)
240
+ }
241
+ } catch (e) {
242
+ console.warn('Could not load kits:', e.message);
243
+ }
244
+ return kits;
245
+ })());
246
+ this.globalScope.set('setTimeout', setTimeout);
247
+ this.globalScope.set('clearTimeout', clearTimeout);
248
+ this.globalScope.set('setInterval', setInterval);
249
+ this.globalScope.set('clearInterval', clearInterval);
250
+ this.globalScope.set('setImmediate', setImmediate);
251
+ this.globalScope.set('clearImmediate', clearImmediate);
252
+ this.globalScope.set('queueMicrotask', queueMicrotask);
253
+ this.globalScope.set('parseInt', parseInt);
254
+ this.globalScope.set('parseFloat', parseFloat);
255
+ this.globalScope.set('isNaN', isNaN);
256
+ this.globalScope.set('isFinite', isFinite);
257
+ this.globalScope.set('sleepAsync', (ms) => new Promise(r => setTimeout(r, ms)));
258
+ this.globalScope.set('Scope', Scope);
259
+ this.globalScope.set('Atomics', Atomics);
260
+ this.globalScope.set('SharedArrayBuffer', SharedArrayBuffer);
261
+ this.globalScope.set('Buffer', Buffer);
262
+ this.globalScope.set('CError', CustomError);
263
+ this.globalScope.set('Error', Error);
264
+ //remaining built in nodejs variables and objects excluding global, process, console (which we want to control access to), and a few others for safety
265
+ this.globalScope.set('textEncoder', TextEncoder);
266
+ this.globalScope.set('textDecoder', TextDecoder);
267
+ this.globalScope.set('Util', require('util'));
268
+ this.globalScope.set('Date', Date);
269
+ // Expose the default sentinel so runtime code can detect it
270
+ this.globalScope.set('__DEFAULT_SENTINEL__', DEFAULT_SENTINEL);
183
271
 
184
272
  this.globalScope.set('core', {
185
- test: { object: { foo: 42 } },
186
273
  getAst: () => this.ast,
187
274
  json: JSON,
188
275
  print: (v) => { process.stdout.write(this.stringify(v) + '\n'); return ''; },
@@ -277,7 +364,7 @@ class Executor {
277
364
  check: (a, pred) => typeof pred === 'function' ? pred(a) : (_qae[pred] ? _qae[pred](a) : false),
278
365
  };
279
366
  this.globalScope.set('qae', _qae);
280
- this.globalScope.set('novaRegex', (pattern, flags) => this._novaRegex(pattern, flags || ''));
367
+ this.globalScope.set('cregex', (pattern, flags) => this._novaRegex(pattern, flags || ''));
281
368
 
282
369
  // ── Sync HTTP fetch ──
283
370
  const _self = this;
@@ -386,6 +473,1594 @@ class Executor {
386
473
  info: (msg) => process.stdout.write('INFO: ' + String(msg) + '\n'),
387
474
  };
388
475
  this.globalScope.set('nvk', nvk);
476
+ this.globalScope.set('charAt', String.fromCharCode);
477
+ // ── waitsync(fn, ...args) ─────────────────────────────────────────────
478
+ // Calls fn(...args) and blocks synchronously until the result settles,
479
+ // whether the function is:
480
+ // • a plain sync nova/JS function → returns immediately
481
+ // • an `async` nova function → returns a Promise from runFunctionNode
482
+ // • any JS function returning a Promise
483
+ //
484
+ // For Promise-returning cases we CANNOT use Atomics.wait on the main thread
485
+ // because that blocks the event loop, starving microtasks and causing a
486
+ // deadlock. Instead we offload the Promise resolution to a disposable
487
+ // worker_threads Worker that has its own event loop, wait on a
488
+ // SharedArrayBuffer signal, and read the result back via a shared buffer.
489
+ this.globalScope.set('waitsync', (fn, ...args) => {
490
+ // Call the function — may return a plain value or a Promise
491
+ let result;
492
+ if (fn && fn.body !== undefined) {
493
+ result = _self.runFunctionNode(fn, _self.globalScope, args);
494
+ } else if (typeof fn === 'function') {
495
+ result = fn(...args);
496
+ } else {
497
+ throw new Error('waitsync: first argument must be a function');
498
+ }
499
+
500
+ // Non-Promise: return immediately
501
+ if (!(result instanceof Promise)) return result;
502
+
503
+ // Promise path: spin up a worker to resolve it without blocking the
504
+ // main thread's event loop.
505
+ const { Worker } = require('worker_threads');
506
+
507
+ // Shared memory layout (Int32Array, 3 slots):
508
+ // [0] flag: 0=pending, 1=resolved, 2=rejected
509
+ // [1-2] unused (value is passed via a separate Buffer)
510
+ const sab = new SharedArrayBuffer(4);
511
+ const flag = new Int32Array(sab);
512
+
513
+ // We pass the resolved/rejected value as a JSON string through a
514
+ // larger shared buffer (up to 4 MB). For values that can't be
515
+ // JSON-serialised we fall back to null.
516
+ const valueBufSize = 4 * 1024 * 1024; // 4 MB
517
+ const valueSab = new SharedArrayBuffer(valueBufSize + 4); // +4 for length prefix
518
+ const valueMeta = new Int32Array(valueSab, 0, 1); // [0] = byte length
519
+ const valueData = new Uint8Array(valueSab, 4); // rest = UTF-8 bytes
520
+
521
+ // Inline worker script — keeps everything self-contained, no extra file.
522
+ const workerSrc = `
523
+ const { workerData, parentPort } = require('worker_threads');
524
+ const { sab, valueSab, promiseSrc } = workerData;
525
+ const flag = new Int32Array(sab);
526
+ const valueMeta = new Int32Array(valueSab, 0, 1);
527
+ const valueData = new Uint8Array(valueSab, 4);
528
+
529
+ // Reconstruct the Promise from its serialised form.
530
+ // The main thread serialises the promise via a tiny wrapper function source.
531
+ let p;
532
+ try { p = eval('(' + promiseSrc + ')')(); } catch(e) { p = Promise.reject(e); }
533
+
534
+ p.then((v) => {
535
+ try {
536
+ const json = JSON.stringify(v === undefined ? null : v);
537
+ const bytes = Buffer.from(json, 'utf8');
538
+ const len = Math.min(bytes.length, valueData.length);
539
+ bytes.copy(Buffer.from(valueData.buffer, valueData.byteOffset), 0, 0, len);
540
+ Atomics.store(valueMeta, 0, len);
541
+ Atomics.store(flag, 0, 1);
542
+ Atomics.notify(flag, 0);
543
+ } catch(e) {
544
+ Atomics.store(valueMeta, 0, 0);
545
+ Atomics.store(flag, 0, 1);
546
+ Atomics.notify(flag, 0);
547
+ }
548
+ }).catch((e) => {
549
+ try {
550
+ const msg = e && e.message ? e.message : String(e);
551
+ const bytes = Buffer.from(JSON.stringify(msg), 'utf8');
552
+ const len = Math.min(bytes.length, valueData.length);
553
+ bytes.copy(Buffer.from(valueData.buffer, valueData.byteOffset), 0, 0, len);
554
+ Atomics.store(valueMeta, 0, len);
555
+ } catch(_) { Atomics.store(valueMeta, 0, 0); }
556
+ Atomics.store(flag, 0, 2);
557
+ Atomics.notify(flag, 0);
558
+ });
559
+ `;
560
+ // We can't pass a live Promise across the worker boundary via structured
561
+ // clone (Promises aren't clonable). Instead we wrap it in an immediately-
562
+ // invoked function string that the worker evals. For our async nova
563
+ // functions the Promise is already resolving in the main thread's
564
+ // microtask queue — the worker just needs to `.then()` it, but that
565
+ // requires a reference. Solution: we resolve the promise in the main
566
+ // thread itself via a bridge, and the worker just waits on the SAB.
567
+ //
568
+ // Bridge: attach .then/.catch in the main thread, write result to SAB.
569
+ // Worker is only used to give us a non-main-thread event loop for the
570
+ // Atomics.wait call. The main thread does the actual resolution.
571
+ result.then((v) => {
572
+ try {
573
+ const json = JSON.stringify(v === undefined ? null : v);
574
+ const bytes = Buffer.from(json, 'utf8');
575
+ const len = Math.min(bytes.length, valueData.length);
576
+ bytes.copy(Buffer.from(valueData.buffer, valueData.byteOffset), 0, 0, len);
577
+ Atomics.store(valueMeta, 0, len);
578
+ Atomics.store(flag, 0, 1);
579
+ Atomics.notify(flag, 0);
580
+ } catch (_) {
581
+ Atomics.store(valueMeta, 0, 0);
582
+ Atomics.store(flag, 0, 1);
583
+ Atomics.notify(flag, 0);
584
+ }
585
+ }).catch((e) => {
586
+ try {
587
+ const msg = e && e.message ? e.message : String(e);
588
+ const bytes = Buffer.from(JSON.stringify(msg), 'utf8');
589
+ const len = Math.min(bytes.length, valueData.length);
590
+ bytes.copy(Buffer.from(valueData.buffer, valueData.byteOffset), 0, 0, len);
591
+ Atomics.store(valueMeta, 0, len);
592
+ } catch (_) { Atomics.store(valueMeta, 0, 0); }
593
+ Atomics.store(flag, 0, 2);
594
+ Atomics.notify(flag, 0);
595
+ });
596
+
597
+ // Spin up a minimal worker whose only job is to Atomics.wait so the
598
+ // main thread's call stack can unwind and drain microtasks.
599
+ // The worker itself does nothing but park; all the real work happens
600
+ // in the .then/.catch we attached above in the main thread.
601
+ const waiterSrc = `
602
+ const { workerData } = require('worker_threads');
603
+ const { sab } = workerData;
604
+ const flag = new Int32Array(sab);
605
+ // Park until the main thread signals resolution
606
+ Atomics.wait(flag, 0, 0);
607
+ // Done — worker exits, main thread was already notified via notify()
608
+ `;
609
+ const waiter = new Worker(waiterSrc, {
610
+ eval: true,
611
+ workerData: { sab },
612
+ });
613
+
614
+ // Block main thread here — the .then() attached above fires in the same
615
+ // microtask queue because Atomics.wait in a Worker does NOT block the
616
+ // main thread event loop; only this synchronous call frame is suspended
617
+ // until Atomics.notify() fires from the .then().
618
+ Atomics.wait(flag, 0, 0);
619
+ waiter.terminate();
620
+
621
+ const status = Atomics.load(flag, 0);
622
+ const byteLen = Atomics.load(valueMeta, 0);
623
+ let parsed = null;
624
+ if (byteLen > 0) {
625
+ try {
626
+ const json = Buffer.from(valueData.buffer, valueData.byteOffset, byteLen).toString('utf8');
627
+ parsed = JSON.parse(json);
628
+ } catch (_) { parsed = null; }
629
+ }
630
+
631
+ if (status === 2) throw new Error(parsed || 'waitsync: async function rejected');
632
+ return parsed;
633
+ });
634
+
635
+ // ── awaitsync(promise) ────────────────────────────────────────────────
636
+ // Takes an already-created Promise (e.g. the return value of an async nova
637
+ // function call) and blocks synchronously until it resolves, then returns
638
+ // the resolved value. This is the "top-level await" escape hatch for
639
+ // synchronous code that needs to wait on a promise it already has.
640
+ //
641
+ // Unlike waitsync() which calls a function, awaitsync() just waits on a
642
+ // value. Non-Promise values pass straight through unchanged.
643
+ //
644
+ // Internally uses the same SAB+Worker bridge as waitsync so the main
645
+ // thread's microtask queue stays live during the wait.
646
+ this.globalScope.set('awaitsync', (promise) => {
647
+ if (!(promise instanceof Promise)) return promise;
648
+
649
+ const { Worker } = require('worker_threads');
650
+
651
+ const sab = new SharedArrayBuffer(4);
652
+ const flag = new Int32Array(sab);
653
+ const valueBufSize = 4 * 1024 * 1024;
654
+ const valueSab = new SharedArrayBuffer(valueBufSize + 4);
655
+ const valueMeta = new Int32Array(valueSab, 0, 1);
656
+ const valueData = new Uint8Array(valueSab, 4);
657
+
658
+ // Attach resolution handlers in the main thread — microtasks fire here
659
+ promise.then((v) => {
660
+ try {
661
+ const json = JSON.stringify(v === undefined ? null : v);
662
+ const bytes = Buffer.from(json, 'utf8');
663
+ const len = Math.min(bytes.length, valueData.length);
664
+ bytes.copy(Buffer.from(valueData.buffer, valueData.byteOffset), 0, 0, len);
665
+ Atomics.store(valueMeta, 0, len);
666
+ Atomics.store(flag, 0, 1);
667
+ Atomics.notify(flag, 0);
668
+ } catch (_) {
669
+ Atomics.store(valueMeta, 0, 0);
670
+ Atomics.store(flag, 0, 1);
671
+ Atomics.notify(flag, 0);
672
+ }
673
+ }).catch((e) => {
674
+ try {
675
+ const msg = e && e.message ? e.message : String(e);
676
+ const bytes = Buffer.from(JSON.stringify(msg), 'utf8');
677
+ const len = Math.min(bytes.length, valueData.length);
678
+ bytes.copy(Buffer.from(valueData.buffer, valueData.byteOffset), 0, 0, len);
679
+ Atomics.store(valueMeta, 0, len);
680
+ } catch (_) { Atomics.store(valueMeta, 0, 0); }
681
+ Atomics.store(flag, 0, 2);
682
+ Atomics.notify(flag, 0);
683
+ });
684
+
685
+ // Worker parks on the SAB; this unblocks the main thread event loop
686
+ // so the .then/.catch above can fire while we wait.
687
+ const waiterSrc = `
688
+ const { workerData } = require('worker_threads');
689
+ const flag = new Int32Array(workerData.sab);
690
+ Atomics.wait(flag, 0, 0);
691
+ `;
692
+ const waiter = new Worker(waiterSrc, { eval: true, workerData: { sab } });
693
+ Atomics.wait(flag, 0, 0);
694
+ waiter.terminate();
695
+
696
+ const status = Atomics.load(flag, 0);
697
+ const byteLen = Atomics.load(valueMeta, 0);
698
+ let parsed = null;
699
+ if (byteLen > 0) {
700
+ try {
701
+ const json = Buffer.from(valueData.buffer, valueData.byteOffset, byteLen).toString('utf8');
702
+ parsed = JSON.parse(json);
703
+ } catch (_) { parsed = null; }
704
+ }
705
+
706
+ if (status === 2) throw new Error(parsed || 'awaitsync: promise rejected');
707
+ return parsed;
708
+ });
709
+
710
+ // ── _processKitMod(mod) ───────────────────────────────────────────────
711
+ // Process a loaded kitdef.js module. Handles all recognised metadata fields:
712
+ // kitdef — namespace of values/functions to inject into scope (required)
713
+ // events — { eventName: handler } — register event listeners
714
+ // dvars — { name: fn() } — register custom dynamic variables
715
+ // hooks — { 'before:exec': fn, 'after:exec': fn, ... }
716
+ // constants — { NAME: value } — inject as frozen scope constants
717
+ // aliases — { alias: existingName } — create name aliases in scope
718
+ // init — fn() — called once when kit is loaded
719
+ // teardown — fn() — registered for process exit
720
+ // namespace — string — wrap kitdef under this namespace name
721
+ // commands — { name: fn } — register as nova dot-commands
722
+ // types — { TypeName: validator } — register custom types
723
+ // config — { key: defaultValue } — merge into nova.config
724
+ // docs — { name: string } — documentation strings (meta only)
725
+ // meta — arbitrary metadata object (stored, not executed)
726
+ // on — alias for events
727
+ // provides — string[] — feature flags this kit provides (informational)
728
+ // setup — alias for init
729
+ // macros — { NAME: value } — register as nova macros
730
+ // filters — { name: fn } — register filter functions in scope
731
+ // transforms — { name: fn } — register transform functions in scope
732
+ // validators — { name: fn } — register validator functions in scope
733
+ // decorators — { name: fn } — register decorator functions in scope
734
+ // schemas — { name: schemaDef } — register schema objects in scope
735
+ // reducers — { name: fn } — register reducer functions in scope
736
+ // effects — { name: fn } — register effect functions in scope
737
+ // plugins — [ fn ] — call each plugin(executor) at load time
738
+ // guards — { name: fn } — register guard functions in scope
739
+ // interceptors — { name: fn } — register interceptor functions in scope
740
+ // tasks — { name: fn } — register named tasks in scope
741
+ // jobs — alias for tasks
742
+ // routes — { pattern: fn } — register router routes
743
+ // handlers — { name: fn } — register named handler functions in scope
744
+ // services — { name: obj } — register service objects in scope
745
+ // resources — { name: value } — register named resources in scope
746
+ // middleware — [ fn ] — register middleware array (stored under kit name)
747
+ // permissions — string[] — declared permissions (informational)
748
+ // dependencies — string[] — kit dependencies (informational)
749
+ // exports — object — additional exports (merged same as kitdef)
750
+ // namespace — when a string, wraps the kitdef namespace under that name
751
+ this._processKitMod = (mod, kitName) => {
752
+ if (!mod || typeof mod !== 'object') return;
753
+
754
+ const ns = mod.kitdef || {};
755
+
756
+ // Determine injection namespace
757
+ const nsName = typeof mod.namespace === 'string' ? mod.namespace : null;
758
+ if (nsName) {
759
+ // Wrap under namespace
760
+ _self.globalScope.set(nsName, new NovaObject({ ...ns }));
761
+ _self.namespaces.set(nsName, ns);
762
+ } else {
763
+ for (const [k, v] of Object.entries(ns)) _self.globalScope.set(k, v);
764
+ }
765
+
766
+ // exports — additional names to inject
767
+ if (mod.exports && typeof mod.exports === 'object') {
768
+ for (const [k, v] of Object.entries(mod.exports)) _self.globalScope.set(k, v);
769
+ }
770
+
771
+ // constants — injected as scope constants
772
+ const consts = mod.constants || {};
773
+ for (const [k, v] of Object.entries(consts)) {
774
+ _self.globalScope.set(k, v, true); // isConst=true
775
+ }
776
+
777
+ // aliases — { alias: existingName }
778
+ const aliases = mod.aliases || {};
779
+ for (const [alias, target] of Object.entries(aliases)) {
780
+ const val = _self.globalScope.get(target);
781
+ if (val !== null && val !== undefined) _self.globalScope.set(alias, val);
782
+ }
783
+
784
+ // events / on — { eventName: handler | handler[] }
785
+ const evMap = mod.events || mod.on || {};
786
+ for (const [evName, handler] of Object.entries(evMap)) {
787
+ const handlers = Array.isArray(handler) ? handler : [handler];
788
+ for (const h of handlers) {
789
+ if (typeof h === 'function') {
790
+ if (!_self.eventBus.has(evName)) _self.eventBus.set(evName, []);
791
+ _self.eventBus.get(evName).push({ body: null, _native: h });
792
+ }
793
+ }
794
+ }
795
+
796
+ // dvars — { varName: fn() } — custom dynamic variables
797
+ // Stored globally; resolved in the 'dvar' evaluate case via _customDvars map.
798
+ const dvars = mod.dvars || {};
799
+ if (!_self._customDvars) _self._customDvars = new Map();
800
+ for (const [dname, dfn] of Object.entries(dvars)) {
801
+ if (typeof dfn === 'function') _self._customDvars.set(dname, dfn);
802
+ }
803
+
804
+ // macros — { NAME: value }
805
+ const macros = mod.macros || {};
806
+ for (const [mname, mval] of Object.entries(macros)) {
807
+ _self.macros.set(mname, mval);
808
+ _self.globalScope.set(mname, mval);
809
+ }
810
+
811
+ // commands — { name: fn } — nova dot-commands
812
+ const cmds = mod.commands || {};
813
+ for (const [cname, cfn] of Object.entries(cmds)) {
814
+ if (typeof cfn === 'function') _self.dot_commands[cname] = cfn;
815
+ }
816
+
817
+ // types — { TypeName: validator } — register custom named types
818
+ const types = mod.types || {};
819
+ for (const [tname, tval] of Object.entries(types)) {
820
+ if (typeof tval === 'function') {
821
+ _self.types.registerType(tname, { name: tname, kind: 'custom', check: tval });
822
+ _self.globalScope.set(tname, tval);
823
+ }
824
+ }
825
+
826
+ // filters, transforms, validators, decorators, reducers, effects,
827
+ // guards, interceptors, tasks/jobs, handlers, services, resources,
828
+ // schemas — all injected as plain named functions/objects in scope
829
+ const injectGroups = [
830
+ 'filters', 'transforms', 'validators', 'decorators', 'reducers', 'effects',
831
+ 'guards', 'interceptors', 'handlers', 'services', 'resources', 'schemas',
832
+ ];
833
+ for (const group of injectGroups) {
834
+ const g = mod[group] || {};
835
+ for (const [k, v] of Object.entries(g)) _self.globalScope.set(k, v);
836
+ }
837
+
838
+ // tasks and jobs (aliases)
839
+ const tasks = { ...(mod.tasks || {}), ...(mod.jobs || {}) };
840
+ for (const [k, v] of Object.entries(tasks)) _self.globalScope.set(k, v);
841
+
842
+ // routes — register via nova's Router if one is active; otherwise just inject
843
+ const routes = mod.routes || {};
844
+ for (const [pattern, handler] of Object.entries(routes)) {
845
+ _self.globalScope.set('__route_' + pattern.replace(/[^a-zA-Z0-9]/g, '_'), handler);
846
+ }
847
+
848
+ // middleware — stored as array under kit name
849
+ if (Array.isArray(mod.middleware)) {
850
+ const mwKey = (kitName || 'kit') + ':middleware';
851
+ _self.globalScope.set(mwKey, new NovaArray(mod.middleware));
852
+ }
853
+
854
+ // plugins — called with executor at load time
855
+ if (Array.isArray(mod.plugins)) {
856
+ for (const plugin of mod.plugins) {
857
+ if (typeof plugin === 'function') {
858
+ try { plugin(_self); } catch (e) { /* plugin errors are non-fatal */ }
859
+ }
860
+ }
861
+ }
862
+
863
+ // init / setup — called once at load time
864
+ const initFn = mod.init || mod.setup;
865
+ if (typeof initFn === 'function') {
866
+ try { initFn(_self); } catch (_) { }
867
+ }
868
+
869
+ // teardown — called at process exit
870
+ const teardownFn = mod.teardown;
871
+ if (typeof teardownFn === 'function') {
872
+ process.on('exit', () => { try { teardownFn(); } catch (_) { } });
873
+ }
874
+
875
+ // meta / docs / provides / permissions / dependencies — informational only
876
+ // Stored on the executor for inspection via nova.kitMeta
877
+ if (!_self._kitMeta) _self._kitMeta = {};
878
+ if (kitName) {
879
+ _self._kitMeta[kitName] = {
880
+ meta: mod.meta || null,
881
+ docs: mod.docs || null,
882
+ provides: mod.provides || [],
883
+ permissions: mod.permissions || [],
884
+ dependencies: mod.dependencies || [],
885
+ config: mod.config || null,
886
+ };
887
+ }
888
+ };
889
+
890
+ // ── include(path) ────────────────────────────────────────────────────
891
+ // Shorthand for `import "path"`. Runs the file in the current scope and
892
+ // merges any exported names directly into it (same as a bare import).
893
+ // Accepts .nova / .nv (parsed + executed) or .js (require()'d as a JS kit).
894
+ this.globalScope.set('include', (filePath) => {
895
+ const _fs = require('fs');
896
+ const _pth = require('path');
897
+ const src = String(filePath);
898
+ const abs = _pth.isAbsolute(src)
899
+ ? src
900
+ : _pth.resolve(process.cwd(), src);
901
+
902
+ if (src.endsWith('.js')) {
903
+ // JS file — require it and merge exported keys into global scope
904
+ const mod = require(abs);
905
+ if (mod && typeof mod === 'object' && (mod.kitdef || mod.events || mod.dvars || mod.init)) {
906
+ // Looks like a kit module — process all metadata fields
907
+ _self._processKitMod(mod, _pth.basename(abs, '.js'));
908
+ } else if (mod && typeof mod === 'object') {
909
+ const ns = mod.kitdef || mod;
910
+ for (const [k, v] of Object.entries(ns)) _self.globalScope.set(k, v);
911
+ }
912
+ return mod;
913
+ }
914
+
915
+ // Nova file — parse and run in a module scope, then merge exports
916
+ const content = _fs.readFileSync(abs, 'utf8');
917
+ const moduleScope = new Scope('module', null, _self.globalScope);
918
+ const exports = {};
919
+ _self.moduleExportsStack.push(exports);
920
+ try {
921
+ const { Parser } = require('./parser');
922
+ _self.run(new Parser(content).parse(), moduleScope);
923
+ } catch (e) {
924
+ if (!(e && '__return' in e)) throw e;
925
+ } finally {
926
+ _self.moduleExportsStack.pop();
927
+ }
928
+ // Merge all module-scope variables + any explicit exports into caller scope
929
+ for (const [k, v] of Object.entries(moduleScope.variables)) {
930
+ _self.globalScope.set(k, v);
931
+ }
932
+ for (const [k, v] of Object.entries(exports)) {
933
+ _self.globalScope.set(k, v);
934
+ }
935
+ return exports;
936
+ });
937
+
938
+ // ── includeKit(kitName) ──────────────────────────────────────────────
939
+ // Loads a kit from ../../kits/<kitName>/kitdef.js (relative to executor.js),
940
+ // falling back to index.nova if no kitdef.js exists.
941
+ // The kit's exported namespace is merged into global scope AND returned.
942
+ this.globalScope.set('includeKit', (kitName) => {
943
+ const _fs = require('fs');
944
+ const _pth = require('path');
945
+ const name = String(kitName);
946
+ const kitsRoot = _pth.join(__dirname, '..', '..', 'kits');
947
+ const kitDir = _pth.join(kitsRoot, name);
948
+
949
+ if (!_fs.existsSync(kitDir)) {
950
+ throw new Error(`includeKit: kit "${name}" not found at ${kitDir}`);
951
+ }
952
+
953
+ const kitdefPath = _pth.join(kitDir, 'kitdef.js');
954
+ const indexNova = _pth.join(kitDir, 'index.nova');
955
+ const indexNv = _pth.join(kitDir, 'index.nv');
956
+
957
+ if (_fs.existsSync(kitdefPath)) {
958
+ // JS kit — load via require and process all metadata fields
959
+ const mod = require(kitdefPath);
960
+ if (!mod || !mod.kitdef) throw new Error(`includeKit: "${name}/kitdef.js" must export { kitdef }`);
961
+ return new NovaObject(mod.kitdef);
962
+ }
963
+
964
+ // Nova kit entry point
965
+ const novaEntry = _fs.existsSync(indexNova) ? indexNova : (_fs.existsSync(indexNv) ? indexNv : null);
966
+ if (!novaEntry) {
967
+ throw new Error(`includeKit: kit "${name}" has no kitdef.js, index.nova, or index.nv`);
968
+ }
969
+ const content = _fs.readFileSync(novaEntry, 'utf8');
970
+ const moduleScope = new Scope('module', null, _self.globalScope);
971
+ const exports = {};
972
+ _self.moduleExportsStack.push(exports);
973
+ try {
974
+ const { Parser } = require('./parser');
975
+ _self.run(new Parser(content).parse(), moduleScope);
976
+ } catch (e) {
977
+ if (!(e && '__return' in e)) throw e;
978
+ } finally {
979
+ _self.moduleExportsStack.pop();
980
+ }
981
+ for (const [k, v] of Object.entries(moduleScope.variables)) _self.globalScope.set(k, v);
982
+ for (const [k, v] of Object.entries(exports)) _self.globalScope.set(k, v);
983
+ return exports;
984
+ });
985
+
986
+ // ── print(...args) ───────────────────────────────────────────────────
987
+ // Top-level shorthand for core.print — prints all args space-separated.
988
+ this.globalScope.set('print', (...args) => {
989
+ process.stdout.write(args.map(a => _self.stringify(a)).join(' ') + '\n');
990
+ return '';
991
+ });
992
+
993
+ this.globalScope.set('printr', (...args) => {
994
+ process.stdout.write(args.join(' ') + '\n');
995
+ return '';
996
+ });
997
+
998
+ this.globalScope.set('eprintr', (...args) => {
999
+ process.stderr.write(args.join(' ') + '\n');
1000
+ return '';
1001
+ });
1002
+
1003
+ // ── println(...args) / eprint(...args) ──────────────────────────────
1004
+ this.globalScope.set('println', (...args) => {
1005
+ process.stdout.write(args.map(a => _self.stringify(a)).join(' ') + '\n');
1006
+ return '';
1007
+ });
1008
+ this.globalScope.set('eprint', (...args) => {
1009
+ process.stderr.write(args.map(a => _self.stringify(a)).join(' ') + '\n');
1010
+ return '';
1011
+ });
1012
+
1013
+ // ── dump(value) ──────────────────────────────────────────────────────
1014
+ // Pretty-prints a value with type info — useful for debugging.
1015
+ this.globalScope.set('dump', (v) => {
1016
+ const type = _self._typeOf(v);
1017
+ const str = _self.stringify(v);
1018
+ process.stdout.write(`[${type}] ${str}\n`);
1019
+ return v;
1020
+ });
1021
+
1022
+ this.globalScope.set('Promise', Promise);
1023
+
1024
+ // ── ensure(cond, msg?) ───────────────────────────────────────────────
1025
+ // Like assert but callable as a function (the `assert` keyword is reserved
1026
+ // for the nova assert statement).
1027
+ this.globalScope.set('ensure', (cond, msg) => {
1028
+ const raw = cond instanceof NovaBool ? cond.valueOf() : cond;
1029
+ if (!raw) throw new Error('Assertion failed' + (msg ? ': ' + String(msg) : ''));
1030
+ return true;
1031
+ });
1032
+
1033
+ // ── range(start, end, step?) ─────────────────────────────────────────
1034
+ // Returns a NovaArray of numbers. Matches Python range() semantics.
1035
+ this.globalScope.set('range', (start, end, step) => {
1036
+ const s = Number(start), e = Number(end), st = step !== undefined ? Number(step) : 1;
1037
+ if (st === 0) throw new Error('range: step cannot be 0');
1038
+ const out = [];
1039
+ if (st > 0) { for (let i = s; i < e; i += st) out.push(i); }
1040
+ else { for (let i = s; i > e; i += st) out.push(i); }
1041
+ return new NovaArray(out);
1042
+ });
1043
+
1044
+ // ── sleep(ms) ────────────────────────────────────────────────────────
1045
+ // Sync sleep using Atomics — same as the wait_stmt implementation.
1046
+ this.globalScope.set('sleep', (ms) => {
1047
+ try {
1048
+ const sab = new SharedArrayBuffer(4);
1049
+ Atomics.wait(new Int32Array(sab), 0, 0, Number(ms));
1050
+ } catch {
1051
+ require('child_process').execSync(`node -e "setTimeout(()=>{},${Number(ms)})"`, { timeout: Number(ms) + 5000 });
1052
+ }
1053
+ });
1054
+
1055
+ // ── env(key?, fallback?) ─────────────────────────────────────────────
1056
+ // Read process environment variables. env() returns full env object,
1057
+ // env("KEY") returns that var (or null), env("KEY", "default") with fallback.
1058
+ this.globalScope.set('env', (key, fallback) => {
1059
+ if (key === undefined) return new NovaObject({ ...process.env });
1060
+ const val = process.env[String(key)];
1061
+ return val !== undefined ? val : (fallback !== undefined ? fallback : null);
1062
+ });
1063
+
1064
+ // ── args() ───────────────────────────────────────────────────────────
1065
+ // Returns the CLI arguments passed after the script name as a NovaArray.
1066
+ this.globalScope.set('args', () => new NovaArray(process.argv.slice(2)));
1067
+
1068
+ // ── exit(code?) ──────────────────────────────────────────────────────
1069
+ this.globalScope.set('exit', (code) => process.exit(Number(code ?? 0)));
1070
+
1071
+ // ── parseInt / parseFloat / isNaN / isFinite ─────────────────────────
1072
+ // Expose JS globals directly so Nova scripts can use them without prefix.
1073
+ this.globalScope.set('parseInt', (v, radix) => parseInt(String(v), radix));
1074
+ this.globalScope.set('parseFloat', (v) => parseFloat(String(v)));
1075
+ this.globalScope.set('isNaN', (v) => isNaN(v));
1076
+ this.globalScope.set('isFinite', (v) => isFinite(v));
1077
+
1078
+ // ── keys(obj) / values(obj) / entries(obj) ──────────────────────────
1079
+ this.globalScope.set('keys', (o) => {
1080
+ const raw = o instanceof NovaObject ? o.inner : o;
1081
+ return new NovaArray(Object.keys(raw ?? {}));
1082
+ });
1083
+ this.globalScope.set('values', (o) => {
1084
+ const raw = o instanceof NovaObject ? o.inner : o;
1085
+ return new NovaArray(Object.values(raw ?? {}));
1086
+ });
1087
+ this.globalScope.set('entries', (o) => {
1088
+ const raw = o instanceof NovaObject ? o.inner : o;
1089
+ return new NovaArray(Object.entries(raw ?? {}).map(([k, v]) => new NovaArray([k, v])));
1090
+ });
1091
+
1092
+ // ── Wasm ──────────────────────────────────────────────────────────────
1093
+ // Wasm object with many methods
1094
+ this.globalScope.set('Wasm', {
1095
+ run: async (bytes, imports) => {
1096
+ const module = await WebAssembly.compile(bytes instanceof NovaArray ? new Uint8Array(bytes.inner) : bytes);
1097
+ const instance = await WebAssembly.instantiate(module, imports instanceof NovaObject ? imports.inner : imports);
1098
+ if (instance.exports && typeof instance.exports._start === 'function') {
1099
+ return instance.exports._start();
1100
+ }
1101
+ return instance;
1102
+ },
1103
+ ...WebAssembly, // re-export all WebAssembly static methods/properties
1104
+ })
1105
+ // ── len(v) ───────────────────────────────────────────────────────────
1106
+ this.globalScope.set('len', (v) => {
1107
+ if (v instanceof NovaArray) return v.length;
1108
+ if (v instanceof NovaObject) return Object.keys(v.inner).length;
1109
+ if (typeof v === 'string') return v.length;
1110
+ if (v && v.length !== undefined) return v.length;
1111
+ return 0;
1112
+ });
1113
+ // ── new WaiterObject() ───────────────────────────────────────────────────
1114
+ // Expose the WaiterObject class so Nova scripts can create waiters directly.
1115
+ class WaiterObject { // a waiter is an object like this: { state: Int, value: any }
1116
+ constructor() {
1117
+ this._state = 0; // 0 = pending, 1 = resolved, 2 = rejected
1118
+ this.value = null;
1119
+ this.onStateChange = (newState) => newState; // override this to react to state changes
1120
+ }
1121
+ get state() { return this._state; }
1122
+ set state(v) { this._state = this.onStateChange(Number(v)); }
1123
+ }
1124
+ this.globalScope.set('WaiterObject', WaiterObject);
1125
+ // ── new Mutex() ───────────────────────────────────────────────────────────
1126
+ // Expose a simple Mutex class for mutual exclusion in async code.
1127
+ class Mutex {
1128
+ constructor(...waiters) {
1129
+ this._locked = false;
1130
+ this._waiters = waiters;
1131
+ }
1132
+ lock() {
1133
+ if (!this._locked) {
1134
+ this._locked = true;
1135
+ return Promise.resolve();
1136
+ }
1137
+ return new Promise(resolve => this._waiters.push(resolve));
1138
+ }
1139
+ unlock() {
1140
+ if (this._waiters.length > 0) {
1141
+ const next = this._waiters.shift();
1142
+ next();
1143
+ } else {
1144
+ this._locked = false;
1145
+ }
1146
+ }
1147
+ }
1148
+ this.globalScope.set('Mutex', Mutex);
1149
+ // ── typeOf_(v) ───────────────────────────────────────────────────────
1150
+ // Returns a string type name for any nova value.
1151
+ // Named typeOf_ to avoid collision with the `type` keyword (type declarations).
1152
+ // Note: `typeOf` (no underscore) is already registered above as a type intrinsic.
1153
+ this.globalScope.set('typeOf_', (v) => _self._typeOf(v));
1154
+
1155
+ // ── str(v) / num(v) / bool(v) ────────────────────────────────────────
1156
+ this.globalScope.set('str', (v) => _self.stringify(v));
1157
+ this.globalScope.set('num', (v) => Number(v instanceof NovaValue ? v.valueOf() : v));
1158
+ this.globalScope.set('bool', (v) => {
1159
+ const raw = v instanceof NovaBool ? v.valueOf() : v;
1160
+ return !!raw;
1161
+ });
1162
+
1163
+ // ── clamp(val, min, max) ─────────────────────────────────────────────
1164
+ this.globalScope.set('clamp', (v, lo, hi) => Math.min(Math.max(Number(v), Number(lo)), Number(hi)));
1165
+
1166
+ // ── zipArrays(...arrays) ─────────────────────────────────────────────
1167
+ // Like Python zip() — pairs elements across arrays into a NovaArray of NovaArrays.
1168
+ // Named zipArrays to avoid collision with nova's infix `zip` operator.
1169
+ this.globalScope.set('zipArrays', (...arrays) => {
1170
+ const arrs = arrays.map(a => a instanceof NovaArray ? a.inner : a);
1171
+ const len = Math.min(...arrs.map(a => a.length));
1172
+ const out = [];
1173
+ for (let i = 0; i < len; i++) out.push(new NovaArray(arrs.map(a => a[i])));
1174
+ return new NovaArray(out);
1175
+ });
1176
+
1177
+ // ── flat(arr, depth?) ────────────────────────────────────────────────
1178
+ this.globalScope.set('flat', (arr, depth) => {
1179
+ const raw = arr instanceof NovaArray ? arr.inner : arr;
1180
+ return new NovaArray(raw.flat(depth !== undefined ? Number(depth) : 1));
1181
+ });
1182
+
1183
+ // ── unique(arr) ──────────────────────────────────────────────────────
1184
+ this.globalScope.set('unique', (arr) => {
1185
+ const raw = arr instanceof NovaArray ? arr.inner : arr;
1186
+ return new NovaArray([...new Set(raw)]);
1187
+ });
1188
+
1189
+ // ── sum(arr) / minimum(arr) / maximum(arr) / average(arr) ──────────────
1190
+ // sum works fine; min/max use variadic form that avoids keyword issues.
1191
+ // avg is a nova infix operator so we expose it as average().
1192
+ this.globalScope.set('sum', (arr) => {
1193
+ const raw = arr instanceof NovaArray ? arr.inner : arr;
1194
+ return raw.reduce((a, b) => a + Number(b), 0);
1195
+ });
1196
+ this.globalScope.set('min', (...args) => {
1197
+ if (args.length === 1 && (args[0] instanceof NovaArray || Array.isArray(args[0]))) {
1198
+ const raw = args[0] instanceof NovaArray ? args[0].inner : args[0];
1199
+ return Math.min(...raw.map(Number));
1200
+ }
1201
+ return Math.min(...args.map(Number));
1202
+ });
1203
+ this.globalScope.set('max', (...args) => {
1204
+ if (args.length === 1 && (args[0] instanceof NovaArray || Array.isArray(args[0]))) {
1205
+ const raw = args[0] instanceof NovaArray ? args[0].inner : args[0];
1206
+ return Math.max(...raw.map(Number));
1207
+ }
1208
+ return Math.max(...args.map(Number));
1209
+ });
1210
+ this.globalScope.set('average', (arr) => {
1211
+ const raw = arr instanceof NovaArray ? arr.inner : arr;
1212
+ if (!raw.length) return 0;
1213
+ return raw.reduce((a, b) => a + Number(b), 0) / raw.length;
1214
+ });
1215
+
1216
+ // ══════════════════════════════════════════════════════════════════════
1217
+ // ── SCOPE UTILITIES ──────────────────────────────────────────────────
1218
+ // ══════════════════════════════════════════════════════════════════════
1219
+
1220
+ // ── deepClone(value) ─────────────────────────────────────────────────
1221
+ // Recursively clones any Nova value into a structurally identical but
1222
+ // fully independent copy. Handles circular refs via a WeakMap.
1223
+ const _deepClone = (v, seen = new WeakMap()) => {
1224
+ if (v === null || v === undefined) return v;
1225
+ const t = typeof v;
1226
+ if (t === 'number' || t === 'string' || t === 'boolean' || t === 'bigint') return v;
1227
+ if (t === 'function') return v; // functions are immutable — share reference
1228
+ if (t === 'symbol') return v;
1229
+ if (t !== 'object') return v;
1230
+ if (seen.has(v)) return seen.get(v);
1231
+ if (v instanceof NovaBool) return new NovaBool(v.valueOf());
1232
+ if (v instanceof NovaNull) return new NovaNull();
1233
+ if (v instanceof NovaNumber) return new NovaNumber(v.valueOf());
1234
+ if (v instanceof NovaString) return new NovaString(v.valueOf());
1235
+ if (v instanceof NovaRange) return new NovaRange(v.start, v.end, v.step);
1236
+ if (v instanceof NovaPointer) return new NovaPointer(_deepClone(v.inner, seen), v.readFn, v.writeFn, v.address);
1237
+ if (v instanceof NovaEnum) { const e = new NovaEnum(v.typeName, v.variant, _deepClone(v.inner, seen)); e.value = _deepClone(v.value, seen); return e; }
1238
+ if (v instanceof NovaStruct) {
1239
+ const inner = {};
1240
+ seen.set(v, inner); // register early to handle circular refs
1241
+ for (const [k, val] of Object.entries(v.inner)) inner[k] = _deepClone(val, seen);
1242
+ const s = new NovaStruct(v.typeName, inner);
1243
+ seen.set(v, s);
1244
+ return s;
1245
+ }
1246
+ if (v instanceof NovaArray) {
1247
+ const placeholder = new NovaArray([]);
1248
+ seen.set(v, placeholder);
1249
+ placeholder.inner = v.inner.map(x => _deepClone(x, seen));
1250
+ return placeholder;
1251
+ }
1252
+ if (v instanceof NovaObject) {
1253
+ const placeholder = new NovaObject({});
1254
+ seen.set(v, placeholder);
1255
+ for (const [k, val] of Object.entries(v.inner)) placeholder.inner[k] = _deepClone(val, seen);
1256
+ return placeholder;
1257
+ }
1258
+ if (v instanceof Scope) {
1259
+ // Clone a Scope — see saveCurrentScope below
1260
+ const cloned = new Scope(v.kind, v.parent, v.globalScope);
1261
+ seen.set(v, cloned);
1262
+ for (const [k, val] of Object.entries(v.variables)) {
1263
+ if (k === 'scope' || k === 'array') continue;
1264
+ cloned.variables[k] = _deepClone(val, seen);
1265
+ }
1266
+ for (const k of Object.keys(v.consts)) cloned.consts[k] = cloned.variables[k];
1267
+ return cloned;
1268
+ }
1269
+ // Plain JS object / AST node
1270
+ if (Array.isArray(v)) {
1271
+ const arr = [];
1272
+ seen.set(v, arr);
1273
+ for (const x of v) arr.push(_deepClone(x, seen));
1274
+ return arr;
1275
+ }
1276
+ const out = {};
1277
+ seen.set(v, out);
1278
+ for (const [k, val] of Object.entries(v)) out[k] = _deepClone(val, seen);
1279
+ return out;
1280
+ };
1281
+ _self._deepClone = _deepClone;
1282
+ this.globalScope.set('deepClone', (v) => _deepClone(v));
1283
+
1284
+ // ── shallowClone(value) ───────────────────────────────────────────────
1285
+ // One-level clone — top container is new but inner values are shared.
1286
+ this.globalScope.set('shallowClone', (v) => {
1287
+ if (v instanceof NovaArray) return new NovaArray([...v.inner]);
1288
+ if (v instanceof NovaObject) return new NovaObject({ ...v.inner });
1289
+ if (v instanceof NovaStruct) return new NovaStruct(v.typeName, { ...v.inner });
1290
+ if (Array.isArray(v)) return [...v];
1291
+ if (v && typeof v === 'object') return { ...v };
1292
+ return v;
1293
+ });
1294
+
1295
+ // ── saveCurrentScope() ───────────────────────────────────────────────
1296
+ // Returns a deep-cloned, fully independent copy of the current global
1297
+ // scope (all user-defined variables). The clone is a live Scope object
1298
+ // you can pass to scopeEnter / scopeExec.
1299
+ // NOTE: called as a zero-arg function; the scope at call-time is the
1300
+ // executor's globalScope since Nova scripts can't pass their own scope ref.
1301
+ this.globalScope.set('saveCurrentScope', () => {
1302
+ const snap = new Scope('saved', null, _self.globalScope);
1303
+ const SKIP = new Set(['scope', 'array', 'std', 'globalScope', 'parent']);
1304
+ for (const [k, v] of Object.entries(_self.globalScope.variables)) {
1305
+ if (SKIP.has(k)) continue;
1306
+ try { snap.variables[k] = _deepClone(v); } catch { snap.variables[k] = v; }
1307
+ }
1308
+ for (const k of Object.keys(_self.globalScope.consts)) {
1309
+ if (!SKIP.has(k) && snap.variables.hasOwnProperty(k)) snap.consts[k] = snap.variables[k];
1310
+ }
1311
+ return snap;
1312
+ });
1313
+
1314
+ // ── saveScope(scope) ─────────────────────────────────────────────────
1315
+ // Like saveCurrentScope() but the caller chooses which Scope to clone.
1316
+ // Accepts any Scope object (e.g. one returned by scopeNew, scopeChain,
1317
+ // scopeParent, or a previously saved scope) and returns a fully
1318
+ // independent deep-cloned copy — changes to either one never affect
1319
+ // the other.
1320
+ this.globalScope.set('saveScope', (scope) => {
1321
+ if (!(scope instanceof Scope))
1322
+ throw new Error('saveScope: argument must be a Scope (got ' + _self._typeOf(scope) + ')');
1323
+ const snap = new Scope('saved', null, _self.globalScope);
1324
+ const SKIP = new Set(['scope', 'array', 'std', 'globalScope', 'parent']);
1325
+ for (const [k, v] of Object.entries(scope.variables)) {
1326
+ if (SKIP.has(k)) continue;
1327
+ try { snap.variables[k] = _deepClone(v); } catch { snap.variables[k] = v; }
1328
+ }
1329
+ for (const k of Object.keys(scope.consts)) {
1330
+ if (!SKIP.has(k) && snap.variables.hasOwnProperty(k)) snap.consts[k] = snap.variables[k];
1331
+ }
1332
+ return snap;
1333
+ });
1334
+
1335
+ // ── scopeSnapshot(scope?) ─────────────────────────────────────────────
1336
+ // Returns a plain NovaObject snapshot (key→value) of the given scope or
1337
+ // the current global scope. Good for inspection / serialization.
1338
+ this.globalScope.set('scopeSnapshot', (scope) => {
1339
+ const src = (scope instanceof Scope) ? scope : _self.globalScope;
1340
+ const SKIP = new Set(['scope', 'array', 'std', 'globalScope', 'parent']);
1341
+ const out = {};
1342
+ for (const [k, v] of Object.entries(src.variables)) {
1343
+ if (SKIP.has(k)) continue;
1344
+ try { out[k] = _deepClone(v); } catch { out[k] = v; }
1345
+ }
1346
+ return new NovaObject(out);
1347
+ });
1348
+
1349
+ // ── scopeEnter(scope) ────────────────────────────────────────────────
1350
+ // Replaces the executor's globalScope with the given scope so that all
1351
+ // subsequent variable lookups / assignments happen in that context.
1352
+ // Returns the previous scope so you can restore it.
1353
+ this.globalScope.set('scopeEnter', (scope) => {
1354
+ if (!(scope instanceof Scope)) throw new Error('scopeEnter: argument must be a Scope');
1355
+ const prev = _self.globalScope;
1356
+ _self.globalScope = scope;
1357
+ return prev;
1358
+ });
1359
+
1360
+ // ── scopeExit(savedScope) ────────────────────────────────────────────
1361
+ // Restore a previously saved scope (the value returned by scopeEnter).
1362
+ this.globalScope.set('scopeExit', (prev) => {
1363
+ if (!(prev instanceof Scope)) throw new Error('scopeExit: argument must be a Scope');
1364
+ _self.globalScope = prev;
1365
+ return prev;
1366
+ });
1367
+
1368
+ // ── scopeExec(scope, fn) ─────────────────────────────────────────────
1369
+ // Run fn() with globalScope temporarily swapped to scope, then restore.
1370
+ // Returns fn's return value. Safe even if fn throws.
1371
+ this.globalScope.set('scopeExec', (scope, fn) => {
1372
+ if (!(scope instanceof Scope)) throw new Error('scopeExec: first arg must be a Scope');
1373
+ const prev = _self.globalScope;
1374
+ _self.globalScope = scope;
1375
+ try {
1376
+ if (typeof fn === 'function') return fn();
1377
+ if (fn && fn.body !== undefined) return _self.runFunctionNode(fn, scope, []);
1378
+ } finally {
1379
+ _self.globalScope = prev;
1380
+ }
1381
+ });
1382
+
1383
+ // ── scopeNew(parent?) ─────────────────────────────────────────────────
1384
+ // Create a fresh empty child scope. parent defaults to globalScope.
1385
+ this.globalScope.set('scopeNew', (parent) => {
1386
+ const p = (parent instanceof Scope) ? parent : _self.globalScope;
1387
+ return new Scope('user', p, _self.globalScope);
1388
+ });
1389
+
1390
+ // ── scopeMerge(target, source) ───────────────────────────────────────
1391
+ // Copy all user variables from source into target (shallow).
1392
+ this.globalScope.set('scopeMerge', (target, source) => {
1393
+ if (!(target instanceof Scope) || !(source instanceof Scope))
1394
+ throw new Error('scopeMerge: both args must be Scopes');
1395
+ const SKIP = new Set(['scope', 'array', 'std', 'globalScope', 'parent']);
1396
+ for (const [k, v] of Object.entries(source.variables)) {
1397
+ if (SKIP.has(k)) continue;
1398
+ target.variables[k] = v;
1399
+ }
1400
+ return target;
1401
+ });
1402
+
1403
+ // ── scopeGet(scope, name) ─────────────────────────────────────────────
1404
+ // Read a variable from any scope by name.
1405
+ this.globalScope.set('scopeGet', (scope, name) => {
1406
+ if (!(scope instanceof Scope)) throw new Error('scopeGet: first arg must be a Scope');
1407
+ return scope.get(String(name));
1408
+ });
1409
+
1410
+ // ── scopeSet(scope, name, value) ─────────────────────────────────────
1411
+ // Write a variable into any scope by name.
1412
+ this.globalScope.set('scopeSet', (scope, name, value) => {
1413
+ if (!(scope instanceof Scope)) throw new Error('scopeSet: first arg must be a Scope');
1414
+ scope.set(String(name), value);
1415
+ return value;
1416
+ });
1417
+
1418
+ // ── scopeHas(scope, name) ─────────────────────────────────────────────
1419
+ // Check whether a variable exists in scope (own variables only).
1420
+ this.globalScope.set('scopeHas', (scope, name) => {
1421
+ if (!(scope instanceof Scope)) throw new Error('scopeHas: first arg must be a Scope');
1422
+ return scope.has(String(name));
1423
+ });
1424
+
1425
+ // ── scopeDelete(scope, name) ──────────────────────────────────────────
1426
+ // Remove a variable from a scope.
1427
+ this.globalScope.set('scopeDelete', (scope, name) => {
1428
+ if (!(scope instanceof Scope)) throw new Error('scopeDelete: first arg must be a Scope');
1429
+ scope.delete(String(name));
1430
+ });
1431
+
1432
+ // ── scopeKeys(scope) ─────────────────────────────────────────────────
1433
+ // List all user-defined variable names in a scope (own only).
1434
+ this.globalScope.set('scopeKeys', (scope) => {
1435
+ const src = (scope instanceof Scope) ? scope : _self.globalScope;
1436
+ const SKIP = new Set(['scope', 'array', 'std', 'globalScope', 'parent']);
1437
+ return new NovaArray(Object.keys(src.variables).filter(k => !SKIP.has(k)));
1438
+ });
1439
+
1440
+ // ── scopeParent(scope) ───────────────────────────────────────────────
1441
+ // Return the parent scope of a given scope.
1442
+ this.globalScope.set('scopeParent', (scope) => {
1443
+ if (!(scope instanceof Scope)) throw new Error('scopeParent: arg must be a Scope');
1444
+ return scope.parent ?? null;
1445
+ });
1446
+
1447
+ // ── scopeDepth(scope) ────────────────────────────────────────────────
1448
+ // How many parents deep is this scope from a root (no parent)?
1449
+ this.globalScope.set('scopeDepth', (scope) => {
1450
+ if (!(scope instanceof Scope)) return 0;
1451
+ let d = 0, cur = scope;
1452
+ while (cur.parent) { d++; cur = cur.parent; }
1453
+ return d;
1454
+ });
1455
+
1456
+ // ── scopeChain(scope) ────────────────────────────────────────────────
1457
+ // Returns a NovaArray of all scopes from this one up to the root.
1458
+ this.globalScope.set('scopeChain', (scope) => {
1459
+ if (!(scope instanceof Scope)) throw new Error('scopeChain: arg must be a Scope');
1460
+ const chain = [];
1461
+ let cur = scope;
1462
+ while (cur) { chain.push(cur); cur = cur.parent; }
1463
+ return new NovaArray(chain);
1464
+ });
1465
+
1466
+ // ── currentScope() ───────────────────────────────────────────────────
1467
+ // Returns the executor's current globalScope reference.
1468
+ this.globalScope.set('currentScope', () => _self.globalScope);
1469
+
1470
+ // ── freezeScope(scope) ───────────────────────────────────────────────
1471
+ // Freeze all current variables in scope as constants (no further writes).
1472
+ this.globalScope.set('freezeScope', (scope) => {
1473
+ const src = (scope instanceof Scope) ? scope : _self.globalScope;
1474
+ const SKIP = new Set(['scope', 'array', 'std', 'globalScope', 'parent']);
1475
+ for (const k of Object.keys(src.variables)) {
1476
+ if (!SKIP.has(k)) src.consts[k] = src.variables[k];
1477
+ }
1478
+ return src;
1479
+ });
1480
+
1481
+ // ── scopeDiff(scopeA, scopeB) ─────────────────────────────────────────
1482
+ // Returns a NovaObject describing keys that differ between two scopes:
1483
+ // { onlyInA: [...], onlyInB: [...], different: [...] }
1484
+ this.globalScope.set('scopeDiff', (a, b) => {
1485
+ if (!(a instanceof Scope) || !(b instanceof Scope))
1486
+ throw new Error('scopeDiff: both args must be Scopes');
1487
+ const SKIP = new Set(['scope', 'array', 'std', 'globalScope', 'parent']);
1488
+ const keysA = new Set(Object.keys(a.variables).filter(k => !SKIP.has(k)));
1489
+ const keysB = new Set(Object.keys(b.variables).filter(k => !SKIP.has(k)));
1490
+ const onlyInA = [...keysA].filter(k => !keysB.has(k));
1491
+ const onlyInB = [...keysB].filter(k => !keysA.has(k));
1492
+ const different = [];
1493
+ for (const k of keysA) {
1494
+ if (keysB.has(k) && a.variables[k] !== b.variables[k]) different.push(k);
1495
+ }
1496
+ return new NovaObject({
1497
+ onlyInA: new NovaArray(onlyInA),
1498
+ onlyInB: new NovaArray(onlyInB),
1499
+ different: new NovaArray(different),
1500
+ });
1501
+ });
1502
+
1503
+ // ── scopeWatch(scope, name, fn) ───────────────────────────────────────
1504
+ // Install a getter/setter descriptor on scope.variables[name] that calls
1505
+ // fn(newValue, oldValue) on every write. Returns an unwatch() function.
1506
+ this.globalScope.set('scopeWatch', (scope, name, fn) => {
1507
+ if (!(scope instanceof Scope)) throw new Error('scopeWatch: first arg must be a Scope');
1508
+ const n = String(name);
1509
+ let _val = scope.variables[n];
1510
+ const descriptor = {
1511
+ read: () => _val,
1512
+ write: (v) => {
1513
+ const old = _val; _val = v;
1514
+ try {
1515
+ if (typeof fn === 'function') fn(v, old);
1516
+ else if (fn && fn.body !== undefined) _self.runFunctionNode(fn, _self.globalScope, [v, old]);
1517
+ } catch (_) { }
1518
+ },
1519
+ };
1520
+ scope.variables[n] = descriptor;
1521
+ // return unwatch function
1522
+ return () => { scope.variables[n] = _val; };
1523
+ });
1524
+
1525
+ // ══════════════════════════════════════════════════════════════════════
1526
+ // ── GENERAL UTILITIES ────────────────────────────────────────────────
1527
+ // ══════════════════════════════════════════════════════════════════════
1528
+
1529
+ // ── deepEqual(a, b) ───────────────────────────────────────────────────
1530
+ // Structural deep equality — returns true if a and b are value-identical
1531
+ // at every level. Handles all Nova types.
1532
+ const _deepEqual = (a, b, seen = new Set()) => {
1533
+ if (a === b) return true;
1534
+ if (a === null || b === null) return a === b;
1535
+ if (typeof a !== typeof b) return false;
1536
+ if (typeof a !== 'object') return a === b;
1537
+ // cycle guard
1538
+ const key = `${Object.prototype.toString.call(a)};${Object.prototype.toString.call(b)}`;
1539
+ if (seen.has(a)) return true;
1540
+ seen.add(a);
1541
+ if (a instanceof NovaArray && b instanceof NovaArray) {
1542
+ if (a.inner.length !== b.inner.length) return false;
1543
+ return a.inner.every((v, i) => _deepEqual(v, b.inner[i], seen));
1544
+ }
1545
+ if (a instanceof NovaObject && b instanceof NovaObject) {
1546
+ const ka = Object.keys(a.inner), kb = Object.keys(b.inner);
1547
+ if (ka.length !== kb.length) return false;
1548
+ return ka.every(k => kb.includes(k) && _deepEqual(a.inner[k], b.inner[k], seen));
1549
+ }
1550
+ if (a instanceof NovaStruct && b instanceof NovaStruct) {
1551
+ if (a.typeName !== b.typeName) return false;
1552
+ return _deepEqual(new NovaObject(a.inner), new NovaObject(b.inner), seen);
1553
+ }
1554
+ if (a instanceof NovaRange && b instanceof NovaRange)
1555
+ return a.start === b.start && a.end === b.end && a.step === b.step;
1556
+ if (a instanceof NovaValue && b instanceof NovaValue)
1557
+ return a.valueOf() === b.valueOf();
1558
+ if (Array.isArray(a) && Array.isArray(b)) {
1559
+ if (a.length !== b.length) return false;
1560
+ return a.every((v, i) => _deepEqual(v, b[i], seen));
1561
+ }
1562
+ const ak = Object.keys(a), bk = Object.keys(b);
1563
+ if (ak.length !== bk.length) return false;
1564
+ return ak.every(k => bk.includes(k) && _deepEqual(a[k], b[k], seen));
1565
+ };
1566
+ this.globalScope.set('deepEqual', (a, b) => _deepEqual(a, b));
1567
+
1568
+ // ── deepFreeze(value) ─────────────────────────────────────────────────
1569
+ // Recursively freeze a Nova object/array so no inner values can be mutated.
1570
+ const _deepFreeze = (v, seen = new WeakSet()) => {
1571
+ if (!v || typeof v !== 'object' || seen.has(v)) return v;
1572
+ seen.add(v);
1573
+ if (v instanceof NovaArray) { Object.freeze(v.inner); v.inner.forEach(x => _deepFreeze(x, seen)); }
1574
+ if (v instanceof NovaObject) { Object.freeze(v.inner); Object.values(v.inner).forEach(x => _deepFreeze(x, seen)); }
1575
+ if (v instanceof NovaStruct) { Object.freeze(v.inner); Object.values(v.inner).forEach(x => _deepFreeze(x, seen)); }
1576
+ return v;
1577
+ };
1578
+ this.globalScope.set('deepFreeze', (v) => _deepFreeze(v));
1579
+
1580
+ // ── deepMerge(target, ...sources) ─────────────────────────────────────
1581
+ // Recursively merge source objects into target. Arrays are concatenated.
1582
+ // Returns the merged object (target is mutated).
1583
+ const _deepMerge = (target, source) => {
1584
+ if (!(source instanceof NovaObject) && typeof source !== 'object') return target;
1585
+ const srcInner = source instanceof NovaObject ? source.inner : source;
1586
+ const tgtInner = target instanceof NovaObject ? target.inner : target;
1587
+ for (const [k, v] of Object.entries(srcInner)) {
1588
+ if (v instanceof NovaObject && tgtInner[k] instanceof NovaObject) {
1589
+ _deepMerge(tgtInner[k], v);
1590
+ } else if (v instanceof NovaArray && tgtInner[k] instanceof NovaArray) {
1591
+ tgtInner[k] = new NovaArray([...tgtInner[k].inner, ...v.inner]);
1592
+ } else {
1593
+ tgtInner[k] = _deepClone(v);
1594
+ }
1595
+ }
1596
+ return target;
1597
+ };
1598
+ this.globalScope.set('deepMerge', (target, ...sources) => {
1599
+ for (const s of sources) _deepMerge(target, s);
1600
+ return target;
1601
+ });
1602
+
1603
+ // ── pick(obj, ...keys) ────────────────────────────────────────────────
1604
+ // Return a new NovaObject with only the specified keys from obj.
1605
+ this.globalScope.set('pick', (obj, ...keys) => {
1606
+ const src = obj instanceof NovaObject ? obj.inner : obj instanceof NovaStruct ? obj.inner : (obj || {});
1607
+ const flatKeys = keys.flatMap(k => k instanceof NovaArray ? k.inner : k);
1608
+ const out = {};
1609
+ for (const k of flatKeys) if (k in src) out[String(k)] = src[String(k)];
1610
+ return new NovaObject(out);
1611
+ });
1612
+
1613
+ // ── omit(obj, ...keys) ────────────────────────────────────────────────
1614
+ // Return a new NovaObject with the specified keys removed.
1615
+ this.globalScope.set('omit', (obj, ...keys) => {
1616
+ const src = obj instanceof NovaObject ? obj.inner : obj instanceof NovaStruct ? obj.inner : (obj || {});
1617
+ const flatKeys = new Set(keys.flatMap(k => k instanceof NovaArray ? k.inner.map(String) : [String(k)]));
1618
+ const out = {};
1619
+ for (const [k, v] of Object.entries(src)) if (!flatKeys.has(k)) out[k] = v;
1620
+ return new NovaObject(out);
1621
+ });
1622
+
1623
+ // ── mapObj(obj, fn) ───────────────────────────────────────────────────
1624
+ // Like Array.map but over object entries: fn(value, key) → new value.
1625
+ this.globalScope.set('mapObj', (obj, fn) => {
1626
+ const src = obj instanceof NovaObject ? obj.inner : obj;
1627
+ const out = {};
1628
+ for (const [k, v] of Object.entries(src))
1629
+ out[k] = (typeof fn === 'function') ? fn(v, k) : _self.runFunctionNode(fn, _self.globalScope, [v, k]);
1630
+ return new NovaObject(out);
1631
+ });
1632
+
1633
+ // ── filterObj(obj, fn) ────────────────────────────────────────────────
1634
+ // Keep only entries where fn(value, key) is truthy.
1635
+ this.globalScope.set('filterObj', (obj, fn) => {
1636
+ const src = obj instanceof NovaObject ? obj.inner : obj;
1637
+ const out = {};
1638
+ for (const [k, v] of Object.entries(src)) {
1639
+ const keep = (typeof fn === 'function') ? fn(v, k) : _self.runFunctionNode(fn, _self.globalScope, [v, k]);
1640
+ if (keep) out[k] = v;
1641
+ }
1642
+ return new NovaObject(out);
1643
+ });
1644
+
1645
+ // ── reduceObj(obj, fn, init) ──────────────────────────────────────────
1646
+ // Reduce over object entries: fn(accumulator, value, key) → accumulator.
1647
+ this.globalScope.set('reduceObj', (obj, fn, init) => {
1648
+ const src = obj instanceof NovaObject ? obj.inner : obj;
1649
+ let acc = init;
1650
+ for (const [k, v] of Object.entries(src))
1651
+ acc = (typeof fn === 'function') ? fn(acc, v, k) : _self.runFunctionNode(fn, _self.globalScope, [acc, v, k]);
1652
+ return acc;
1653
+ });
1654
+
1655
+ // ── groupBy(arr, fn) ─────────────────────────────────────────────────
1656
+ // Group array elements by the key returned by fn(element).
1657
+ this.globalScope.set('groupBy', (arr, fn) => {
1658
+ const inner = arr instanceof NovaArray ? arr.inner : (Array.isArray(arr) ? arr : []);
1659
+ const groups = {};
1660
+ for (const v of inner) {
1661
+ const key = String((typeof fn === 'function') ? fn(v) : _self.runFunctionNode(fn, _self.globalScope, [v]));
1662
+ if (!groups[key]) groups[key] = [];
1663
+ groups[key].push(v);
1664
+ }
1665
+ const out = {};
1666
+ for (const [k, arr2] of Object.entries(groups)) out[k] = new NovaArray(arr2);
1667
+ return new NovaObject(out);
1668
+ });
1669
+
1670
+ // ── chunk(arr, size) ─────────────────────────────────────────────────
1671
+ // Split array into chunks of at most `size` elements.
1672
+ this.globalScope.set('chunk', (arr, size) => {
1673
+ const inner = arr instanceof NovaArray ? arr.inner : (Array.isArray(arr) ? arr : []);
1674
+ const n = Math.max(1, Number(size));
1675
+ const out = [];
1676
+ for (let i = 0; i < inner.length; i += n) out.push(new NovaArray(inner.slice(i, i + n)));
1677
+ return new NovaArray(out);
1678
+ });
1679
+
1680
+ // ── flatten(arr, depth?) ──────────────────────────────────────────────
1681
+ // Flatten nested arrays. depth defaults to Infinity (fully flat).
1682
+ this.globalScope.set('flatten', (arr, depth) => {
1683
+ const inner = arr instanceof NovaArray ? arr.inner : (Array.isArray(arr) ? arr : []);
1684
+ const d = depth !== undefined ? Number(depth) : Infinity;
1685
+ const flatInner = (a, remaining) => {
1686
+ const out = [];
1687
+ for (const v of a) {
1688
+ if (remaining > 0 && (v instanceof NovaArray || Array.isArray(v))) {
1689
+ out.push(...flatInner(v instanceof NovaArray ? v.inner : v, remaining - 1));
1690
+ } else out.push(v);
1691
+ }
1692
+ return out;
1693
+ };
1694
+ return new NovaArray(flatInner(inner, d));
1695
+ });
1696
+
1697
+ // ── zip(...arrays) ────────────────────────────────────────────────────
1698
+ // Alias for zipArrays but accepting a single array-of-arrays or varargs.
1699
+ this.globalScope.set('zip', (...arrays) => {
1700
+ const arrs = arrays.length === 1 && arrays[0] instanceof NovaArray
1701
+ ? arrays[0].inner.map(a => a instanceof NovaArray ? a.inner : a)
1702
+ : arrays.map(a => a instanceof NovaArray ? a.inner : a);
1703
+ const len = Math.min(...arrs.map(a => a.length));
1704
+ return new NovaArray(Array.from({ length: len }, (_, i) => new NovaArray(arrs.map(a => a[i]))));
1705
+ });
1706
+
1707
+ // ── unzip(arr) ────────────────────────────────────────────────────────
1708
+ // Inverse of zip: [[a1,b1],[a2,b2]] → [[a1,a2],[b1,b2]]
1709
+ this.globalScope.set('unzip', (arr) => {
1710
+ const rows = arr instanceof NovaArray ? arr.inner : [];
1711
+ if (!rows.length) return new NovaArray([]);
1712
+ const cols = (rows[0] instanceof NovaArray ? rows[0].inner : rows[0]).length || 0;
1713
+ const out = Array.from({ length: cols }, () => []);
1714
+ for (const row of rows) {
1715
+ const r = row instanceof NovaArray ? row.inner : row;
1716
+ r.forEach((v, i) => { if (out[i]) out[i].push(v); });
1717
+ }
1718
+ return new NovaArray(out.map(c => new NovaArray(c)));
1719
+ });
1720
+
1721
+ // ── partition(arr, fn) ────────────────────────────────────────────────
1722
+ // Split array into [passing, failing] based on predicate fn.
1723
+ this.globalScope.set('partition', (arr, fn) => {
1724
+ const inner = arr instanceof NovaArray ? arr.inner : (Array.isArray(arr) ? arr : []);
1725
+ const pass = [], fail = [];
1726
+ for (const v of inner) {
1727
+ const ok = (typeof fn === 'function') ? fn(v) : _self.runFunctionNode(fn, _self.globalScope, [v]);
1728
+ (ok ? pass : fail).push(v);
1729
+ }
1730
+ return new NovaArray([new NovaArray(pass), new NovaArray(fail)]);
1731
+ });
1732
+
1733
+ // ── sortBy(arr, fn, order?) ───────────────────────────────────────────
1734
+ // Sort by a key function. order: 'asc' (default) | 'desc'
1735
+ this.globalScope.set('sortBy', (arr, fn, order) => {
1736
+ const inner = arr instanceof NovaArray ? [...arr.inner] : (Array.isArray(arr) ? [...arr] : []);
1737
+ const desc = String(order || '').toLowerCase() === 'desc';
1738
+ inner.sort((a, b) => {
1739
+ const ka = (typeof fn === 'function') ? fn(a) : _self.runFunctionNode(fn, _self.globalScope, [a]);
1740
+ const kb = (typeof fn === 'function') ? fn(b) : _self.runFunctionNode(fn, _self.globalScope, [b]);
1741
+ const cmp = ka < kb ? -1 : ka > kb ? 1 : 0;
1742
+ return desc ? -cmp : cmp;
1743
+ });
1744
+ return new NovaArray(inner);
1745
+ });
1746
+
1747
+ // ── countBy(arr, fn) ─────────────────────────────────────────────────
1748
+ // Count how many elements map to each key: { key: count }
1749
+ this.globalScope.set('countBy', (arr, fn) => {
1750
+ const inner = arr instanceof NovaArray ? arr.inner : (Array.isArray(arr) ? arr : []);
1751
+ const out = {};
1752
+ for (const v of inner) {
1753
+ const key = String((typeof fn === 'function') ? fn(v) : _self.runFunctionNode(fn, _self.globalScope, [v]));
1754
+ out[key] = (out[key] || 0) + 1;
1755
+ }
1756
+ return new NovaObject(out);
1757
+ });
1758
+
1759
+ // ── frequencies(arr) ─────────────────────────────────────────────────
1760
+ // Count occurrences of each value. Returns NovaObject {value: count}.
1761
+ this.globalScope.set('frequencies', (arr) => {
1762
+ const inner = arr instanceof NovaArray ? arr.inner : (Array.isArray(arr) ? arr : []);
1763
+ const out = {};
1764
+ for (const v of inner) { const k = _self.stringify(v); out[k] = (out[k] || 0) + 1; }
1765
+ return new NovaObject(out);
1766
+ });
1767
+
1768
+ // ── range2(start, end, step?) ─────────────────────────────────────────
1769
+ // Named range2 to avoid clobbering the existing range() builtin.
1770
+ // Returns a NovaArray (range() returns a NovaRange).
1771
+ this.globalScope.set('range2', (start, end, step) => {
1772
+ const s = Number(start), e = Number(end), st = step !== undefined ? Number(step) : 1;
1773
+ if (st === 0) throw new Error('range2: step cannot be 0');
1774
+ const out = [];
1775
+ if (st > 0) { for (let i = s; i <= e; i += st) out.push(i); }
1776
+ else { for (let i = s; i >= e; i += st) out.push(i); }
1777
+ return new NovaArray(out);
1778
+ });
1779
+
1780
+ // ── linspace(start, end, n) ───────────────────────────────────────────
1781
+ // n evenly spaced values from start to end (inclusive).
1782
+ this.globalScope.set('linspace', (start, end, n) => {
1783
+ const s = Number(start), e = Number(end), count = Math.max(2, Number(n));
1784
+ const out = [];
1785
+ for (let i = 0; i < count; i++) out.push(s + (e - s) * i / (count - 1));
1786
+ return new NovaArray(out);
1787
+ });
1788
+
1789
+ // ── pipe(value, ...fns) ───────────────────────────────────────────────
1790
+ // Thread a value through a sequence of functions left-to-right.
1791
+ this.globalScope.set('pipe', (value, ...fns) => {
1792
+ return fns.reduce((v, fn) => {
1793
+ if (typeof fn === 'function') return fn(v);
1794
+ if (fn && fn.body !== undefined) return _self.runFunctionNode(fn, _self.globalScope, [v]);
1795
+ return v;
1796
+ }, value);
1797
+ });
1798
+
1799
+ // ── compose(...fns) ───────────────────────────────────────────────────
1800
+ // Compose functions right-to-left. Returns a new function.
1801
+ this.globalScope.set('compose', (...fns) => {
1802
+ return (value) => fns.reduceRight((v, fn) => {
1803
+ if (typeof fn === 'function') return fn(v);
1804
+ if (fn && fn.body !== undefined) return _self.runFunctionNode(fn, _self.globalScope, [v]);
1805
+ return v;
1806
+ }, value);
1807
+ });
1808
+
1809
+ // ── memoize(fn) ───────────────────────────────────────────────────────
1810
+ // Wrap a function so repeated calls with the same args return cached results.
1811
+ this.globalScope.set('memoize', (fn) => {
1812
+ const cache = new Map();
1813
+ return (...args) => {
1814
+ const key = JSON.stringify(args.map(a => _self.stringify(a)));
1815
+ if (cache.has(key)) return cache.get(key);
1816
+ const result = (typeof fn === 'function')
1817
+ ? fn(...args)
1818
+ : _self.runFunctionNode(fn, _self.globalScope, args);
1819
+ cache.set(key, result);
1820
+ return result;
1821
+ };
1822
+ });
1823
+
1824
+ // ── once(fn) ──────────────────────────────────────────────────────────
1825
+ // Return a wrapper that calls fn at most once; subsequent calls return the first result.
1826
+ this.globalScope.set('once', (fn) => {
1827
+ let called = false, result;
1828
+ return (...args) => {
1829
+ if (!called) {
1830
+ called = true;
1831
+ result = (typeof fn === 'function') ? fn(...args) : _self.runFunctionNode(fn, _self.globalScope, args);
1832
+ }
1833
+ return result;
1834
+ };
1835
+ });
1836
+
1837
+ // ── debounce(fn, ms) ─────────────────────────────────────────────────
1838
+ // Returns a debounced version of fn (trailing edge, async-safe).
1839
+ this.globalScope.set('debounce', (fn, ms) => {
1840
+ let timer = null;
1841
+ return (...args) => {
1842
+ clearTimeout(timer);
1843
+ timer = setTimeout(() => {
1844
+ timer = null;
1845
+ if (typeof fn === 'function') fn(...args);
1846
+ else _self.runFunctionNode(fn, _self.globalScope, args);
1847
+ }, Number(ms) || 0);
1848
+ };
1849
+ });
1850
+
1851
+ // ── throttle(fn, ms) ─────────────────────────────────────────────────
1852
+ // Returns a throttled version of fn (leading edge).
1853
+ this.globalScope.set('throttle', (fn, ms) => {
1854
+ let last = 0;
1855
+ return (...args) => {
1856
+ const now = Date.now();
1857
+ if (now - last >= Number(ms)) {
1858
+ last = now;
1859
+ if (typeof fn === 'function') return fn(...args);
1860
+ return _self.runFunctionNode(fn, _self.globalScope, args);
1861
+ }
1862
+ };
1863
+ });
1864
+
1865
+ // ── curry(fn, arity?) ─────────────────────────────────────────────────
1866
+ // Auto-curry a function. Calls fn when enough args have been collected.
1867
+ this.globalScope.set('curry', (fn, arity) => {
1868
+ const n = arity !== undefined ? Number(arity) : (fn.length || (fn.args && fn.args.length) || 1);
1869
+ const curried = (collected) => (...args) => {
1870
+ const all = [...collected, ...args];
1871
+ if (all.length >= n) {
1872
+ return (typeof fn === 'function') ? fn(...all) : _self.runFunctionNode(fn, _self.globalScope, all);
1873
+ }
1874
+ return curried(all);
1875
+ };
1876
+ return curried([]);
1877
+ });
1878
+
1879
+ // ── partial(fn, ...boundArgs) ─────────────────────────────────────────
1880
+ // Partially apply fn with the given leading arguments.
1881
+ this.globalScope.set('partial', (fn, ...boundArgs) => {
1882
+ return (...moreArgs) => {
1883
+ const all = [...boundArgs, ...moreArgs];
1884
+ if (typeof fn === 'function') return fn(...all);
1885
+ return _self.runFunctionNode(fn, _self.globalScope, all);
1886
+ };
1887
+ });
1888
+
1889
+ // ── noop() ────────────────────────────────────────────────────────────
1890
+ this.globalScope.set('noop', () => undefined);
1891
+
1892
+ // ── identity(x) ───────────────────────────────────────────────────────
1893
+ this.globalScope.set('identity', (x) => x);
1894
+
1895
+ // ── tap(value, fn) ────────────────────────────────────────────────────
1896
+ // Call fn(value) for side effects and return value unchanged.
1897
+ this.globalScope.set('tap', (value, fn) => {
1898
+ if (typeof fn === 'function') fn(value);
1899
+ else if (fn && fn.body !== undefined) _self.runFunctionNode(fn, _self.globalScope, [value]);
1900
+ return value;
1901
+ });
1902
+
1903
+ // ── tryCatch(fn, handler?) ────────────────────────────────────────────
1904
+ // Call fn(); if it throws, call handler(err) and return its value.
1905
+ // If no handler, returns null on error.
1906
+ this.globalScope.set('tryCatch', (fn, handler) => {
1907
+ try {
1908
+ return (typeof fn === 'function') ? fn() : _self.runFunctionNode(fn, _self.globalScope, []);
1909
+ } catch (e) {
1910
+ const err = e && e.payload !== undefined ? e.payload : (e instanceof Error ? e.message : String(e));
1911
+ if (handler) {
1912
+ return (typeof handler === 'function') ? handler(err) : _self.runFunctionNode(handler, _self.globalScope, [err]);
1913
+ }
1914
+ return null;
1915
+ }
1916
+ });
1917
+
1918
+ // ── repeat(n, fn) ─────────────────────────────────────────────────────
1919
+ // Call fn(i) n times and return array of results.
1920
+ this.globalScope.set('times', (n, fn) => {
1921
+ const out = [];
1922
+ for (let i = 0; i < Number(n); i++)
1923
+ out.push((typeof fn === 'function') ? fn(i) : _self.runFunctionNode(fn, _self.globalScope, [i]));
1924
+ return new NovaArray(out);
1925
+ });
1926
+
1927
+ // ── objectFromEntries(arr) ────────────────────────────────────────────
1928
+ // Build a NovaObject from [[key, value], ...] pairs.
1929
+ this.globalScope.set('objectFromEntries', (arr) => {
1930
+ const inner = arr instanceof NovaArray ? arr.inner : (Array.isArray(arr) ? arr : []);
1931
+ const out = {};
1932
+ for (const pair of inner) {
1933
+ const p = pair instanceof NovaArray ? pair.inner : pair;
1934
+ if (p && p.length >= 2) out[String(p[0])] = p[1];
1935
+ }
1936
+ return new NovaObject(out);
1937
+ });
1938
+
1939
+ // ── invert(obj) ───────────────────────────────────────────────────────
1940
+ // Swap keys and values of an object.
1941
+ this.globalScope.set('invert', (obj) => {
1942
+ const src = obj instanceof NovaObject ? obj.inner : (obj || {});
1943
+ const out = {};
1944
+ for (const [k, v] of Object.entries(src)) out[_self.stringify(v)] = k;
1945
+ return new NovaObject(out);
1946
+ });
1947
+
1948
+ // ── toPairs(obj) / fromPairs(arr) ─────────────────────────────────────
1949
+ this.globalScope.set('toPairs', (obj) => {
1950
+ const src = obj instanceof NovaObject ? obj.inner : (obj || {});
1951
+ return new NovaArray(Object.entries(src).map(([k, v]) => new NovaArray([k, v])));
1952
+ });
1953
+ this.globalScope.set('fromPairs', (arr) => {
1954
+ const inner = arr instanceof NovaArray ? arr.inner : (Array.isArray(arr) ? arr : []);
1955
+ const out = {};
1956
+ for (const pair of inner) {
1957
+ const p = pair instanceof NovaArray ? pair.inner : pair;
1958
+ if (p && p.length >= 2) out[String(p[0])] = p[1];
1959
+ }
1960
+ return new NovaObject(out);
1961
+ });
1962
+
1963
+ // ── defaults(obj, ...sources) ──────────────────────────────────────────
1964
+ // Like Object.assign but only fills in missing (null/undefined) keys.
1965
+ this.globalScope.set('defaults', (obj, ...sources) => {
1966
+ const tgt = obj instanceof NovaObject ? obj : new NovaObject({ ...(obj || {}) });
1967
+ for (const src of sources) {
1968
+ const s = src instanceof NovaObject ? src.inner : (src || {});
1969
+ for (const [k, v] of Object.entries(s))
1970
+ if (tgt.inner[k] === undefined || tgt.inner[k] === null) tgt.inner[k] = v;
1971
+ }
1972
+ return tgt;
1973
+ });
1974
+
1975
+ // ── clamp2(min, max) → fn (or clamp2(val, min, max)) ─────────────────
1976
+ // Already have clamp(v,lo,hi) — this gives a curried version usable as a pipe step.
1977
+ this.globalScope.set('clampFn', (lo, hi) => (v) => Math.min(Math.max(Number(v), Number(lo)), Number(hi)));
1978
+
1979
+ // ── lerp(a, b, t) ─────────────────────────────────────────────────────
1980
+ // Linear interpolation between a and b at fraction t ∈ [0,1].
1981
+ this.globalScope.set('lerp', (a, b, t) => Number(a) + (Number(b) - Number(a)) * Number(t));
1982
+
1983
+ // ── wrap(value, min, max) ─────────────────────────────────────────────
1984
+ // Wrap value into [min, max) range (like modulo but for arbitrary ranges).
1985
+ this.globalScope.set('wrap', (v, lo, hi) => {
1986
+ const range = Number(hi) - Number(lo);
1987
+ if (range === 0) return Number(lo);
1988
+ return ((((Number(v) - Number(lo)) % range) + range) % range) + Number(lo);
1989
+ });
1990
+
1991
+ // ── mapRange(value, inMin, inMax, outMin, outMax) ─────────────────────
1992
+ // Remap a value from one range to another.
1993
+ this.globalScope.set('mapRange', (v, inLo, inHi, outLo, outHi) => {
1994
+ const t = (Number(v) - Number(inLo)) / (Number(inHi) - Number(inLo));
1995
+ return Number(outLo) + t * (Number(outHi) - Number(outLo));
1996
+ });
1997
+
1998
+ // ── roundTo(value, decimals) ──────────────────────────────────────────
1999
+ this.globalScope.set('roundTo', (v, dec) => {
2000
+ const f = 10 ** Number(dec || 0);
2001
+ return Math.round(Number(v) * f) / f;
2002
+ });
2003
+
2004
+ // ── toBase(number, base) ─────────────────────────────────────────────
2005
+ this.globalScope.set('toBase', (n, base) => Number(n).toString(Number(base)));
2006
+
2007
+ // ── fromBase(str, base) ──────────────────────────────────────────────
2008
+ this.globalScope.set('fromBase', (s, base) => parseInt(String(s), Number(base)));
2009
+
2010
+ // ── uuid() ────────────────────────────────────────────────────────────
2011
+ // Already in nvk.uuid but expose at top level too.
2012
+ this.globalScope.set('uuid', () => {
2013
+ const crypto = require('crypto');
2014
+ return crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
2015
+ });
2016
+
2017
+ // ── timestamp() ───────────────────────────────────────────────────────
2018
+ this.globalScope.set('timestamp', () => Date.now());
2019
+
2020
+ // ── isoDate() ─────────────────────────────────────────────────────────
2021
+ this.globalScope.set('isoDate', () => new Date().toISOString());
2022
+
2023
+ // ── formatDate(ms?, format?) ─────────────────────────────────────────
2024
+ // Format a timestamp or now. format tokens: YYYY MM DD HH mm ss
2025
+ this.globalScope.set('formatDate', (ms, fmt) => {
2026
+ const d = new Date(ms !== undefined ? Number(ms) : Date.now());
2027
+ const pad = (n) => String(n).padStart(2, '0');
2028
+ const tokens = {
2029
+ YYYY: d.getFullYear(), MM: pad(d.getMonth() + 1), DD: pad(d.getDate()),
2030
+ HH: pad(d.getHours()), mm: pad(d.getMinutes()), ss: pad(d.getSeconds()),
2031
+ };
2032
+ const f = String(fmt || 'YYYY-MM-DD HH:mm:ss');
2033
+ return f.replace(/YYYY|MM|DD|HH|mm|ss/g, m => tokens[m]);
2034
+ });
2035
+
2036
+ // ── assertEq(a, b, msg?) ─────────────────────────────────────────────
2037
+ // Throw if a !== b (deep structural for objects).
2038
+ this.globalScope.set('assertEq', (a, b, msg) => {
2039
+ if (!_deepEqual(a, b)) {
2040
+ throw new Error((msg ? String(msg) + ': ' : 'assertEq: ') +
2041
+ _self.stringify(a) + ' !== ' + _self.stringify(b));
2042
+ }
2043
+ return true;
2044
+ });
2045
+
2046
+ // ── assertType(value, typeName, msg?) ─────────────────────────────────
2047
+ this.globalScope.set('assertType', (v, typeName, msg) => {
2048
+ const actual = _self._typeOf(v);
2049
+ if (actual !== String(typeName)) {
2050
+ throw new Error((msg ? String(msg) + ': ' : 'assertType: ') +
2051
+ 'expected ' + typeName + ', got ' + actual);
2052
+ }
2053
+ return true;
2054
+ });
2055
+
2056
+ // ── inspect(value) ────────────────────────────────────────────────────
2057
+ // Return a human-readable debug string with type info.
2058
+ this.globalScope.set('inspect', (v) => {
2059
+ const type = _self._typeOf(v);
2060
+ const str = _self.stringify(v);
2061
+ return `[${type}] ${str}`;
2062
+ });
2063
+
389
2064
  this._initThread();
390
2065
  }
391
2066
 
@@ -430,7 +2105,7 @@ class Executor {
430
2105
  }
431
2106
  // NovaBool / NovaNumber / NovaString / NovaNull — all have _v
432
2107
  if (v instanceof NovaBool || v instanceof NovaNumber ||
433
- v instanceof NovaString || v instanceof NovaNull) {
2108
+ v instanceof NovaString || v instanceof NovaNull) {
434
2109
  return { _v: v.valueOf !== undefined ? v.valueOf() : null };
435
2110
  }
436
2111
  if (v instanceof NovaRange) return { start: v.start, end: v.end };
@@ -458,10 +2133,10 @@ class Executor {
458
2133
  if (Array.isArray(v)) return v.map(x => this._rehydrate(x));
459
2134
  if ('__jsfn__' in v) { try { return eval('(' + v.__jsfn__ + ')'); } catch { return null; } }
460
2135
  if ('_v' in v) {
461
- if (v._v === null) return new NovaNull();
2136
+ if (v._v === null) return new NovaNull();
462
2137
  if (typeof v._v === 'boolean') return new NovaBool(v._v);
463
- if (typeof v._v === 'number') return new NovaNumber(v._v);
464
- if (typeof v._v === 'string') return new NovaString(v._v);
2138
+ if (typeof v._v === 'number') return new NovaNumber(v._v);
2139
+ if (typeof v._v === 'string') return new NovaString(v._v);
465
2140
  return v._v;
466
2141
  }
467
2142
  if ('start' in v && 'end' in v && Object.keys(v).length === 2)
@@ -502,12 +2177,12 @@ class Executor {
502
2177
  // Also snapshot the global user-defined variables (functions, declared vars)
503
2178
  // but skip built-in keys that are circular, huge, or executor-internal.
504
2179
  const SKIP = new Set([
505
- 'scope','array','std','core','nova','nvk','qae','novaRegex','fetch',
506
- 'setTimeout','typecheck','satisfies','typeOf','Thread',
507
- 'ForLoop','WhileLoop','IfBlock','TryCatch','Pipeline','FuncDef',
508
- 'MatchBlock','Timer','Counter','Stack','Queue','LinkedList',
509
- 'State','Observable','Validator','DataStream','Transformer',
510
- 'TransformerJSON','TransformerBase64','Router','EventBus','Memo','Lazy','Signal',
2180
+ 'scope', 'array', 'std', 'core', 'nova', 'nvk', 'qae', 'cregex', 'fetch',
2181
+ 'setTimeout', 'typecheck', 'satisfies', 'typeOf', 'Thread',
2182
+ 'ForLoop', 'WhileLoop', 'IfBlock', 'TryCatch', 'Pipeline', 'FuncDef',
2183
+ 'MatchBlock', 'Timer', 'Counter', 'Stack', 'Queue', 'LinkedList',
2184
+ 'State', 'Observable', 'Validator', 'DataStream', 'Transformer',
2185
+ 'TransformerJSON', 'TransformerBase64', 'Router', 'EventBus', 'Memo', 'Lazy', 'Signal',
511
2186
  ]);
512
2187
  for (const [k, v] of Object.entries(this.globalScope.variables || {})) {
513
2188
  if (SKIP.has(k) || snap.hasOwnProperty(k)) continue;
@@ -523,20 +2198,20 @@ class Executor {
523
2198
  _initThread() {
524
2199
  const _path = require('path');
525
2200
  const _workerScriptPath = _path.join(__dirname, 'nova_thread_worker.js');
526
- const _executorPath = _path.join(__dirname, 'executor.js');
2201
+ const _executorPath = _path.join(__dirname, 'executor.js');
527
2202
  const _exe = this;
528
2203
 
529
2204
  class NovaThread {
530
2205
  constructor(fnNode, callerScope) {
531
- this._fnNode = fnNode;
2206
+ this._fnNode = fnNode;
532
2207
  this._callerScope = callerScope;
533
- this._worker = null;
534
- this._mutations = []; // { name, value } buffered until join()
2208
+ this._worker = null;
2209
+ this._mutations = []; // { name, value } buffered until join()
535
2210
  this._msgHandlers = [];
536
- this._done = false;
537
- this._result = undefined;
538
- this._error = null;
539
- this._sab = new SharedArrayBuffer(8);
2211
+ this._done = false;
2212
+ this._result = undefined;
2213
+ this._error = null;
2214
+ this._sab = new SharedArrayBuffer(8);
540
2215
  this._flag = new Int32Array(this._sab);
541
2216
  this._proxy = this._makeProxy();
542
2217
  }
@@ -544,13 +2219,13 @@ class Executor {
544
2219
  _makeProxy() {
545
2220
  const t = this;
546
2221
  return new NovaObject({
547
- start: () => { t.start(); return t._proxy; },
548
- join: () => t.join(),
549
- send: (v) => { t.send(v); return t._proxy; },
2222
+ start: () => { t.start(); return t._proxy; },
2223
+ join: () => t.join(),
2224
+ send: (v) => { t.send(v); return t._proxy; },
550
2225
  on_message: (fn) => { t.on_message(fn); return t._proxy; },
551
- get result() { return t._result; },
552
- get error() { return t._error; },
553
- get done() { return t._done; },
2226
+ get result() { return t._result; },
2227
+ get error() { return t._error; },
2228
+ get done() { return t._done; },
554
2229
  });
555
2230
  }
556
2231
 
@@ -689,24 +2364,103 @@ class Executor {
689
2364
  return 'unknown';
690
2365
  }
691
2366
 
2367
+ // ══════════════════════════════════════════════════════════════════════
2368
+ // _runAsyncBody(stmts, scope, execFn)
2369
+ // ──────────────────────────────────────────────────────────────────────
2370
+ // Shared engine for both top-level `await` and `func async` bodies.
2371
+ // Runs statements one by one; when an `await` suspension is thrown it
2372
+ // chains a real .then() on the Promise, patches the AST node in-place
2373
+ // with the resolved value, re-executes the statement, then continues.
2374
+ // Returns a Promise that resolves with the last statement result
2375
+ // (or whatever was thrown as __return).
2376
+ _runAsyncBody(stmts, scope, execFn) {
2377
+ const self = this;
2378
+ let i = 0;
2379
+ let lastResult;
2380
+
2381
+ // Walk AST tree depth-first; replace first { kind:'await' } node with
2382
+ // { kind:'value', value:val } so the statement can be re-executed cleanly.
2383
+ function patchFirst(node, val) {
2384
+ if (!node || typeof node !== 'object') return false;
2385
+ for (const key of Object.keys(node)) {
2386
+ const child = node[key];
2387
+ if (child && typeof child === 'object') {
2388
+ if (child.kind === 'await') { node[key] = { kind: 'value', value: val }; return true; }
2389
+ if (patchFirst(child, val)) return true;
2390
+ }
2391
+ }
2392
+ return false;
2393
+ }
2394
+
2395
+ // Sentinel to distinguish "no suspension" from a real return value of null/undefined
2396
+ const NO_SUSPENSION = Symbol('no_suspension');
2397
+
2398
+ // Run one statement; if it suspends on await, chain .then() and recurse.
2399
+ function runOne(stmt) {
2400
+ try {
2401
+ lastResult = execFn(stmt, scope);
2402
+ return NO_SUSPENSION; // no suspension — caller proceeds to next stmt
2403
+ } catch (e) {
2404
+ if (e !== null && typeof e === 'object' && '__return' in e)
2405
+ return Promise.resolve({ __earlyReturn__: e.__return });
2406
+ if (e !== null && typeof e === 'object' && e.__awaitSuspension__) {
2407
+ return e.promise.then((val) => {
2408
+ patchFirst(stmt, val);
2409
+ return runOne(stmt); // re-run the same statement with patched value
2410
+ });
2411
+ }
2412
+ return Promise.reject(e);
2413
+ }
2414
+ }
2415
+
2416
+ function step() {
2417
+ while (i < stmts.length) {
2418
+ const stmt = stmts[i++];
2419
+ const suspension = runOne(stmt);
2420
+ if (suspension !== NO_SUSPENSION) {
2421
+ // Suspension or return — chain remaining steps after it resolves
2422
+ return suspension.then((r) => {
2423
+ if (r && typeof r === 'object' && '__earlyReturn__' in r) return r.__earlyReturn__;
2424
+ return step(); // continue from next statement
2425
+ });
2426
+ }
2427
+ }
2428
+ const ret = scope.get ? (scope.get('__return') != null ? scope.get('__return') : lastResult) : lastResult;
2429
+ return Promise.resolve(ret);
2430
+ }
2431
+
2432
+ return step();
2433
+ }
2434
+
692
2435
  run(ast, scope) {
693
- if (this.awaiting) return undefined; // blocked until await resolves
694
2436
  if (ast && !scope) this.ast = ast;
695
2437
  if (!ast) return;
696
2438
  const stmts = Array.isArray(ast) ? ast : ast.nodes;
2439
+ const execScope = scope || this.globalScope;
697
2440
  let result;
698
- for (const node of stmts) { // builtin events
2441
+
2442
+ // Run synchronously; if an await suspension bubbles up, hand off the
2443
+ // current + remaining statements to _runAsyncBody (top-level await).
2444
+ for (let i = 0; i < stmts.length; i++) {
2445
+ const node = stmts[i];
699
2446
  for (const h of (this.eventBus.get('nv:tick') || [])) {
700
- const hs = new Scope('function', scope, this.globalScope);
2447
+ const hs = new Scope('function', execScope, this.globalScope);
701
2448
  this.runLoop(h.body, hs);
702
2449
  }
703
- result = this.execute(node, scope || this.globalScope);
2450
+ try {
2451
+ result = this.execute(node, execScope);
2452
+ } catch (e) {
2453
+ if (e !== null && typeof e === 'object' && e.__awaitSuspension__) {
2454
+ const execFn = this.execute.bind(this);
2455
+ return this._runAsyncBody(stmts.slice(i), execScope, execFn);
2456
+ }
2457
+ throw e;
2458
+ }
704
2459
  }
705
2460
  return result;
706
2461
  }
707
2462
 
708
2463
  runLoop(body, scope) {
709
- if (this.awaiting) return null; // blocked until await resolves
710
2464
  const stmts = Array.isArray(body) ? body : body.nodes;
711
2465
  for (const node of stmts) {
712
2466
  try { this.execute(node, scope || this.globalScope); }
@@ -742,6 +2496,8 @@ class Executor {
742
2496
  if (fs.existsSync(kitdefFile)) {
743
2497
  const kitMod = require(kitdefFile);
744
2498
  if (!kitMod.kitdef) throw new Error(`Kit "${name}": kitdef.js must export { kitdef }`);
2499
+ // Process all metadata fields (events, dvars, init, hooks, etc.)
2500
+ _self._processKitMod(kitMod, name);
745
2501
  return { kind: 'kit', exports: kitMod.kitdef };
746
2502
  }
747
2503
  const indexNova = path.join(novaModDir, 'index.nova');
@@ -854,28 +2610,186 @@ class Executor {
854
2610
  return content;
855
2611
  }
856
2612
 
857
- stringify(v) {
858
- if (v === null || v === undefined) return 'null';
859
- if (v instanceof NovaNull) return 'null';
860
- if (v instanceof NovaBool) return v.valueOf() ? 'true' : 'false';
861
- if (v instanceof NovaString) return v.valueOf();
862
- if (v instanceof NovaNumber) return String(v.valueOf());
863
- if (v instanceof NovaRange) return v.toString();
864
- if (v instanceof NovaStruct) return v.toString();
865
- if (v instanceof NovaArray) return '[' + v.inner.map(x => this.stringify(x)).join(', ') + ']';
866
- if (v instanceof NovaObject) return '{' + Object.entries(v.inner).map(([k, x]) => k + ': ' + this.stringify(x)).join(', ') + '}';
867
- if (v instanceof NovaValue) return v.toString();
868
- if (v instanceof Scope) return v.toString();
869
- if (v && v.kind === 'function') return 'Function [' + (v.name || 'anon') + ']';
870
- if (v && v.kind === 'class') return 'Class [' + (v.node && v.node.name) + ']';
871
- if (v instanceof NovaEnum) return v.toString();
2613
+ stringify(v, indent = 0, seen = new WeakSet()) {
2614
+ const pad = " ".repeat(indent);
2615
+
2616
+ const color = {
2617
+ reset: "\x1b[0m",
2618
+ key: "\x1b[36m",
2619
+ string: "\x1b[32m",
2620
+ number: "\x1b[33m",
2621
+ bool: "\x1b[35m",
2622
+ null: "\x1b[90m",
2623
+ func: "\x1b[31m",
2624
+ circular: "\x1b[91m",
2625
+ };
2626
+
2627
+ const nl = "\n";
2628
+
2629
+ const next = (x, i = indent + 1) =>
2630
+ this.stringify(x, i, seen);
2631
+
2632
+ if (v === null || v === undefined)
2633
+ return color.null + "null" + color.reset;
2634
+
2635
+ if (v instanceof NovaNull)
2636
+ return color.null + "null" + color.reset;
2637
+
2638
+ if (v instanceof NovaBool)
2639
+ return color.bool + String(v.valueOf()) + color.reset;
2640
+
2641
+ if (v instanceof NovaString)
2642
+ return color.string + `"${v.valueOf()}"` + color.reset;
2643
+
2644
+ if (v instanceof NovaNumber)
2645
+ return color.number + String(v.valueOf()) + color.reset;
2646
+
2647
+ if (v instanceof Promise)
2648
+ return color.func + `Promise [${v.name || "anon"}]` + color.reset;
2649
+
2650
+ if (v instanceof NovaRange)
2651
+ return v.toString();
2652
+
2653
+ if (v instanceof NovaStruct)
2654
+ return v.toString();
2655
+
2656
+ // ── Plain JS getter/setter descriptor { read, write } ────────────────────
2657
+ // Scope variables with modifiers (tracked, lazy, clamp, etc.) are stored as
2658
+ // plain objects with read() / write() functions. Show the live value instead.
2659
+ if (
2660
+ v !== null && typeof v === 'object' &&
2661
+ !(v instanceof NovaValue) && !(v instanceof NovaArray) &&
2662
+ !(v instanceof NovaObject) && !(v instanceof NovaStruct) &&
2663
+ !(v instanceof NovaEnum) && !(v instanceof Scope) &&
2664
+ !(v instanceof Promise) && !Array.isArray(v) &&
2665
+ typeof v.read === 'function' && typeof v.write === 'function'
2666
+ ) {
2667
+ return color.key + '[get/set ×]' + color.reset;
2668
+ }
2669
+
2670
+ if (typeof v === "object" && v !== null) {
2671
+ if (seen.has(v))
2672
+ return color.circular + "[Circular]" + color.reset;
2673
+
2674
+ seen.add(v);
2675
+ }
2676
+
2677
+ if (v instanceof NovaArray) {
2678
+ if (!v.inner.length)
2679
+ return "[]";
2680
+
2681
+ return "[\n" +
2682
+ v.inner.map(x =>
2683
+ pad + " " + next(x, indent + 1)
2684
+ ).join(",\n") +
2685
+ nl + pad + "]";
2686
+ }
2687
+
2688
+ if (v instanceof NovaObject) {
2689
+ // ── Build annotation badges ─────────────────────────────────────────────
2690
+ const _annotations = [];
2691
+
2692
+ // Operator overloads stored under __operators
2693
+ const _opsRaw = v.inner.__operators;
2694
+ if (_opsRaw) {
2695
+ const _opsObj = _opsRaw instanceof NovaObject
2696
+ ? _opsRaw.inner
2697
+ : (typeof _opsRaw === 'object' && _opsRaw !== null ? _opsRaw : {});
2698
+ const _binOps = Object.keys(
2699
+ _opsObj.binary instanceof NovaObject ? _opsObj.binary.inner : (_opsObj.binary || {})
2700
+ );
2701
+ const _unOps = Object.keys(
2702
+ _opsObj.unary instanceof NovaObject ? _opsObj.unary.inner : (_opsObj.unary || {})
2703
+ ).map(k => 'u' + k);
2704
+ const _allOps = [..._binOps, ..._unOps];
2705
+ _annotations.push('\x1b[33m[ops:' + (_allOps.length ? ' ' + _allOps.join(' ') : '') + ']\x1b[0m');
2706
+ }
2707
+
2708
+ // Meta traps attached via attachMeta()
2709
+ if (v._meta) {
2710
+ const _traps = [];
2711
+ if (typeof v._meta.get === 'function') _traps.push('get');
2712
+ if (typeof v._meta.set === 'function') _traps.push('set');
2713
+ if (typeof v._meta.missing === 'function') _traps.push('missing');
2714
+ if (_traps.length) _annotations.push('\x1b[36m[meta:' + _traps.join(',') + ']\x1b[0m');
2715
+ }
2716
+
2717
+ const _prefix = _annotations.length ? _annotations.join(' ') + ' ' : '';
2718
+
2719
+ // Hide internal plumbing keys from display
2720
+ const _HIDE = new Set(['__operators']);
2721
+ const _entries = Object.entries(v.inner).filter(([k]) => !_HIDE.has(k));
2722
+
2723
+ if (!_entries.length)
2724
+ return _prefix + "{}";
2725
+
2726
+ return _prefix + "{\n" +
2727
+ _entries.map(([k, x]) =>
2728
+ pad + " " +
2729
+ color.key + k + color.reset +
2730
+ ": " +
2731
+ next(x, indent + 1)
2732
+ ).join(",\n") +
2733
+ nl + pad + "}";
2734
+ }
2735
+
2736
+ if (v instanceof NovaValue)
2737
+ return v.toString();
2738
+
2739
+ if (v instanceof Scope)
2740
+ return v.toString();
2741
+
2742
+ if (v && v.kind === "function")
2743
+ return color.func +
2744
+ `Function [${v.name || "anon"}]` +
2745
+ color.reset;
2746
+
2747
+ if (v && v.kind === "class")
2748
+ return color.func +
2749
+ `Class [${v.node?.name || "Unknown"}]` +
2750
+ color.reset;
2751
+
2752
+ if (v instanceof NovaEnum)
2753
+ return v.toString();
2754
+
872
2755
  switch (typeof v) {
873
- case 'number': return isNaN(v) ? 'NaN' : String(v);
874
- case 'string': return v;
875
- case 'boolean': return String(v);
876
- case 'object': return JSON.stringify(v);
877
- case 'function': return 'Function [' + (v.registered ? 'Registered' : 'NativeJs') + ']';
2756
+ case "number":
2757
+ return color.number +
2758
+ (isNaN(v) ? "NaN" : String(v)) +
2759
+ color.reset;
2760
+
2761
+ case "string":
2762
+ return color.string +
2763
+ `"${v}"` +
2764
+ color.reset;
2765
+
2766
+ case "boolean":
2767
+ return color.bool +
2768
+ String(v) +
2769
+ color.reset;
2770
+
2771
+ case "object": {
2772
+ const entries = Object.entries(v);
2773
+
2774
+ if (!entries.length)
2775
+ return "Native {}";
2776
+
2777
+ return "Native {\n" +
2778
+ entries.map(([k, val]) =>
2779
+ pad + " " +
2780
+ color.key + k + color.reset +
2781
+ ": " +
2782
+ next(val, indent + 1)
2783
+ ).join(",\n") +
2784
+ nl + pad + "}";
2785
+ }
2786
+
2787
+ case "function":
2788
+ return color.func +
2789
+ `Function [${v.registered ? "Registered" : "NativeJs"}]` +
2790
+ color.reset;
878
2791
  }
2792
+
879
2793
  return String(v);
880
2794
  }
881
2795
 
@@ -911,13 +2825,17 @@ class Executor {
911
2825
 
912
2826
  const ls = new Scope('function', scope, this.globalScope);
913
2827
  ls.set('this', scope);
2828
+ ls._funcName = fn.name || null; // used by __func__ dvar
914
2829
 
915
2830
  (fn.args || []).forEach((arg, i) => {
916
2831
  if (typeof arg === 'string') { ls.set(arg, args[i]); return; }
917
2832
  if (arg.rest) { ls.set(arg.name, new NovaArray(args.slice(i))); return; }
918
- const v = args[i] !== undefined
919
- ? args[i]
920
- : (arg.defaultValue ? this.evaluate(arg.defaultValue, ls) : undefined);
2833
+ // `default` sentinel treat as if argument was omitted (use default value)
2834
+ const rawArg = args[i];
2835
+ const useDefault = rawArg === DEFAULT_SENTINEL || rawArg === undefined;
2836
+ const v = useDefault
2837
+ ? (arg.defaultValue ? this.evaluate(arg.defaultValue, ls) : undefined)
2838
+ : rawArg;
921
2839
  if (arg.type && !this.types.check(v, arg.type)) {
922
2840
  const tn = arg.type.name || JSON.stringify(arg.type);
923
2841
  throw new Error("Argument '" + arg.name + "': expected " + tn + ", got " + this._typeOf(v));
@@ -951,11 +2869,30 @@ class Executor {
951
2869
  return iter;
952
2870
  }
953
2871
 
2872
+ // ── Async function: execute body inside a Promise chain ──
2873
+ if (fn.isAsync) {
2874
+ // Delegate to the shared _runAsyncBody engine which handles await suspensions
2875
+ // via .then() chains, patching AST nodes in-place for each resolved await.
2876
+ const execFn = originalExecute;
2877
+ const self = this;
2878
+ return new Promise((resolve, reject) => {
2879
+ self._runAsyncBody(fn.body, ls, execFn)
2880
+ .then(resolve)
2881
+ .catch((e) => {
2882
+ if (e !== null && typeof e === 'object' && '__return' in e) resolve(e.__return);
2883
+ else reject(e);
2884
+ });
2885
+ }).finally(() => {
2886
+ this.execute = originalExecute;
2887
+ if (fn.defer) this.execute(fn.defer);
2888
+ });
2889
+ }
2890
+
954
2891
  try {
955
2892
  let result;
956
2893
  for (const stmt of fn.body) result = this.execute(stmt, ls);
957
2894
  const ret = ls.get('__return') !== undefined ? ls.get('__return') : result;
958
- return fn.isAsync ? Promise.resolve(ret) : ret; // ← add this
2895
+ return ret;
959
2896
  } catch (e) {
960
2897
  if (e !== null && typeof e === 'object' && '__return' in e) {
961
2898
  if (fn.memoize) fn._memoCache.set(JSON.stringify(args), () => e.__return);
@@ -1004,12 +2941,20 @@ class Executor {
1004
2941
  switch (current.type) {
1005
2942
  case 'if': {
1006
2943
  if (this.evaluate(current.args, scope)) {
1007
- this._runBodyWithYields(current.body, scope, collector);
2944
+ const _sig = this._runBodyWithYields(current.body, scope, collector);
1008
2945
  executed = true;
2946
+ if (_sig === 'break') throw { __break: true };
2947
+ if (_sig === 'continue') throw { __continue: true };
1009
2948
  } else current = current.next;
1010
2949
  break;
1011
2950
  }
1012
- case 'else': { this._runBodyWithYields(current.body, scope, collector); executed = true; break; }
2951
+ case 'else': {
2952
+ const _sig = this._runBodyWithYields(current.body, scope, collector);
2953
+ executed = true;
2954
+ if (_sig === 'break') throw { __break: true };
2955
+ if (_sig === 'continue') throw { __continue: true };
2956
+ break;
2957
+ }
1013
2958
  case 'while': {
1014
2959
  const c = Array.isArray(current.args) ? current.args[0] : current.args;
1015
2960
  while (this.evaluate(c, scope)) {
@@ -1026,7 +2971,11 @@ class Executor {
1026
2971
  }
1027
2972
  executed = true; break;
1028
2973
  }
1029
- default: { this.execute(node, scope); executed = true; break; }
2974
+ default: {
2975
+ try { this.execute(node, scope); }
2976
+ catch (e) { if (e && '__yield' in e) { collector.push(e.__yield); } else throw e; }
2977
+ executed = true; break;
2978
+ }
1030
2979
  }
1031
2980
  }
1032
2981
  return;
@@ -1097,6 +3046,16 @@ class Executor {
1097
3046
  switch (node.kind) {
1098
3047
  case 'EOF': return undefined;
1099
3048
 
3049
+ // ── @classic compatibility block ──
3050
+ // All Classic novac keywords (foreach, gear, echo, macro, etc.) are
3051
+ // only parseable inside @classic { } blocks. The executor just runs
3052
+ // their body statements normally — the gating happens at parse time.
3053
+ case 'classic_block': {
3054
+ let result;
3055
+ for (const n of node.body) result = this.execute(n, scope);
3056
+ return result;
3057
+ }
3058
+
1100
3059
  // ── type declarations ──
1101
3060
  case 'type_decl': {
1102
3061
  this.types.registerType(node.name, node);
@@ -1187,7 +3146,8 @@ class Executor {
1187
3146
  // ── declare ──
1188
3147
  case 'declare': {
1189
3148
  if (node.destructure) {
1190
- const val = this.evaluate(node.value, scope);
3149
+ const evaled = this.evaluate(node.value, scope);
3150
+ const val = evaled instanceof NovaObject ? evaled.inner : evaled;
1191
3151
  if (node.destructure.kind === 'objpattern') {
1192
3152
  for (const { key, alias, defaultValue } of node.destructure.props) {
1193
3153
  let v = val && Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;
@@ -1207,15 +3167,143 @@ class Executor {
1207
3167
  }
1208
3168
  return val;
1209
3169
  }
1210
- scope.set(node.name, undefined);
1211
- let val = this.evaluate(node.value, scope);
1212
- if (node.isPointer) val = new NovaPointer(val, (v) => v, (nv) => { val.inner = nv; });
3170
+ scope.setOwn(node.name, undefined);
3171
+ const isLazy = node.modifiers?.lazy;
3172
+ let val = isLazy ? undefined : this.evaluate(node.value, scope);
3173
+ if (!isLazy && node.isPointer) val = new NovaPointer(val, (v) => v, (nv) => { val.inner = nv; });
1213
3174
  // runtime type check on declare
1214
- if (node.explicitType && !this.types.check(val, node.explicitType)) {
1215
- const tn = node.explicitType.name || JSON.stringify(node.explicitType);
1216
- throw new Error("Variable '" + node.name + "': expected " + tn + ", got " + this._typeOf(val));
3175
+ if (node.explicitType) {
3176
+ const checkType = node.explicitType;
3177
+ const hasValueUnion = checkType.kind === 'union_type' &&
3178
+ checkType.variants.some(v => v.kind === 'value_type');
3179
+
3180
+ if (!this.types.check(val, checkType)) {
3181
+ const tn = checkType.name || (hasValueUnion
3182
+ ? checkType.variants.map(v => v.kind === 'value_type' ? String(v.value) : v.name).join(' | ')
3183
+ : JSON.stringify(checkType));
3184
+ throw new Error("Variable '" + node.name + "': expected " + tn + ", got " + this._typeOf(val) + '(' + val + ')');
3185
+ }
3186
+
3187
+ // If it's a value union, build a descriptor that enforces it on every write
3188
+ if (hasValueUnion && !node.modifiers) {
3189
+ const self = this;
3190
+ let _val = val;
3191
+ const applySet = (v) => {
3192
+ if (!self.types.check(v, checkType)) {
3193
+ const tn2 = checkType.variants.map(vt => vt.kind === 'value_type' ? String(vt.value) : vt.name).join(' | ');
3194
+ throw new Error("Variable '" + node.name + "': value must be one of [" + tn2 + "], got " + String(v));
3195
+ }
3196
+ _val = v;
3197
+ };
3198
+ scope.variables[node.name] = { read: () => _val, write: applySet };
3199
+ return val;
3200
+ }
3201
+ }
3202
+
3203
+ // ── smart variable modifiers ──
3204
+ const mods = node.modifiers;
3205
+ if (mods) {
3206
+ const self = this;
3207
+ const varName = node.name;
3208
+
3209
+ // Build constraint checker from explicitType (value union)
3210
+ let typeConstraint = null;
3211
+ if (node.explicitType && node.explicitType.kind === 'union_type') {
3212
+ // Check if it's a value-union (has literal values) or type-union
3213
+ typeConstraint = (v) => {
3214
+ if (!self.types.check(v, node.explicitType)) {
3215
+ // Also do value equality check for each variant
3216
+ const ok = node.explicitType.variants.some(variant => {
3217
+ if (variant.kind === 'named_type') {
3218
+ // Try as literal value: "true","false","null", or a quoted string
3219
+ const n = variant.name;
3220
+ if (n === 'true') return v === true;
3221
+ if (n === 'false') return v === false;
3222
+ if (n === 'null') return v == null;
3223
+ return self.types.check(v, variant);
3224
+ }
3225
+ return false;
3226
+ });
3227
+ if (!ok) throw new Error("Variable '" + varName + "': value must be one of the declared union variants, got " + self._typeOf(v) + "(" + v + ")");
3228
+ }
3229
+ };
3230
+ }
3231
+
3232
+ // clamp constraint (fnum/fint)
3233
+ let clampFn = null;
3234
+ if (mods.clampType) {
3235
+ const rangeExpr = mods.clampExpr;
3236
+ clampFn = (v) => {
3237
+ const range = self.evaluate(rangeExpr, scope);
3238
+ const arr = range instanceof NovaArray ? range.inner : (Array.isArray(range) ? range : [range]);
3239
+ const min = Number(arr[0] ?? -Infinity), max = Number(arr[1] ?? Infinity);
3240
+ let n = typeof v === 'number' ? v : Number(v);
3241
+ n = Math.min(max, Math.max(min, n));
3242
+ if (mods.clampType === 'fint') n = Math.trunc(n);
3243
+ return n;
3244
+ };
3245
+ }
3246
+
3247
+ // Build getter/setter functions
3248
+ const getterFn = mods.getter ? this.evaluate(mods.getter, scope) : null;
3249
+ const setterFn = mods.setter ? this.evaluate(mods.setter, scope) : null;
3250
+
3251
+ // State
3252
+ let _val = val;
3253
+ let _frozen = false;
3254
+ let _lazyReady = !mods.lazy;
3255
+ const _lazyNode = mods.lazy ? node.value : null;
3256
+
3257
+ const applySet = (v) => {
3258
+ if ((mods.frozen || mods.once) && _frozen) throw new Error("Variable '" + varName + "' is " + (mods.once ? 'write-once and already set' : 'frozen and cannot be reassigned'));
3259
+ if (mods.nonull && (v === null || v === undefined || (typeof v === 'symbol' && v.toString().includes('NOVA_NULL')) || v instanceof NovaNull)) throw new Error("Variable '" + varName + "' cannot be null or undefined");
3260
+ if (clampFn) v = clampFn(v);
3261
+ if (typeConstraint) typeConstraint(v);
3262
+ const old = _val;
3263
+ if (setterFn) {
3264
+ const result = self._call(setterFn, [v, old]);
3265
+ if (result !== undefined) v = result;
3266
+ }
3267
+ if (mods.tracked) console.log('[tracked] ' + varName + ': ' + self.stringify(old) + ' → ' + self.stringify(v));
3268
+ _val = v;
3269
+ _lazyReady = true;
3270
+ };
3271
+
3272
+ const applyGet = () => {
3273
+ if (!_lazyReady) {
3274
+ _val = self.evaluate(_lazyNode, scope);
3275
+ if (clampFn) _val = clampFn(_val);
3276
+ if (mods.tracked) console.log('[tracked] ' + varName + ': (lazy init) → ' + self.stringify(_val));
3277
+ _lazyReady = true;
3278
+ }
3279
+ if (getterFn) return self._call(getterFn, [_val]);
3280
+ return _val;
3281
+ };
3282
+
3283
+ // Apply initial value — run through full pipeline including setter
3284
+ if (val !== undefined) {
3285
+ if (clampFn) val = clampFn(val);
3286
+ if (typeConstraint) typeConstraint(val);
3287
+ if (setterFn) {
3288
+ const result = self._call(setterFn, [val, undefined]);
3289
+ if (result !== undefined) val = result;
3290
+ }
3291
+ if (mods.tracked) console.log('[tracked] ' + varName + ': (init) → ' + self.stringify(val));
3292
+ _val = val;
3293
+ }
3294
+ // After init: freeze if frozen or once
3295
+ if (mods.frozen || mods.once) _frozen = true;
3296
+
3297
+ // Register as descriptor in scope
3298
+ scope.variables[varName] = { read: applyGet, write: applySet };
3299
+ return _val;
1217
3300
  }
1218
- return scope.set(node.name, val, node.isConst);
3301
+
3302
+ return scope.setOwn(node.name, val, node.isConst);
3303
+ }
3304
+
3305
+ case 'dot_cmd': {
3306
+ return this.dot_commands[node.cmd.value](...node.args)
1219
3307
  }
1220
3308
 
1221
3309
  // ── branch ──
@@ -1224,38 +3312,72 @@ class Executor {
1224
3312
  while (current && !executed) {
1225
3313
  switch (current.type) {
1226
3314
  case 'if': {
1227
- if (this.evaluate(current.args, scope)) { this.runLoop(current.body, scope); executed = true; }
1228
- else current = current.next;
3315
+ if (this.evaluate(current.args, scope)) {
3316
+ const bs = new Scope('block', scope, this.globalScope);
3317
+ const _sig = this.runLoop(current.body, bs);
3318
+ executed = true;
3319
+ if (_sig === 'break') throw { __break: true };
3320
+ if (_sig === 'continue') throw { __continue: true };
3321
+ } else current = current.next;
1229
3322
  break;
1230
3323
  }
1231
- case 'else': { this.runLoop(current.body, scope); executed = true; break; }
1232
- case 'unless': { if (!this.evaluate(current.args, scope)) this.runLoop(current.body, scope); executed = true; break; }
3324
+ case 'else': {
3325
+ const bs = new Scope('block', scope, this.globalScope);
3326
+ const _sig = this.runLoop(current.body, bs);
3327
+ executed = true;
3328
+ if (_sig === 'break') throw { __break: true };
3329
+ if (_sig === 'continue') throw { __continue: true };
3330
+ break;
3331
+ }
3332
+ case 'unless': {
3333
+ if (!this.evaluate(current.args, scope)) {
3334
+ const bs = new Scope('block', scope, this.globalScope);
3335
+ const _sig = this.runLoop(current.body, bs);
3336
+ if (_sig === 'break') throw { __break: true };
3337
+ if (_sig === 'continue') throw { __continue: true };
3338
+ }
3339
+ executed = true; break;
3340
+ }
1233
3341
  case 'while': {
1234
3342
  const c = Array.isArray(current.args) ? current.args[0] : current.args;
1235
- while (this.evaluate(c, scope)) { if (this.runLoop(current.body, scope) === 'break') break; }
3343
+ while (this.evaluate(c, scope)) {
3344
+ const bs = new Scope('block', scope, this.globalScope);
3345
+ if (this.runLoop(current.body, bs) === 'break') break;
3346
+ }
1236
3347
  executed = true; break;
1237
3348
  }
1238
3349
  case 'until': {
1239
3350
  const c = Array.isArray(current.args) ? current.args[0] : current.args;
1240
- while (!this.evaluate(c, scope)) { if (this.runLoop(current.body, scope) === 'break') break; }
3351
+ while (!this.evaluate(c, scope)) {
3352
+ const bs = new Scope('block', scope, this.globalScope);
3353
+ if (this.runLoop(current.body, bs) === 'break') break;
3354
+ }
1241
3355
  executed = true; break;
1242
3356
  }
1243
3357
  case 'do': {
1244
3358
  const c = Array.isArray(current.args) ? current.args[0] : current.args;
1245
- do { if (this.runLoop(current.body, scope) === 'break') break; } while (this.evaluate(c, scope));
3359
+ do {
3360
+ const bs = new Scope('block', scope, this.globalScope);
3361
+ if (this.runLoop(current.body, bs) === 'break') break;
3362
+ } while (this.evaluate(c, scope));
1246
3363
  executed = true; break;
1247
3364
  }
1248
3365
  case 'repeat': {
1249
3366
  const t = this.evaluate(Array.isArray(current.args) ? current.args[0] : current.args, scope);
1250
- for (let i = 0; i < t; i++) { if (this.runLoop(current.body, scope) === 'break') break; }
3367
+ for (let i = 0; i < t; i++) {
3368
+ const bs = new Scope('block', scope, this.globalScope);
3369
+ if (this.runLoop(current.body, bs) === 'break') break;
3370
+ }
1251
3371
  executed = true; break;
1252
3372
  }
1253
3373
  case 'for': {
1254
3374
  const [init, cond, upd] = current.args;
1255
- if (init) this.execute(init, scope);
1256
- while (!cond || this.evaluate(cond, scope)) {
1257
- if (this.runLoop(current.body, scope) === 'break') break;
1258
- if (upd) this.execute(upd, scope);
3375
+ const bs = new Scope('block', scope, this.globalScope);
3376
+ if (init) this.execute(init, bs);
3377
+ while (!cond || this.evaluate(cond, bs)) {
3378
+ const loopBs = new Scope('block', bs, this.globalScope);
3379
+ if (this.runLoop(current.body, loopBs) === 'break') break;
3380
+ if (upd) this.execute(upd, bs);
1259
3381
  }
1260
3382
  executed = true; break;
1261
3383
  }
@@ -1267,7 +3389,11 @@ class Executor {
1267
3389
 
1268
3390
  case 'for_of': {
1269
3391
  const it = this._toIterable(this.evaluate(node.iterable, scope));
1270
- for (const val of it) { scope.set(node.varName, val); if (this.runLoop(node.body, scope) === 'break') break; }
3392
+ for (const val of it) {
3393
+ const bs = new Scope('block', scope, this.globalScope);
3394
+ bs.set(node.varName, val);
3395
+ if (this.runLoop(node.body, bs) === 'break') break;
3396
+ }
1271
3397
  break;
1272
3398
  }
1273
3399
  case 'for_in': {
@@ -1276,15 +3402,20 @@ class Executor {
1276
3402
  : obj instanceof NovaStruct ? Object.keys(obj.inner)
1277
3403
  : obj instanceof Scope ? Object.keys(obj.variables).filter(k => k !== 'scope' && k !== 'array')
1278
3404
  : Object.keys(obj || {});
1279
- for (const key of keys) { scope.set(node.varName, key); if (this.runLoop(node.body, scope) === 'break') break; }
3405
+ for (const key of keys) {
3406
+ const bs = new Scope('block', scope, this.globalScope);
3407
+ bs.set(node.varName, key);
3408
+ if (this.runLoop(node.body, bs) === 'break') break;
3409
+ }
1280
3410
  break;
1281
3411
  }
1282
3412
  case 'each': {
1283
3413
  const it = this._toIterable(this.evaluate(node.iterable, scope));
1284
3414
  for (let i = 0; i < it.length; i++) {
1285
- scope.set(node.varName, it[i]);
1286
- if (node.indexName) scope.set(node.indexName, i);
1287
- if (this.runLoop(node.body, scope) === 'break') break;
3415
+ const bs = new Scope('block', scope, this.globalScope);
3416
+ bs.set(node.varName, it[i]);
3417
+ if (node.indexName) bs.set(node.indexName, i);
3418
+ if (this.runLoop(node.body, bs) === 'break') break;
1288
3419
  }
1289
3420
  break;
1290
3421
  }
@@ -1329,9 +3460,13 @@ class Executor {
1329
3460
  const name = this.stringify(this.evaluate(node.event, scope));
1330
3461
  const val = node.value ? this.evaluate(node.value, scope) : undefined;
1331
3462
  for (const h of (this.eventBus.get(name) || [])) {
1332
- const hs = new Scope('function', scope, this.globalScope);
1333
- if (h.param) hs.set(h.param, val);
1334
- this.runLoop(h.body, hs);
3463
+ if (h._native && typeof h._native === 'function') {
3464
+ try { h._native(val); } catch (_) { }
3465
+ } else {
3466
+ const hs = new Scope('function', scope, this.globalScope);
3467
+ if (h.param) hs.set(h.param, val);
3468
+ this.runLoop(h.body, hs);
3469
+ }
1335
3470
  }
1336
3471
  break;
1337
3472
  }
@@ -1426,6 +3561,33 @@ class Executor {
1426
3561
  case 'eval': {
1427
3562
  return this.execute(node.code, scope);
1428
3563
  }
3564
+
3565
+ case 'register_escape': {
3566
+ // register_escape NAME expr
3567
+ // Registers a custom escape sequence into the lexer's CUSTOM_ESCAPES map.
3568
+ // The value can be:
3569
+ // - a string → used as the literal replacement
3570
+ // - a function → called with (args[]) at lex time on future strings
3571
+ // (note: already-lexed strings are not retroactively affected)
3572
+ const { CUSTOM_ESCAPES } = require('./lexer');
3573
+ const val = this.evaluate(node.value, scope);
3574
+ if (typeof val === 'string' || typeof val === 'function') {
3575
+ CUSTOM_ESCAPES.set(node.name, val);
3576
+ } else if (val && typeof val === 'object' && typeof val.__call === 'function') {
3577
+ // Nova function — wrap as JS function that calls it
3578
+ const novafn = val;
3579
+ CUSTOM_ESCAPES.set(node.name, (args) => {
3580
+ try {
3581
+ const childScope = scope.child();
3582
+ return String(this._callFunction(novafn, args, childScope) ?? '');
3583
+ } catch (e) { return ''; }
3584
+ });
3585
+ } else {
3586
+ CUSTOM_ESCAPES.set(node.name, String(val ?? ''));
3587
+ }
3588
+ return undefined;
3589
+ }
3590
+
1429
3591
  case 'import': {
1430
3592
  const exports = this._loadModuleExports(node.source, scope);
1431
3593
  if (Array.isArray(node.names) && node.names.length > 0) {
@@ -1481,6 +3643,7 @@ class Executor {
1481
3643
  return undefined;
1482
3644
  }
1483
3645
  default: {
3646
+ if (Array.isArray(node)) return this.run(node)
1484
3647
  const r = this._execNovaClassic(node, scope);
1485
3648
  if (r !== null) return r;
1486
3649
  this.error('Unknown node kind: ' + node.kind, node);
@@ -1496,6 +3659,10 @@ class Executor {
1496
3659
  switch (expr.kind) {
1497
3660
  case 'EOF': return undefined;
1498
3661
 
3662
+ case 'regex_literal':
3663
+ let pattern = this.evaluate(expr.pattern);
3664
+ return new RegExp(typeof pattern === 'string' ? pattern : pattern.inner, expr.flags);
3665
+
1499
3666
  case 'url_literal':
1500
3667
  return expr.value;
1501
3668
 
@@ -1508,7 +3675,7 @@ class Executor {
1508
3675
  case 'fetch_expr': {
1509
3676
  const url = this.evaluate(expr.url, scope);
1510
3677
  const options = expr.options ? this.evaluate(expr.options, scope) : null;
1511
- return this._syncFetch(url, options);
3678
+ return fetch(url, options);
1512
3679
  }
1513
3680
 
1514
3681
  case 'value': {
@@ -1516,13 +3683,160 @@ class Executor {
1516
3683
  if (typeof raw === 'string' || typeof raw === 'number') return raw;
1517
3684
  if (typeof raw === 'symbol') {
1518
3685
  const s = raw.toString();
1519
- if (s === 'Symbol(NOVA_TRUE)') return new NovaBool(true);
1520
- if (s === 'Symbol(NOVA_FALSE)') return new NovaBool(false);
3686
+ if (s === 'Symbol(NOVA_TRUE)') return true;
3687
+ if (s === 'Symbol(NOVA_FALSE)') return false;
1521
3688
  if (s === 'Symbol(NOVA_NULL)') return new NovaNull();
1522
3689
  }
1523
3690
  return raw;
1524
3691
  }
1525
3692
 
3693
+ // ── Dynamic / meta variables ──────────────────────────────────────────
3694
+ case 'dvar': {
3695
+ const _os = require('os');
3696
+ const _pth = require('path');
3697
+ const _cry = require('crypto');
3698
+ const dname = expr.name;
3699
+
3700
+ switch (dname) {
3701
+ case 'default':
3702
+ // Return the sentinel so runFunctionNode knows to use the param default
3703
+ return DEFAULT_SENTINEL;
3704
+
3705
+ case '__line__':
3706
+ return expr.srcLine ?? 0;
3707
+
3708
+ case '__col__':
3709
+ return expr.srcCol ?? 0;
3710
+
3711
+ case '__file__': {
3712
+ const f = process.argv[1];
3713
+ return f ? _pth.resolve(f) : '<repl>';
3714
+ }
3715
+
3716
+ case '__dirname__': {
3717
+ const f = process.argv[1];
3718
+ return f ? _pth.dirname(_pth.resolve(f)) : process.cwd();
3719
+ }
3720
+
3721
+ case '__func__': {
3722
+ // Walk scope chain looking for a named function scope
3723
+ let s = scope;
3724
+ while (s) {
3725
+ if (s.kind === 'function' && s._funcName) return s._funcName;
3726
+ s = s.parent;
3727
+ }
3728
+ return '<top>';
3729
+ }
3730
+
3731
+ case '__date__': {
3732
+ const d = new Date();
3733
+ return d.toISOString().slice(0, 10);
3734
+ }
3735
+
3736
+ case '__time__': {
3737
+ const d = new Date();
3738
+ return d.toISOString().slice(11, 19);
3739
+ }
3740
+
3741
+ case '__datetime__':
3742
+ return new Date().toISOString();
3743
+
3744
+ case '__timestamp__':
3745
+ return Date.now();
3746
+
3747
+ case '__version__':
3748
+ return '2.0';
3749
+
3750
+ case '__pid__':
3751
+ return process.pid;
3752
+
3753
+ case '__platform__':
3754
+ return process.platform;
3755
+
3756
+ case '__arch__':
3757
+ return _os.arch();
3758
+
3759
+ case '__argv__':
3760
+ return new NovaArray(process.argv);
3761
+
3762
+ case '__env__':
3763
+ return new NovaObject({ ...process.env });
3764
+
3765
+ case '__scope__':
3766
+ return scope;
3767
+
3768
+ case '__caller__': {
3769
+ let s2 = scope && scope.parent;
3770
+ while (s2) {
3771
+ if (s2.kind === 'function' && s2._funcName) return s2._funcName;
3772
+ s2 = s2.parent;
3773
+ }
3774
+ return '<top>';
3775
+ }
3776
+
3777
+ case '__module__': {
3778
+ const f2 = process.argv[1];
3779
+ if (!f2) return '<repl>';
3780
+ return _pth.basename(f2, _pth.extname(f2));
3781
+ }
3782
+
3783
+ case '__namespace__': {
3784
+ // Walk scope looking for a namespace marker
3785
+ let s3 = scope;
3786
+ while (s3) {
3787
+ if (s3._namespace) return s3._namespace;
3788
+ s3 = s3.parent;
3789
+ }
3790
+ return null;
3791
+ }
3792
+
3793
+ case '__random__':
3794
+ return Math.random();
3795
+
3796
+ case '__uuid__':
3797
+ return _cry.randomUUID ? _cry.randomUUID() : _cry.randomBytes(16).toString('hex');
3798
+
3799
+ case '__iter__': {
3800
+ // Walk scopes looking for an active loop counter
3801
+ let s4 = scope;
3802
+ while (s4) {
3803
+ if (s4._loopIndex !== undefined) return s4._loopIndex;
3804
+ s4 = s4.parent;
3805
+ }
3806
+ return -1;
3807
+ }
3808
+
3809
+ case '__stack__': {
3810
+ const frames = [];
3811
+ let s5 = scope;
3812
+ while (s5) {
3813
+ if (s5.kind === 'function') frames.push(s5._funcName || '<anon>');
3814
+ s5 = s5.parent;
3815
+ }
3816
+ return new NovaArray(frames);
3817
+ }
3818
+
3819
+ default: {
3820
+ // Check kit-registered custom dvars
3821
+ if (this._customDvars && this._customDvars.has(dname)) {
3822
+ try { return this._customDvars.get(dname)(scope, this); } catch (_) { return null; }
3823
+ }
3824
+ return null;
3825
+ }
3826
+ }
3827
+ }
3828
+
3829
+ // { key? true/false } — boolean prop literal
3830
+ case 'bool': return new NovaBool(expr.value);
3831
+
3832
+ // { key? <expr> } where expr must evaluate to a boolean
3833
+ case 'bool_assert': {
3834
+ const v = this.evaluate(expr.expr, scope);
3835
+ const raw = v instanceof NovaBool ? v.valueOf() : v;
3836
+ if (typeof raw !== 'boolean') throw new Error("Boolean property expression must be true or false, got " + this._typeOf(v));
3837
+ return new NovaBool(raw);
3838
+ }
3839
+
1526
3840
  // inline function node -> make callable
1527
3841
  case 'function': {
1528
3842
  const fn = (...args) => this.runFunctionNode(expr, scope, args);
@@ -1537,12 +3851,15 @@ class Executor {
1537
3851
  }
1538
3852
 
1539
3853
  case 'ref': {
1540
- let i = 0;
1541
3854
  function getRef(nv, namee = expr.name) {
1542
- if (scope.has(namee)) {
1543
- return scope.get(namee)
3855
+ const val = scope.get(namee);
3856
+ if (val !== null && val !== undefined) {
3857
+ return val;
1544
3858
  } else if (nv.blocks.has(namee)) {
1545
3859
  return nv.runFunctionNode(nv.blocks.get(namee), nv.blocks.get(namee).scope, [])
3860
+ } else if (nv._customDvars && nv._customDvars.has(namee)) {
3861
+ // Custom dynamic variable registered via dvars or nova.registerDvar
3862
+ try { return nv._customDvars.get(namee)(scope, nv); } catch (_) { return null; }
1546
3863
  } else if (nv.options.unknownRefs) {
1547
3864
  if (nv.optionDecls.hasOwnProperty('unknownRefs')) {
1548
3865
  return nv.optionDecls.unknownRefs(namee)
@@ -1571,6 +3888,8 @@ class Executor {
1571
3888
 
1572
3889
  case 'object': {
1573
3890
  const obj = {};
3891
+ const boolKeys = new Set();
3892
+ let metaDescriptor = null;
1574
3893
  for (const entry of expr.props) {
1575
3894
  if (entry.kind === 'spread') {
1576
3895
  const v = this.evaluate(entry.value, scope);
@@ -1578,8 +3897,20 @@ class Executor {
1578
3897
  : v instanceof Scope ? v.variables : (v && typeof v === 'object' ? v : {});
1579
3898
  Object.assign(obj, src);
1580
3899
  } else if (entry.kind === 'computed') {
1581
- const key = this.stringify(this.evaluate(entry.key, scope));
1582
- obj[key] = this.evaluate(entry.value, scope);
3900
+ const keyVal = this.evaluate(entry.key, scope);
3901
+ const key = this.stringify(keyVal);
3902
+ // Detect [method] key used as meta-protocol marker
3903
+ if (key === '__meta__' || (entry.key.kind === 'ref' && entry.key.name === 'method')) {
3904
+ metaDescriptor = this.evaluate(entry.value, scope);
3905
+ } else if (key === '__expr__' || (entry.key.kind === 'ref' && entry.key.name === 'expr')) {
3906
+ //detect [expr] key
3907
+ obj['.expr'] = this.evaluate(entry.value, scope);
3908
+ } else if (key === '__op__' || (entry.key.kind === 'ref' && entry.key.name === 'op')) {
3909
+ //detect [operator] key
3910
+ obj['__operators'] = this.evaluate(entry.value, scope);
3911
+ } else {
3912
+ obj[key] = this.evaluate(entry.value, scope);
3913
+ }
1583
3914
  } else {
1584
3915
  // prop or method shorthand
1585
3916
  const val = entry.value;
@@ -1587,12 +3918,52 @@ class Executor {
1587
3918
  const fn = (...args) => this.runFunctionNode(val, scope, args);
1588
3919
  fn.args = val.args; fn.body = val.body; try { Object.defineProperty(fn, "name", { value: val.name || entry.key, configurable: true }); } catch (_) { }
1589
3920
  obj[entry.key] = fn;
3921
+ } else if (entry.boolProp) {
3922
+ // { key? true/false } — value must be boolean; record key as bool-constrained
3923
+ const bv = this.evaluate(val, scope);
3924
+ const raw = bv instanceof NovaBool ? bv.valueOf() : bv;
3925
+ if (typeof raw !== 'boolean') throw new Error("Property '" + entry.key + "' declared with ? must be true or false");
3926
+ obj[entry.key] = raw;
3927
+ boolKeys.add(entry.key);
1590
3928
  } else {
1591
3929
  obj[entry.key] = this.evaluate(val, scope);
1592
3930
  }
1593
3931
  }
1594
3932
  }
1595
- return new NovaObject(obj);
3933
+ const novaObj = new NovaObject(obj);
3934
+ // Build combined meta: user descriptor + bool-key enforcement
3935
+ const hasBoolKeys = boolKeys.size > 0;
3936
+ if (metaDescriptor || hasBoolKeys) {
3937
+ const md = metaDescriptor
3938
+ ? (metaDescriptor instanceof NovaObject ? metaDescriptor.inner : metaDescriptor)
3939
+ : {};
3940
+ const self2 = this;
3941
+ const capturedScope = scope;
3942
+ const callMeta = (fn, args) => {
3943
+ if (!fn) return undefined;
3944
+ if (typeof fn === 'function') return fn(...args);
3945
+ if (fn && fn.args !== undefined && fn.body) return self2.runFunctionNode(fn, capturedScope, args);
3946
+ };
3947
+ novaObj.attachMeta({
3948
+ get: md.get ? (k, v) => callMeta(md.get, [k, v]) : null,
3949
+ set: (k, v, inner) => {
3950
+ // Bool-key enforcement always runs first
3951
+ if (hasBoolKeys && boolKeys.has(k)) {
3952
+ const raw = v instanceof NovaBool ? v.valueOf() : v;
3953
+ if (typeof raw !== 'boolean') throw new Error("Property '" + k + "' is bool-constrained and must be true or false, got " + self2._typeOf(v));
3954
+ v = raw; // normalise to JS bool
3955
+ }
3956
+ if (md.set) {
3957
+ const result = callMeta(md.set, [k, v]);
3958
+ if (result !== false) inner[k] = v;
3959
+ } else {
3960
+ inner[k] = v;
3961
+ }
3962
+ },
3963
+ missing: md.missing ? (k) => callMeta(md.missing, [k]) : null,
3964
+ });
3965
+ }
3966
+ return novaObj;
1596
3967
  }
1597
3968
 
1598
3969
  case 'new_expr': {
@@ -1607,7 +3978,8 @@ class Executor {
1607
3978
 
1608
3979
  case 'assign': {
1609
3980
  const val = this.evaluate(expr.value, scope);
1610
- const n = expr.name;
3981
+ let n = expr.name;
3982
+ if (n.kind === 'dexpr') n = { name: this.evaluate(n.code, scope) }
1611
3983
  if (n.kind === 'deref') {
1612
3984
  const ptr = this.evaluate(n.operand, scope);
1613
3985
  if (!(ptr instanceof NovaPointer)) this.error('Deref non-pointer', expr);
@@ -1628,6 +4000,7 @@ class Executor {
1628
4000
  else if (obj && typeof obj === 'object') obj[idx] = val;
1629
4001
  return val;
1630
4002
  }
4003
+ if (n.kind === 'link') return this.evaluate({ kind: 'assign', name: n.linked, value: { kind: 'value', value: val } }, scope);
1631
4004
  return scope.set(n.name != null ? n.name : n, val);
1632
4005
  }
1633
4006
 
@@ -1650,6 +4023,32 @@ class Executor {
1650
4023
  case 'ternary': return this.evaluate(expr.condition, scope)
1651
4024
  ? this.evaluate(expr.consequent, scope) : this.evaluate(expr.alternate, scope);
1652
4025
 
4026
+ case 'if_ternary': {
4027
+ let cond = this.evaluate(expr.cond, scope);
4028
+ if (cond) { return this.evaluate(expr.operand, scope) }
4029
+ else if (expr.elseExpr) { return this.evaluate(expr.elseExpr, scope) }
4030
+ }
4031
+
4032
+ case 'run_expr': return this.execute(expr.code, scope)
4033
+
4034
+ case 'native':
4035
+ let val = this.evaluate(expr.value);
4036
+ return val instanceof NovaValue ? val.inner : val;
4037
+
4038
+ case 'ast': return { kind:"ast", ast: expr.ast };
4039
+
4040
+ case 'link': return {
4041
+ read: () => this.evaluate(expr.linked),
4042
+ write: (val) =>
4043
+ this.evaluate({
4044
+ kind: 'assign',
4045
+ name: expr.linked,
4046
+ value: { kind: 'value', value: val }
4047
+ }, scope)
4048
+ };
4049
+
4050
+ case 'dexpr': return this.evaluate(expr.code, scope);
4051
+
1653
4052
  case 'rate_cast': {
1654
4053
  const val = this.evaluate(expr.value, scope);
1655
4054
  return this._rateCast(val, expr.cast_type);
@@ -1716,7 +4115,7 @@ class Executor {
1716
4115
  if (obj == null || obj instanceof NovaNull) return null;
1717
4116
  if (obj instanceof NovaStruct) return obj.inner[expr.name];
1718
4117
  if (obj instanceof NovaObject || obj instanceof NovaArray) return obj.get(expr.name);
1719
- return obj instanceof Scope ? obj.get(expr.name) : obj?.[expr.name];
4118
+ return obj?.[expr.name];
1720
4119
  }
1721
4120
  case 'optional_subscript': {
1722
4121
  const obj = this.evaluate(expr.object, scope);
@@ -1737,6 +4136,14 @@ class Executor {
1737
4136
 
1738
4137
  case 'arrowfunc': {
1739
4138
  const self = this;
4139
+ // If the lambda is marked async or generator, build a proper fn node
4140
+ // so runFunctionNode handles the Promise chain / yield collection.
4141
+ if (expr.isAsync || expr.isGenerator) {
4142
+ const fn = (...args) => self.runFunctionNode(expr, scope, args);
4143
+ fn.args = expr.args; fn.body = expr.body;
4144
+ fn.isAsync = expr.isAsync; fn.isGenerator = expr.isGenerator;
4145
+ return fn;
4146
+ }
1740
4147
  const fn = (...args) => {
1741
4148
  const ls = new Scope('function', scope, self.globalScope);
1742
4149
  (expr.args || []).forEach((name, i) => ls.set(typeof name === 'string' ? name : name.name, args[i]));
@@ -1756,7 +4163,7 @@ class Executor {
1756
4163
  if (obj instanceof NovaStruct) return this._structGet(obj, pn, scope);
1757
4164
  // NovaArray: check _arrayMethod first so built-in names take priority
1758
4165
  if (obj instanceof NovaArray) {
1759
- const m = this._arrayMethod(obj, pn);
4166
+ const m = this._arrayMethod(obj, pn, scope);
1760
4167
  if (m !== undefined) return m;
1761
4168
  const v = obj.get(pn);
1762
4169
  return v !== undefined ? v : null;
@@ -1767,7 +4174,8 @@ class Executor {
1767
4174
  const m = this._objectMethod(obj, pn);
1768
4175
  return m !== undefined ? m : null;
1769
4176
  }
1770
- return obj instanceof Scope ? obj.get(pn) : (obj != null ? obj[pn] : null);
4177
+ if (!(Object.hasOwnProperty(obj, 'name')) && pn === 'name') return (typeof expr.object.name === 'string') ? expr.object.name : 'anon';
4178
+ return (obj != null ? obj[pn] : null);
1771
4179
  }
1772
4180
 
1773
4181
  case 'call': {
@@ -1780,7 +4188,7 @@ class Executor {
1780
4188
  if (obj instanceof NovaStruct) fn = this._structGet(obj, expr.name.name, scope);
1781
4189
  else if (obj instanceof Scope) fn = obj.get(expr.name.name);
1782
4190
  else if (obj instanceof NovaArray) {
1783
- fn = this._arrayMethod(obj, expr.name.name);
4191
+ fn = this._arrayMethod(obj, expr.name.name, scope);
1784
4192
  if (fn === undefined) fn = obj.get(expr.name.name);
1785
4193
  }
1786
4194
  else if (obj instanceof NovaObject) {
@@ -1805,11 +4213,25 @@ class Executor {
1805
4213
 
1806
4214
  case 'binary': {
1807
4215
  const op = expr.operator;
1808
- if (op === '&&' || op === 'and') { const l = this.evaluate(expr.left, scope); return l ? this.evaluate(expr.right, scope) : l; }
1809
- if (op === '||' || op === 'or') { const l = this.evaluate(expr.left, scope); return l ? l : this.evaluate(expr.right, scope); }
1810
- if (op === '??') { const l = this.evaluate(expr.left, scope); return (l == null || l instanceof NovaNull) ? this.evaluate(expr.right, scope) : l; }
1811
4216
  const left = this.evaluate(expr.left, scope);
1812
4217
  const right = this.evaluate(expr.right, scope);
4218
+ if (left instanceof NovaObject) {
4219
+ let overload = left?.get?.('__operators')?.get?.('binary')?.get?.(expr.operator);
4220
+ if (overload) return overload(right);
4221
+ } else if (typeof left === 'object') {
4222
+ let overload = left?.__operators?.binary?.[op]
4223
+ if (overload) return overload(right);
4224
+ }
4225
+ if (right instanceof NovaObject) {
4226
+ let overload = right?.get?.('__operators')?.get?.('binary')?.get?.(expr.operator);
4227
+ if (overload) return overload(left);
4228
+ } else if (typeof right === 'object') {
4229
+ let overload = right?.__operators?.binary?.[op]
4230
+ if (overload) return overload(left);
4231
+ }
4232
+ if (op === '&&' || op === 'and') return left ? right : left;
4233
+ if (op === '||' || op === 'or') return left ? left : right
4234
+ if (op === '??') return (left == null || left instanceof NovaNull) ? right : left;
1813
4235
  switch (op) {
1814
4236
  case '+': return left + right; case '-': return left - right;
1815
4237
  case '*': return left * right; case '/': return left / right; case '%': return left % right;
@@ -1936,13 +4358,8 @@ class Executor {
1936
4358
  }
1937
4359
 
1938
4360
  case 'unary': {
4361
+ // ONE-Special handlong ones, (only delete)
1939
4362
  switch (expr.operator) {
1940
- case '-': return -this.evaluate(expr.operand, scope);
1941
- case '+': return +this.evaluate(expr.operand, scope);
1942
- case '!': case 'not': return !this.evaluate(expr.operand, scope);
1943
- case '~': return ~this.evaluate(expr.operand, scope);
1944
- case 'typeof': return this._typeOf(this.evaluate(expr.operand, scope));
1945
- case 'void': this.evaluate(expr.operand, scope); return undefined;
1946
4363
  case 'delete': {
1947
4364
  const o = expr.operand;
1948
4365
  if (o.kind === 'ref') return scope.delete(o.name);
@@ -1952,8 +4369,7 @@ class Executor {
1952
4369
  else if (obj instanceof Scope) obj.delete(o.name);
1953
4370
  else delete obj[o.name];
1954
4371
  return true;
1955
- }
1956
- if (o.kind === 'subscript') {
4372
+ } if (o.kind === 'subscript') {
1957
4373
  const obj = this.evaluate(o.object, scope); const idx = this.evaluate(o.index, scope);
1958
4374
  if (obj instanceof NovaObject || obj instanceof NovaArray) obj.delete(idx);
1959
4375
  else if (obj instanceof Scope) obj.delete(idx);
@@ -1962,6 +4378,19 @@ class Executor {
1962
4378
  }
1963
4379
  this.error('Invalid delete operand', expr);
1964
4380
  }
4381
+ }
4382
+ let operand = this.evaluate(expr.operand, scope);
4383
+ if (operand instanceof NovaObject) {
4384
+ let overload = operand?.get?.('__operators')?.get?.('unary')?.get?.(expr.operator);
4385
+ if (overload) return overload();
4386
+ }
4387
+ switch (expr.operator) {
4388
+ case '-': return -operand
4389
+ case '+': return +operand
4390
+ case '!': case 'not': return !operand
4391
+ case '~': return ~operand
4392
+ case 'typeof': return this._typeOf(operand);
4393
+ case 'void': return undefined;
1965
4394
  default: this.error("Unknown unary operator '" + expr.operator + "'", expr);
1966
4395
  }
1967
4396
  }
@@ -2013,33 +4442,13 @@ class Executor {
2013
4442
 
2014
4443
  case 'await': {
2015
4444
  const val = this.evaluate(expr.operand, scope);
4445
+ // If it's not a Promise, return as-is (mirrors JS behaviour).
2016
4446
  if (!(val instanceof Promise)) return val;
2017
-
2018
- // ── Sync-block until the promise settles no real JS async/await ──
2019
- // SharedArrayBuffer as a cross-turn signal:
2020
- // flag[0] === 0 → pending (Atomics.wait sleeps here)
2021
- // flag[0] === 1 → resolved
2022
- // flag[0] === 2 → rejected
2023
- const sab = new SharedArrayBuffer(8);
2024
- const flag = new Int32Array(sab);
2025
- let resolved, rejected;
2026
-
2027
- this.awaiting = true;
2028
-
2029
- val.then(
2030
- (v) => { resolved = v; Atomics.store(flag, 0, 1); Atomics.notify(flag, 0); },
2031
- (e) => { rejected = e; Atomics.store(flag, 0, 2); Atomics.notify(flag, 0); }
2032
- );
2033
-
2034
- // Block synchronously until the microtask above fires.
2035
- // Atomics.wait is always allowed in Node.js worker threads and the
2036
- // main thread when --allow-atomics-wait is set (default in most envs).
2037
- Atomics.wait(flag, 0, 0);
2038
-
2039
- this.awaiting = false;
2040
-
2041
- if (Atomics.load(flag, 0) === 2) throw rejected;
2042
- return resolved;
4447
+ // Return the Promise itself — the caller (runFunctionNode for async fns,
4448
+ // or top-level run) must handle it with .then() chaining.
4449
+ // We signal suspension by throwing an AwaitSuspension so the nearest
4450
+ // async function frame can chain the continuation properly.
4451
+ throw { __awaitSuspension__: true, promise: val };
2043
4452
  }
2044
4453
 
2045
4454
  default: this.error('Unknown expression kind: ' + expr.kind, expr);
@@ -2064,21 +4473,22 @@ class Executor {
2064
4473
  }
2065
4474
 
2066
4475
  // ── built-in array methods ──
2067
- _arrayMethod(arr, name) {
4476
+ _arrayMethod(arr, name, scope) {
2068
4477
  const inner = arr.inner;
2069
4478
  const self = this;
4479
+ const sc = scope || self.globalScope;
2070
4480
  const wrap = (v) => Array.isArray(v) ? new NovaArray(v) : v;
2071
4481
  switch (name) {
2072
- case 'map': return (fn) => new NovaArray(inner.map((v, i) => self._call(fn, [v, i])));
2073
- case 'filter': return (fn) => new NovaArray(inner.filter((v, i) => self._call(fn, [v, i])));
2074
- case 'reduce': return (fn, init) => inner.reduce((acc, v, i) => self._call(fn, [acc, v, i]), init);
2075
- case 'find': return (fn) => inner.find((v, i) => self._call(fn, [v, i]));
2076
- case 'findIndex': return (fn) => inner.findIndex((v, i) => self._call(fn, [v, i]));
2077
- case 'some': return (fn) => inner.some((v, i) => self._call(fn, [v, i]));
2078
- case 'every': return (fn) => inner.every((v, i) => self._call(fn, [v, i]));
4482
+ case 'map': return (fn) => new NovaArray(inner.map((v, i) => self._call(fn, [v, i], sc)));
4483
+ case 'filter': return (fn) => new NovaArray(inner.filter((v, i) => self._call(fn, [v, i], sc)));
4484
+ case 'reduce': return (fn, init) => inner.reduce((acc, v, i) => self._call(fn, [acc, v, i], sc), init);
4485
+ case 'find': return (fn) => inner.find((v, i) => self._call(fn, [v, i], sc));
4486
+ case 'findIndex': return (fn) => inner.findIndex((v, i) => self._call(fn, [v, i], sc));
4487
+ case 'some': return (fn) => inner.some((v, i) => self._call(fn, [v, i], sc));
4488
+ case 'every': return (fn) => inner.every((v, i) => self._call(fn, [v, i], sc));
2079
4489
  case 'flat': return (depth = 1) => new NovaArray(inner.flat(depth));
2080
- case 'flatMap': return (fn) => new NovaArray(inner.flatMap((v, i) => { const r = self._call(fn, [v, i]); return r instanceof NovaArray ? r.inner : r; }));
2081
- case 'sort': return (fn) => { const copy = [...inner]; copy.sort((a, b) => fn ? self._call(fn, [a, b]) : (a > b ? 1 : a < b ? -1 : 0)); return new NovaArray(copy); };
4490
+ case 'flatMap': return (fn) => new NovaArray(inner.flatMap((v, i) => { const r = self._call(fn, [v, i], sc); return r instanceof NovaArray ? r.inner : r; }));
4491
+ case 'sort': return (fn) => { const copy = [...inner]; copy.sort((a, b) => fn ? self._call(fn, [a, b], sc) : (a > b ? 1 : a < b ? -1 : 0)); return new NovaArray(copy); };
2082
4492
  case 'reverse': return () => new NovaArray([...inner].reverse());
2083
4493
  case 'slice': return (a, b) => new NovaArray(inner.slice(a, b));
2084
4494
  case 'splice': return (start, del, ...items) => { const removed = inner.splice(start, del ?? inner.length, ...items); return new NovaArray(removed); };
@@ -2091,7 +4501,7 @@ class Executor {
2091
4501
  case 'unshift': return (...vals) => { inner.unshift(...vals); return inner.length; };
2092
4502
  case 'concat': return (...others) => new NovaArray(inner.concat(...others.map(o => o instanceof NovaArray ? o.inner : o)));
2093
4503
  case 'fill': return (v, s, e) => { inner.fill(v, s, e); return arr; };
2094
- case 'forEach': return (fn) => new NovaArray(inner.forEach((v, i) => self._call(fn, [v, i])));
4504
+ case 'forEach': return (fn) => new NovaArray(inner.forEach((v, i) => self._call(fn, [v, i], sc)));
2095
4505
  case 'keys': return () => new NovaArray([...inner.keys()]);
2096
4506
  case 'values': return () => new NovaArray([...inner.values()]);
2097
4507
  case 'entries': return () => new NovaArray(inner.map((v, i) => new NovaArray([i, v])));
@@ -2105,7 +4515,7 @@ class Executor {
2105
4515
  case 'groupBy': return (fn) => {
2106
4516
  const groups = {};
2107
4517
  inner.forEach(v => {
2108
- const key = self._call(fn, [v]);
4518
+ const key = self._call(fn, [v], sc);
2109
4519
  if (!groups[key]) groups[key] = [];
2110
4520
  groups[key].push(v);
2111
4521
  });
@@ -2121,8 +4531,6 @@ class Executor {
2121
4531
  }
2122
4532
  return new NovaArray(copy);
2123
4533
  };
2124
- case 'reverse': return () => new NovaArray([...inner].reverse());
2125
- case 'sort': return (fn) => { const copy = [...inner]; copy.sort((a, b) => fn ? self._call(fn, [a, b]) : (a > b ? 1 : a < b ? -1 : 0)); return new NovaArray(copy); };
2126
4534
  case 'min': return () => inner.reduce((min, v) => v < min ? v : min, inner[0]);
2127
4535
  case 'max': return () => inner.reduce((max, v) => v > max ? v : max, inner[0]);
2128
4536
  case 'sum': return () => inner.reduce((acc, v) => acc + v, 0);
@@ -2175,9 +4583,9 @@ class Executor {
2175
4583
  }
2176
4584
 
2177
4585
  // call a nova or JS fn
2178
- _call(fn, args) {
4586
+ _call(fn, args, scope) {
2179
4587
  if (typeof fn === 'function') return fn(...args);
2180
- if (fn && fn.args !== undefined && fn.body) return this.runFunctionNode(fn, this.globalScope, args);
4588
+ if (fn && fn.args !== undefined && fn.body) return this.runFunctionNode(fn, scope || this.globalScope, args);
2181
4589
  return fn;
2182
4590
  }
2183
4591
 
@@ -3199,4 +5607,4 @@ req.end();`;
3199
5607
  }
3200
5608
  }
3201
5609
 
3202
- module.exports = { Executor, Scope, TypeRegistry };
5610
+ module.exports = { Executor, Scope, TypeRegistry };