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.
- package/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
package/src/core/executor.js
CHANGED
|
@@ -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 '
|
|
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('
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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('
|
|
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
|
-
|
|
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)
|
|
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')
|
|
464
|
-
if (typeof v._v === 'string')
|
|
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','
|
|
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
|
|
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
|
|
2206
|
+
this._fnNode = fnNode;
|
|
532
2207
|
this._callerScope = callerScope;
|
|
533
|
-
this._worker
|
|
534
|
-
this._mutations
|
|
2208
|
+
this._worker = null;
|
|
2209
|
+
this._mutations = []; // { name, value } buffered until join()
|
|
535
2210
|
this._msgHandlers = [];
|
|
536
|
-
this._done
|
|
537
|
-
this._result
|
|
538
|
-
this._error
|
|
539
|
-
this._sab
|
|
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:
|
|
548
|
-
join:
|
|
549
|
-
send:
|
|
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()
|
|
552
|
-
get error()
|
|
553
|
-
get 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
|
-
|
|
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',
|
|
2447
|
+
const hs = new Scope('function', execScope, this.globalScope);
|
|
701
2448
|
this.runLoop(h.body, hs);
|
|
702
2449
|
}
|
|
703
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
|
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': {
|
|
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: {
|
|
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
|
|
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.
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
|
1215
|
-
const
|
|
1216
|
-
|
|
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
|
-
|
|
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)) {
|
|
1228
|
-
|
|
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': {
|
|
1232
|
-
|
|
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)) {
|
|
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)) {
|
|
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 {
|
|
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++) {
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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) {
|
|
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) {
|
|
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
|
-
|
|
1286
|
-
|
|
1287
|
-
if (
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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
|
|
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
|
|
1520
|
-
if (s === 'Symbol(NOVA_FALSE)') return
|
|
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
|
-
|
|
1543
|
-
|
|
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
|
|
1582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
2019
|
-
//
|
|
2020
|
-
//
|
|
2021
|
-
|
|
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 };
|