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,762 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nova Parser (CommonJS)
|
|
3
|
+
* -----------------------
|
|
4
|
+
* Generates Nova-style AST with full expression support.
|
|
5
|
+
*/
|
|
6
|
+
const { Lexer } = require("./lexer");
|
|
7
|
+
const { CustomError } = require("./error");
|
|
8
|
+
|
|
9
|
+
// Operator precedence map
|
|
10
|
+
const PRECEDENCE = {
|
|
11
|
+
"||": 1,
|
|
12
|
+
"&&": 2,
|
|
13
|
+
"==": 3,
|
|
14
|
+
"!=": 3,
|
|
15
|
+
"<": 4,
|
|
16
|
+
"<=": 4,
|
|
17
|
+
">": 4,
|
|
18
|
+
">=": 4,
|
|
19
|
+
"+": 5,
|
|
20
|
+
"-": 5,
|
|
21
|
+
"*": 6,
|
|
22
|
+
"/": 6,
|
|
23
|
+
"%": 6,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class Parser {
|
|
27
|
+
constructor(source) {
|
|
28
|
+
this.tokens = new Lexer(source).tokenize();
|
|
29
|
+
this.rawsrc = source;
|
|
30
|
+
this.current = 0;
|
|
31
|
+
this.symbols = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
parse() {
|
|
35
|
+
const nodes = [];
|
|
36
|
+
while (!this.isAtEnd()) {
|
|
37
|
+
const node = this.statement();
|
|
38
|
+
if (node) nodes.push(node);
|
|
39
|
+
}
|
|
40
|
+
return { kind: "program", nodes };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
nd(obj) {
|
|
44
|
+
let token = this.peek();
|
|
45
|
+
return {
|
|
46
|
+
...obj,
|
|
47
|
+
line: token.line ?? 0,
|
|
48
|
+
column: token.column ?? 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─────────────── Statements ───────────────
|
|
53
|
+
statement() {
|
|
54
|
+
if (this.match("KEYWORD", "class")) return this.classDeclaration();
|
|
55
|
+
if (this.match("KEYWORD", "const")) return this.varDeclaration(true);
|
|
56
|
+
if (this.match("KEYWORD", "var") || this.match("KEYWORD", "let"))
|
|
57
|
+
return this.varDeclaration();
|
|
58
|
+
if (this.match("KEYWORD", "func")) return this.funcDeclaration(true);
|
|
59
|
+
if (this.match("KEYWORD", "if")) return this.branchBody("if");
|
|
60
|
+
if (this.match("KEYWORD", "while")) return this.branchBody("while");
|
|
61
|
+
if (this.match("KEYWORD", "repeat")) return this.branchBody("repeat");
|
|
62
|
+
if (this.match("KEYWORD", "do")) return this.branchBody("do");
|
|
63
|
+
if (this.match("KEYWORD", "until")) return this.branchBody("until");
|
|
64
|
+
if (this.match("KEYWORD", "unless")) return this.branchBody("unless");
|
|
65
|
+
if (this.match("KEYWORD", "for")) return this.branchBody("for", ";");
|
|
66
|
+
if (this.match("KEYWORD", "function")) return this.funcDeclaration();
|
|
67
|
+
if (this.match("KEYWORD", "return")) return this.returnStatement(false);
|
|
68
|
+
if (this.match("KEYWORD", "give")) return this.returnStatement(true);
|
|
69
|
+
if (this.match("KEYWORD", "throw")) return this.throwStatement();
|
|
70
|
+
if (this.match("KEYWORD", "try")) return this.tryStatement();
|
|
71
|
+
if (this.match("PUNCTUATION", "{")) return this.blockBody(true);
|
|
72
|
+
return this.expressionStatement();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
templateLiteral() {
|
|
76
|
+
this.consume("TEMPLATE_START", "Expected start of template literal");
|
|
77
|
+
|
|
78
|
+
const parts = [];
|
|
79
|
+
|
|
80
|
+
while (!this.check("TEMPLATE_END") && !this.isAtEnd()) {
|
|
81
|
+
// Raw text part
|
|
82
|
+
if (this.match("STRING_PART")) {
|
|
83
|
+
parts.push({
|
|
84
|
+
type: "Literal",
|
|
85
|
+
value: this.previous().value,
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Interpolation section
|
|
91
|
+
if (this.match("INTERPOLATION_START")) {
|
|
92
|
+
const expr = this.expression();
|
|
93
|
+
|
|
94
|
+
// Expect matching INTERPOLATION_END now (old parser expected raw '}')
|
|
95
|
+
this.consume(
|
|
96
|
+
"INTERPOLATION_END",
|
|
97
|
+
"Expected '}' after expression in template literal"
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
parts.push(expr);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Safety net for unexpected stuff
|
|
105
|
+
this.error(
|
|
106
|
+
`Unexpected token '${this.peek().value}' in template literal`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Closing backtick
|
|
111
|
+
this.consume("TEMPLATE_END", "Unterminated template literal");
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
type: "TemplateLiteral",
|
|
115
|
+
parts,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throwStatement() {
|
|
120
|
+
const value = this.expression();
|
|
121
|
+
this.match("PUNCTUATION", ";");
|
|
122
|
+
return this.nd({ kind: "throw", value });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
tryStatement() {
|
|
126
|
+
const tryBody = this.blockBody();
|
|
127
|
+
let catchBody = null;
|
|
128
|
+
let catchName = null;
|
|
129
|
+
let finallyBody = null;
|
|
130
|
+
|
|
131
|
+
if (this.match("KEYWORD", "catch")) {
|
|
132
|
+
this.consume("PUNCTUATION", "(", "Expected '(' for catch block");
|
|
133
|
+
catchName = this.consume(
|
|
134
|
+
"IDENTIFIER",
|
|
135
|
+
"Expected catch variable name",
|
|
136
|
+
).value;
|
|
137
|
+
this.consume("PUNCTUATION", ")", "Expected ')' for catch block");
|
|
138
|
+
catchBody = this.blockBody();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.match("KEYWORD", "finally")) {
|
|
142
|
+
finallyBody = this.blockBody();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!catchBody && !finallyBody) {
|
|
146
|
+
this.error("Try block must be followed by catch or finally");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return this.nd({
|
|
150
|
+
kind: "try",
|
|
151
|
+
tryBody,
|
|
152
|
+
catchBody,
|
|
153
|
+
catchName,
|
|
154
|
+
finallyBody,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
varDeclaration(isConst) {
|
|
159
|
+
let nameNode = null;
|
|
160
|
+
let destructure = null;
|
|
161
|
+
let isPointer = false;
|
|
162
|
+
if (this.match("OPERATOR", "*")) {
|
|
163
|
+
// 🔥 NEW/MODIFIED CHECK
|
|
164
|
+
isPointer = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// check destructuring pattern
|
|
168
|
+
if (this.check("PUNCTUATION", "{")) {
|
|
169
|
+
destructure = this.objectPattern();
|
|
170
|
+
} else if (this.check("PUNCTUATION", "[")) {
|
|
171
|
+
destructure = this.arrayPattern();
|
|
172
|
+
} else {
|
|
173
|
+
nameNode = this.consume("IDENTIFIER", "Expected variable name");
|
|
174
|
+
|
|
175
|
+
// 🔥 NEW: Check for explicit type annotation (e.g. let x: int)
|
|
176
|
+
let explicitType = null;
|
|
177
|
+
if (this.match("PUNCTUATION", ":")) {
|
|
178
|
+
explicitType = this.consume("IDENTIFIER", "Expected type name after ':'").value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 🔥 Attach to nameNode
|
|
182
|
+
nameNode = { ...nameNode, typeAnnotation: explicitType };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let value = null;
|
|
186
|
+
if (this.match("OPERATOR", "=")) {
|
|
187
|
+
value = this.expression();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.match("PUNCTUATION", ";");
|
|
191
|
+
|
|
192
|
+
if (destructure) {
|
|
193
|
+
return this.nd({
|
|
194
|
+
kind: "declare",
|
|
195
|
+
destructure,
|
|
196
|
+
value,
|
|
197
|
+
isConst,
|
|
198
|
+
isPointer,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 🧩 Store declared variable type in the symbol table
|
|
203
|
+
if (nameNode.value) {
|
|
204
|
+
this.symbols.set(nameNode.value, {
|
|
205
|
+
type: nameNode.typeAnnotation || null,
|
|
206
|
+
isConst,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ✅ Parse-time type validation for explicit types
|
|
211
|
+
if (nameNode.typeAnnotation && value?.kind === "value") {
|
|
212
|
+
const val = value.value;
|
|
213
|
+
let inferredType;
|
|
214
|
+
|
|
215
|
+
if (typeof val === "number") {
|
|
216
|
+
inferredType = Number.isInteger(val) ? "int" : "float";
|
|
217
|
+
} else if (typeof val === "string") {
|
|
218
|
+
inferredType = "string";
|
|
219
|
+
} else if (typeof val === "boolean") {
|
|
220
|
+
inferredType = "bool";
|
|
221
|
+
} else if (typeof val === "object") {
|
|
222
|
+
inferredType = "obj";
|
|
223
|
+
} else {
|
|
224
|
+
inferredType = typeof val;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (inferredType !== nameNode.typeAnnotation) {
|
|
228
|
+
this.error(
|
|
229
|
+
`[Type mismatch] '${nameNode.value}' expected '${nameNode.typeAnnotation}', got '${inferredType}'`,
|
|
230
|
+
this.peek()
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return this.nd({
|
|
236
|
+
kind: "declare",
|
|
237
|
+
name: nameNode.value,
|
|
238
|
+
value,
|
|
239
|
+
isConst,
|
|
240
|
+
isPointer,
|
|
241
|
+
explicitType: nameNode.typeAnnotation || null, // ← new line
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
objectPattern() {
|
|
246
|
+
this.consume("PUNCTUATION", "{", "Expected '{' in destructuring");
|
|
247
|
+
const props = [];
|
|
248
|
+
if (!this.check("PUNCTUATION", "}")) {
|
|
249
|
+
do {
|
|
250
|
+
const key = this.consume("IDENTIFIER", "Expected property name").value;
|
|
251
|
+
let alias = key;
|
|
252
|
+
if (this.match("PUNCTUATION", ":")) {
|
|
253
|
+
alias = this.consume("IDENTIFIER", "Expected alias name").value;
|
|
254
|
+
}
|
|
255
|
+
props.push({ key, alias });
|
|
256
|
+
} while (this.match("PUNCTUATION", ","));
|
|
257
|
+
}
|
|
258
|
+
this.consume("PUNCTUATION", "}", "Expected '}'");
|
|
259
|
+
return { kind: "objpattern", props };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
arrayPattern(Consumed) {
|
|
263
|
+
if (!Consumed)
|
|
264
|
+
this.consume("PUNCTUATION", "[", "Expected '[' in destructuring");
|
|
265
|
+
const elements = [];
|
|
266
|
+
if (!this.check("PUNCTUATION", "]")) {
|
|
267
|
+
do {
|
|
268
|
+
const name = this.consume("IDENTIFIER", "Expected element name").value;
|
|
269
|
+
elements.push(name);
|
|
270
|
+
} while (this.match("PUNCTUATION", ","));
|
|
271
|
+
}
|
|
272
|
+
this.consume("PUNCTUATION", "]", "Expected ']'");
|
|
273
|
+
return this.nd({ kind: "arrpattern", elements });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
returnStatement(isGive) {
|
|
277
|
+
let value = null;
|
|
278
|
+
if (!this.check("PUNCTUATION", ";") && !this.check("EOF")) {
|
|
279
|
+
value = this.expression();
|
|
280
|
+
}
|
|
281
|
+
this.match("PUNCTUATION", ";");
|
|
282
|
+
return { kind: "return", value, terminate: isGive };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
funcDeclaration(isArrow) {
|
|
286
|
+
let isAsync = false;
|
|
287
|
+
if (this.match("KEYWORD", "async")) isAsync = true;
|
|
288
|
+
const name = this.consume("IDENTIFIER", "Expected function name");
|
|
289
|
+
const { args, body } = this.parseFuncBody(isArrow);
|
|
290
|
+
return { kind: "function", name: name.value, args, body, isAsync };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
classDeclaration() {
|
|
294
|
+
// Consume 'class' keyword (already matched in statement)
|
|
295
|
+
const name = this.consume("IDENTIFIER", "Expected class name");
|
|
296
|
+
|
|
297
|
+
// 🔥 NEW: Parse optional 'extends' clause
|
|
298
|
+
let superClass = null;
|
|
299
|
+
if (this.match("KEYWORD", "extends")) {
|
|
300
|
+
const superName = this.consume("IDENTIFIER", "Expected superclass name");
|
|
301
|
+
superClass = this.nd({ kind: "ref", name: superName.value }); // Store as a ref node
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Delegate parsing of the class body to objectDeclaration
|
|
305
|
+
const bodyObj = this.objectDeclaration(); // parses everything inside { ... }
|
|
306
|
+
|
|
307
|
+
// Convert objectDeclaration props into class members
|
|
308
|
+
const members = [];
|
|
309
|
+
for (const [k, v] of Object.entries(bodyObj.props)) {
|
|
310
|
+
members.push({
|
|
311
|
+
kind: v.kind === "function" ? "function" : "declare",
|
|
312
|
+
name: k,
|
|
313
|
+
value: v.kind !== "function" ? v : undefined,
|
|
314
|
+
args: v.kind === "function" ? v.args : undefined,
|
|
315
|
+
body: v.kind === "function" ? v.body : undefined,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 🔥 Return the AST node with the new superClass property
|
|
320
|
+
return { kind: "class", name: name.value, superClass, members };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
expressionStatement() {
|
|
324
|
+
let expr = this.expression();
|
|
325
|
+
|
|
326
|
+
// Handle assignment: a = b
|
|
327
|
+
if (expr.kind === "ref" && this.match("OPERATOR", "=")) {
|
|
328
|
+
const value = this.expression();
|
|
329
|
+
expr = { kind: "assign", name: expr.name, value };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.match("PUNCTUATION", ";");
|
|
333
|
+
return { kind: "exec", expr };
|
|
334
|
+
}
|
|
335
|
+
constExpression(minPrec) {
|
|
336
|
+
return this.expression(minPrec, true);
|
|
337
|
+
}
|
|
338
|
+
// ─────────────── Expressions ───────────────
|
|
339
|
+
expression(minPrec = 0, isConst = false) {
|
|
340
|
+
if (this.isAtEnd()) return { kind: "EOF" };
|
|
341
|
+
let tok = this.peek();
|
|
342
|
+
let left;
|
|
343
|
+
|
|
344
|
+
// ───── Unary / Primary (Handling 'await') ─────
|
|
345
|
+
if (this.check("KEYWORD", "await")) {
|
|
346
|
+
const awaitToken = this.advance(); // consume 'await'
|
|
347
|
+
const operand = this.expression(9); // 9 for very high precedence
|
|
348
|
+
left = this.nd({ kind: "await", operand });
|
|
349
|
+
} else if (tok.type === "KEYWORD" && tok.value === "const") {
|
|
350
|
+
this.advance();
|
|
351
|
+
const operand = this.constExpression(minPrec);
|
|
352
|
+
left = this.nd({ kind: "value", value: operand });
|
|
353
|
+
} else if (tok.type === "OPERATOR" && tok.isUnary) {
|
|
354
|
+
const op = this.advance().value;
|
|
355
|
+
const operand = this.expression(7);
|
|
356
|
+
if (op === "*") left = this.nd({ kind: "deref", operand });
|
|
357
|
+
else left = this.nd({ kind: "unary", operator: op, operand });
|
|
358
|
+
} else if (tok.type === "PUNCTUATION" && tok.value === "(") {
|
|
359
|
+
// might be grouped expression OR arrow func params
|
|
360
|
+
const startPos = this.current;
|
|
361
|
+
this.advance(); // consume '('
|
|
362
|
+
|
|
363
|
+
const args = [];
|
|
364
|
+
let isArrow = false;
|
|
365
|
+
|
|
366
|
+
if (!this.check("PUNCTUATION", ")")) {
|
|
367
|
+
do {
|
|
368
|
+
args.push(this.expression());
|
|
369
|
+
} while (this.match("PUNCTUATION", ","));
|
|
370
|
+
}
|
|
371
|
+
this.consume("PUNCTUATION", ")", "Expected ')' after parameters");
|
|
372
|
+
if (this.match("OPERATOR", "=>")) isArrow = true;
|
|
373
|
+
|
|
374
|
+
if (isArrow) {
|
|
375
|
+
let body;
|
|
376
|
+
const values = args;
|
|
377
|
+
if (this.check("PUNCTUATION", "{")) {
|
|
378
|
+
body = this.blockBody();
|
|
379
|
+
} else {
|
|
380
|
+
body = [
|
|
381
|
+
this.nd({
|
|
382
|
+
kind: "return",
|
|
383
|
+
value: this.expression(),
|
|
384
|
+
terminate: true,
|
|
385
|
+
}),
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
return this.nd({
|
|
389
|
+
kind: "arrowfunc",
|
|
390
|
+
args: values.map((a) => a?.name),
|
|
391
|
+
body,
|
|
392
|
+
});
|
|
393
|
+
} else {
|
|
394
|
+
// normal grouped expr
|
|
395
|
+
left = this.nd(args[0]);
|
|
396
|
+
}
|
|
397
|
+
} else if (this.match("PUNCTUATION", "[")) {
|
|
398
|
+
const elements = [];
|
|
399
|
+
if (!this.check("PUNCTUATION", "]")) {
|
|
400
|
+
do {
|
|
401
|
+
elements.push(this.expression());
|
|
402
|
+
} while (this.match("PUNCTUATION", ","));
|
|
403
|
+
}
|
|
404
|
+
this.consume("PUNCTUATION", "]", "Expected ']' after array literal");
|
|
405
|
+
left = this.nd({ kind: "array", elements });
|
|
406
|
+
} else if (tok.type === "PUNCTUATION" && tok.value === "{") {
|
|
407
|
+
left = this.objectDeclaration();
|
|
408
|
+
} else if (
|
|
409
|
+
tok.type === "NUMBER" ||
|
|
410
|
+
tok.type === "STRING" ||
|
|
411
|
+
tok.type === "LITERAL"
|
|
412
|
+
) {
|
|
413
|
+
left = this.nd({ kind: "value", value: this.advance().value });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
else if (tok.type === "TEMPLATE_START") {
|
|
417
|
+
left = this.templateLiteral();
|
|
418
|
+
} else if (tok.type === 'INTERPOLATION_END') {
|
|
419
|
+
return;
|
|
420
|
+
} else if (tok.type === "IDENTIFIER") {
|
|
421
|
+
// may be single-param arrow
|
|
422
|
+
const name = this.advance().value;
|
|
423
|
+
if (this.match("OPERATOR", "=>")) {
|
|
424
|
+
let body;
|
|
425
|
+
if (this.check("PUNCTUATION", "{")) {
|
|
426
|
+
body = this.blockBody();
|
|
427
|
+
} else {
|
|
428
|
+
body = [
|
|
429
|
+
this.nd({
|
|
430
|
+
kind: "return",
|
|
431
|
+
value: this.expression(),
|
|
432
|
+
terminate: true,
|
|
433
|
+
}),
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
return this.nd({ kind: "arrowfunc", args: [name], body });
|
|
437
|
+
}
|
|
438
|
+
const sym = this.symbols.get(name);
|
|
439
|
+
|
|
440
|
+
left = this.nd({
|
|
441
|
+
kind: "ref",
|
|
442
|
+
name,
|
|
443
|
+
explicitType: sym ? sym.type : null, // 🪄 carry type info
|
|
444
|
+
})
|
|
445
|
+
} else if (tok.type === "EOF") {
|
|
446
|
+
return this.nd({ kind: "EOF" });
|
|
447
|
+
} else {
|
|
448
|
+
this.error(`Unexpected token '${tok.value}'`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ───── Postfix / Calls / Property ─────
|
|
452
|
+
while (!this.isAtEnd()) {
|
|
453
|
+
tok = this.peek();
|
|
454
|
+
if (
|
|
455
|
+
tok.type === "PUNCTUATION" &&
|
|
456
|
+
(tok.value === ";" || tok.value === "}")
|
|
457
|
+
)
|
|
458
|
+
break;
|
|
459
|
+
|
|
460
|
+
if (tok.type === "OPERATOR" && tok.isPostfix) {
|
|
461
|
+
const op = this.advance().value;
|
|
462
|
+
left = this.nd({ kind: "postfix", operator: op, operand: left });
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (tok.type === "IDENTIFIER" || tok.type === "NUMBER") {
|
|
467
|
+
const op = this.advance().value;
|
|
468
|
+
left = this.nd({ kind: "currency", name: op, operand: left });
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (tok.type === "PUNCTUATION" && tok.value === "(") {
|
|
473
|
+
this.advance();
|
|
474
|
+
const args = [];
|
|
475
|
+
if (!this.check("PUNCTUATION", ")")) {
|
|
476
|
+
do {
|
|
477
|
+
args.push(this.expression());
|
|
478
|
+
} while (this.match("PUNCTUATION", ","));
|
|
479
|
+
}
|
|
480
|
+
this.consume("PUNCTUATION", ")", "Expected ')' after arguments");
|
|
481
|
+
if (left.kind === "ref") {
|
|
482
|
+
switch (left.name) {
|
|
483
|
+
case 'PRS_goto':
|
|
484
|
+
let exec = new (require("./executor.js").Executor)();
|
|
485
|
+
let value = exec.evaluate(args[0]);
|
|
486
|
+
this.current = value;
|
|
487
|
+
left = this.nd({ kind: "value", value });
|
|
488
|
+
case 'PRS_generate':
|
|
489
|
+
left = this.nd({ kind: "value", value: (new Parser((new Lexer(args[0]?.value)).tokenize())).parse() });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
left = this.nd({
|
|
494
|
+
kind: "call",
|
|
495
|
+
name: left,
|
|
496
|
+
args,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (tok.type === "PUNCTUATION" && tok.value === "{") {
|
|
503
|
+
this.advance();
|
|
504
|
+
let args = [];
|
|
505
|
+
if (!this.check("PUNCTUATION", "}")) {
|
|
506
|
+
do {
|
|
507
|
+
args.push(this.expression());
|
|
508
|
+
} while (this.match("PUNCTUATION", ","));
|
|
509
|
+
}
|
|
510
|
+
this.consume("PUNCTUATION", "}", "Expected '}' after construct opening brace");
|
|
511
|
+
left = this.nd({
|
|
512
|
+
kind: "construct",
|
|
513
|
+
left,
|
|
514
|
+
args,
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (tok.type === "PUNCTUATION" && tok.value === ".") {
|
|
520
|
+
this.advance();
|
|
521
|
+
const prop = this.consume("IDENTIFIER", "Expected property name");
|
|
522
|
+
left = this.nd({ kind: "prop", object: left, name: prop.value });
|
|
523
|
+
continue;
|
|
524
|
+
} else if (this.match("PUNCTUATION", "[")) {
|
|
525
|
+
const indexExpr = this.expression();
|
|
526
|
+
this.consume("PUNCTUATION", "]", "Expected ']' after subscript");
|
|
527
|
+
left = this.nd({ kind: "subscript", object: left, index: indexExpr });
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ───── Binary operators ─────
|
|
535
|
+
while (!this.isAtEnd()) {
|
|
536
|
+
tok = this.peek();
|
|
537
|
+
if (tok.type !== "OPERATOR" || tok.value === ";" || tok.value === "}")
|
|
538
|
+
break;
|
|
539
|
+
|
|
540
|
+
const prec = tok.precedence || PRECEDENCE[tok.value] || 0;
|
|
541
|
+
if (prec < minPrec) break;
|
|
542
|
+
|
|
543
|
+
const op = this.advance().value;
|
|
544
|
+
const right = this.expression(prec + 1);
|
|
545
|
+
|
|
546
|
+
if (op === "=") {
|
|
547
|
+
// 🔥 Parse-time type validation for assignments
|
|
548
|
+
if (left && left.explicitType && right?.kind === "value") {
|
|
549
|
+
const val = right.value;
|
|
550
|
+
let inferredType;
|
|
551
|
+
|
|
552
|
+
if (typeof val === "number") {
|
|
553
|
+
inferredType = Number.isInteger(val) ? "int" : "float";
|
|
554
|
+
} else if (typeof val === "string") {
|
|
555
|
+
inferredType = "string";
|
|
556
|
+
} else if (typeof val === "boolean") {
|
|
557
|
+
inferredType = "bool";
|
|
558
|
+
} else if (typeof val === "object") {
|
|
559
|
+
inferredType = "obj";
|
|
560
|
+
} else {
|
|
561
|
+
inferredType = typeof val;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const expected = left.explicitType;
|
|
565
|
+
const compatible =
|
|
566
|
+
inferredType === expected ||
|
|
567
|
+
(expected === "float" && inferredType === "int"); // implicit promotion allowed
|
|
568
|
+
|
|
569
|
+
if (!compatible) {
|
|
570
|
+
this.error(
|
|
571
|
+
`[Type mismatch] '${left.name || left.value}' expected '${expected}', got '${inferredType}'`,
|
|
572
|
+
this.peek()
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
left = this.nd({
|
|
578
|
+
kind: "assign",
|
|
579
|
+
name: left,
|
|
580
|
+
value: right,
|
|
581
|
+
});
|
|
582
|
+
} else {
|
|
583
|
+
left = this.nd({ kind: "binary", operator: op, left, right });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (isConst) {
|
|
588
|
+
let exec = new (require("./executor.js").Executor)();
|
|
589
|
+
return exec.evaluate(left);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return left;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ─────────────── Helpers ───────────────
|
|
596
|
+
parseFuncBody(isArrow) {
|
|
597
|
+
this.consume("PUNCTUATION", "(", "Expected '('");
|
|
598
|
+
const args = [];
|
|
599
|
+
if (!this.check("PUNCTUATION", ")")) {
|
|
600
|
+
do {
|
|
601
|
+
args.push(this.consume("IDENTIFIER", "Expected argument name").value);
|
|
602
|
+
} while (this.match("PUNCTUATION", ","));
|
|
603
|
+
}
|
|
604
|
+
this.consume("PUNCTUATION", ")", "Expected ')'");
|
|
605
|
+
if (isArrow) this.consume("OPERATOR", "=>", "Expected '=>");
|
|
606
|
+
this.consume("PUNCTUATION", "{", "Expected '{'");
|
|
607
|
+
const body = [];
|
|
608
|
+
while (!this.check("PUNCTUATION", "}") && !this.isAtEnd())
|
|
609
|
+
body.push(this.statement());
|
|
610
|
+
this.consume("PUNCTUATION", "}", "Expected '}'");
|
|
611
|
+
return { args, body };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
parseParenBody(branchName, SEP = ",") {
|
|
615
|
+
// Parse the arguments / condition
|
|
616
|
+
this.consume("PUNCTUATION", "(", "Expected '('");
|
|
617
|
+
const args = [];
|
|
618
|
+
if (branchName === "for") {
|
|
619
|
+
args.push(this.statement());
|
|
620
|
+
this.match("PUNCTUATION", ";");
|
|
621
|
+
args.push(this.expression());
|
|
622
|
+
this.match("PUNCTUATION", ";");
|
|
623
|
+
args.push(this.statement());
|
|
624
|
+
} else {
|
|
625
|
+
if (!this.check("PUNCTUATION", ")")) {
|
|
626
|
+
do {
|
|
627
|
+
args.push(this.expression());
|
|
628
|
+
} while (this.match("PUNCTUATION", SEP));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
this.consume("PUNCTUATION", ")", "Expected ')'");
|
|
632
|
+
|
|
633
|
+
// Automatically handle block or single statement for body
|
|
634
|
+
let body;
|
|
635
|
+
if (this.check("PUNCTUATION", "{")) {
|
|
636
|
+
body = this.blockBody(); // grabs all statements inside { … }
|
|
637
|
+
} else {
|
|
638
|
+
body = [this.statement()]; // single statement
|
|
639
|
+
}
|
|
640
|
+
this.match("PUNCTUATION", ";");
|
|
641
|
+
// For `if`, only take the first argument as condition
|
|
642
|
+
return { args: branchName === "if" ? args[0] : args, body };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
branchBody(name = "if", SEP = ",") {
|
|
646
|
+
const { args, body } = this.parseParenBody(name, SEP);
|
|
647
|
+
const branchNode = this.nd({ kind: "branch", type: name, args, body });
|
|
648
|
+
|
|
649
|
+
// check if the next statement is 'else' and attach it as branchNode.next
|
|
650
|
+
if (this.match("KEYWORD", "else")) {
|
|
651
|
+
let elseNode;
|
|
652
|
+
// plain else
|
|
653
|
+
const elseBody = [this.statement()];
|
|
654
|
+
elseNode = this.nd({
|
|
655
|
+
kind: "branch",
|
|
656
|
+
type: "else",
|
|
657
|
+
args: null,
|
|
658
|
+
body: elseBody,
|
|
659
|
+
});
|
|
660
|
+
branchNode.next = elseNode; // attach chain
|
|
661
|
+
}
|
|
662
|
+
this.match("PUNCTUATION", ";");
|
|
663
|
+
return branchNode;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
blockBody(consumed) {
|
|
667
|
+
if (!consumed) this.consume("PUNCTUATION", "{", "Expected '{'");
|
|
668
|
+
const body = [];
|
|
669
|
+
while (!this.check("PUNCTUATION", "}") && !this.isAtEnd()) {
|
|
670
|
+
body.push(this.statement());
|
|
671
|
+
}
|
|
672
|
+
this.consume("PUNCTUATION", "}", "Expected '}'");
|
|
673
|
+
return body;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
objectDeclaration() {
|
|
677
|
+
const props = {};
|
|
678
|
+
this.consume("PUNCTUATION", "{", "Expected '{' to start object literal");
|
|
679
|
+
|
|
680
|
+
while (!this.check("PUNCTUATION", "}") && !this.isAtEnd()) {
|
|
681
|
+
const tok = this.peek();
|
|
682
|
+
|
|
683
|
+
// Nested object literal
|
|
684
|
+
if (tok.type === "PUNCTUATION" && tok.value === "{") {
|
|
685
|
+
const nested = this.objectDeclaration();
|
|
686
|
+
Object.assign(props, nested.props);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Property key
|
|
691
|
+
const nameTok = this.consume("IDENTIFIER", "Expected property name");
|
|
692
|
+
const propName = nameTok.value;
|
|
693
|
+
let value;
|
|
694
|
+
|
|
695
|
+
// Function value
|
|
696
|
+
if (this.check("PUNCTUATION", "(")) {
|
|
697
|
+
value = this.parseFuncBody();
|
|
698
|
+
} else {
|
|
699
|
+
this.consume("OPERATOR", ":", "Expected ':' after property name");
|
|
700
|
+
value = this.expression();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
this.match("PUNCTUATION", ","); // optional
|
|
704
|
+
props[propName] = value;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
this.consume("PUNCTUATION", "}", "Expected '}' to close object literal");
|
|
708
|
+
return this.nd({ kind: "object", props });
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ─────────────── Token helpers ───────────────
|
|
712
|
+
peek() {
|
|
713
|
+
return this.tokens[this.current];
|
|
714
|
+
}
|
|
715
|
+
previous() {
|
|
716
|
+
return this.tokens[this.current - 1];
|
|
717
|
+
}
|
|
718
|
+
next() {
|
|
719
|
+
return this.tokens[this.current + 1];
|
|
720
|
+
}
|
|
721
|
+
isAtEnd() {
|
|
722
|
+
return this.peek().type === "EOF";
|
|
723
|
+
}
|
|
724
|
+
advance() {
|
|
725
|
+
if (!this.isAtEnd()) this.current++;
|
|
726
|
+
return this.previous();
|
|
727
|
+
}
|
|
728
|
+
check(type, value = null) {
|
|
729
|
+
if (this.isAtEnd()) return false;
|
|
730
|
+
const t = this.peek();
|
|
731
|
+
if (t.type !== type) return false;
|
|
732
|
+
if (value !== null && t.value !== value) return false;
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
match(type, value = null) {
|
|
736
|
+
if (this.check(type, value)) {
|
|
737
|
+
this.advance();
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
consume(type, valueOrMsg, msgIfVal) {
|
|
743
|
+
const hasValue = typeof valueOrMsg === "string" && msgIfVal;
|
|
744
|
+
const expectedValue = hasValue ? valueOrMsg : null;
|
|
745
|
+
const msg = hasValue ? msgIfVal : valueOrMsg;
|
|
746
|
+
if (this.check(type, expectedValue)) return this.advance();
|
|
747
|
+
this.error(msg);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
error(msg) {
|
|
751
|
+
const t = this.peek();
|
|
752
|
+
const lineText = this.rawsrc.split("\n")[t.line - 1] ?? "";
|
|
753
|
+
throw new new CustomError("ParseError")(
|
|
754
|
+
`[NovaParser ${t.line}:${t.column}] ${msg}\n` +
|
|
755
|
+
` error at:\n` +
|
|
756
|
+
` ${lineText}\n` +
|
|
757
|
+
` ${" ".repeat(Math.max(t.column - 1, 0))}^^`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
module.exports = { Parser };
|