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