@dseict/psc-interpreter 0.0.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,765 @@
1
+ import PSCParser from "./_antlr/PSCParser";
2
+ import PSCParserVisitor from "./_antlr/PSCParserVisitor";
3
+ import { PSCAccessNonExistingVariableError, PSCArrayAccessNotArrayError, PSCConditionNotBooleanError, PSCForRangeNotIntegerError, PSCForVariableReuseError, PSCImpossibleError, PSCInvalidArrayIndexError, PSCOperationValueTypeMismatchError, PSCUnmatchedArgumentsError, } from "./error";
4
+ import { PSCEventBus, } from "./events";
5
+ export class PSCInterpretVisitor extends PSCParserVisitor {
6
+ #options;
7
+ // Stack of variable scopes
8
+ #variableStack = [{}];
9
+ // Current return value, undefined means currently not in returning state
10
+ #currentReturn = undefined;
11
+ // Event bus
12
+ #eventBus = new PSCEventBus();
13
+ constructor(options) {
14
+ super();
15
+ this.#options = options;
16
+ }
17
+ // Returns a function that can be called to unregister the event handler
18
+ on(eventType, handler) {
19
+ return this.#eventBus.on(eventType, handler);
20
+ }
21
+ #stringSmartCast(value) {
22
+ if (typeof value === "string") {
23
+ // Try to cast to boolean
24
+ if (value.toLowerCase() === "true") {
25
+ return true;
26
+ }
27
+ if (value.toLowerCase() === "false") {
28
+ return false;
29
+ }
30
+ if (value.toLowerCase() === "null") {
31
+ return null;
32
+ }
33
+ // Handle ridiculous edge cases for JS built-in casting v_v
34
+ if (value.trim() === "" || !/^-?\d*(\.\d+)?$/.test(value)) {
35
+ return value; // Return as is
36
+ }
37
+ // Try to cast to number
38
+ const numValue = Number(value);
39
+ if (!isNaN(numValue)) {
40
+ return numValue;
41
+ }
42
+ }
43
+ return value; // Return as is if no casting is possible
44
+ }
45
+ #assignVariable(_ctx, name, value) {
46
+ // Found if the variable is declared, and reassign it in the correct scope
47
+ for (const stack of this.#variableStack.slice().reverse()) {
48
+ if (stack[name] !== undefined) {
49
+ stack[name] = value;
50
+ return;
51
+ }
52
+ }
53
+ // If variable not found declared, assign it to the current scope
54
+ this.#variableStack[this.#variableStack.length - 1][name] = value;
55
+ }
56
+ #readVariable(ctx, name) {
57
+ for (const stack of this.#variableStack.slice().reverse()) {
58
+ if (stack[name] !== undefined) {
59
+ return stack[name];
60
+ }
61
+ }
62
+ throw new PSCAccessNonExistingVariableError(ctx, name);
63
+ }
64
+ #deleteVariable(ctx, name) {
65
+ for (const stack of this.#variableStack.slice().reverse()) {
66
+ if (stack[name] !== undefined) {
67
+ delete stack[name];
68
+ return;
69
+ }
70
+ }
71
+ throw new PSCImpossibleError(ctx, `Cannot delete variable '${name}' because it does not exist.`);
72
+ }
73
+ #variableExists(name) {
74
+ for (const stack of this.#variableStack.slice().reverse()) {
75
+ if (stack[name] !== undefined) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+ #newVariableStack(_ctx) {
82
+ this.#variableStack.push({});
83
+ }
84
+ #popVariableStack(ctx) {
85
+ if (this.#variableStack.length === 1) {
86
+ throw new PSCImpossibleError(ctx, "Cannot pop the global variable stack.");
87
+ }
88
+ this.#variableStack.pop();
89
+ }
90
+ #initiateReturn(value) {
91
+ this.#currentReturn = value;
92
+ }
93
+ #isReturning() {
94
+ return this.#currentReturn !== undefined;
95
+ }
96
+ #terminateReturn() {
97
+ const ret = this.#currentReturn;
98
+ this.#currentReturn = undefined;
99
+ return ret;
100
+ }
101
+ #asString(ctx, value) {
102
+ if (Array.isArray(value)) {
103
+ return `[${value.map((v) => this.#asString(ctx, v)).join(",")}]`;
104
+ }
105
+ else if (typeof value === "boolean") {
106
+ return value ? "true" : "false";
107
+ }
108
+ else if (typeof value === "number") {
109
+ return value.toString();
110
+ }
111
+ else if (typeof value === "string") {
112
+ return `"` + value + `"`; // Wrap string in quotes
113
+ }
114
+ else if (value === undefined) {
115
+ return "";
116
+ }
117
+ else if (value === null) {
118
+ return "null";
119
+ }
120
+ else if (value instanceof Function) {
121
+ return "[Function]";
122
+ }
123
+ throw new PSCImpossibleError(ctx, `Cannot convert value of type ${typeof value} to string.`);
124
+ }
125
+ #normalizeArrayIndexOrThrow(ctx, index) {
126
+ if (typeof index !== "number" ||
127
+ !Number.isInteger(index - this.#options.arrayStartIndex) ||
128
+ index - this.#options.arrayStartIndex < 0) {
129
+ throw new PSCInvalidArrayIndexError(ctx, this.#asString(ctx, index));
130
+ }
131
+ return index - this.#options.arrayStartIndex;
132
+ }
133
+ visitProgram = async (ctx) => {
134
+ for (const subprogramCtx of ctx.subprogram_list()) {
135
+ await this.visitSubprogram(subprogramCtx);
136
+ }
137
+ await this.visitStmts(ctx.stmts());
138
+ };
139
+ // Expression and literals
140
+ visitExpr = async (ctx) => {
141
+ const eventParams = {
142
+ startLine: ctx.start.line,
143
+ startCol: ctx.start.column,
144
+ endLine: ctx.stop?.line,
145
+ endCol: ctx.stop !== undefined
146
+ ? ctx.stop.column + ctx.stop.stop - ctx.stop.start
147
+ : undefined,
148
+ };
149
+ this.#eventBus.emit("pre_eval_expr", eventParams);
150
+ const result = await this.visitOrExpr(ctx.orExpr());
151
+ this.#eventBus.emit("post_eval_expr", { ...eventParams, result });
152
+ return result;
153
+ };
154
+ visitOrExpr = async (ctx) => {
155
+ let result = this.#stringSmartCast(await this.visitAndExpr(ctx.andExpr(0)));
156
+ for (let i = 1; i < ctx.andExpr_list().length; i++) {
157
+ const right = this.#stringSmartCast(await this.visitAndExpr(ctx.andExpr(i)));
158
+ const resultBool = typeof result == "boolean";
159
+ const rightBool = typeof right == "boolean";
160
+ if (!resultBool || !rightBool) {
161
+ throw new PSCOperationValueTypeMismatchError(ctx, "OR", "boolean", this.#asString(ctx, !resultBool ? result : right));
162
+ }
163
+ result = result || right;
164
+ }
165
+ return result;
166
+ };
167
+ visitAndExpr = async (ctx) => {
168
+ let result = this.#stringSmartCast(await this.visitCompExpr(ctx.compExpr(0)));
169
+ for (let i = 1; i < ctx.compExpr_list().length; i++) {
170
+ const right = this.#stringSmartCast(await this.visitCompExpr(ctx.compExpr(i)));
171
+ const resultBool = typeof result == "boolean";
172
+ const rightBool = typeof right == "boolean";
173
+ if (!resultBool || !rightBool) {
174
+ throw new PSCOperationValueTypeMismatchError(ctx, "AND", "boolean", this.#asString(ctx, !resultBool ? result : right));
175
+ }
176
+ result = result && right;
177
+ }
178
+ return result;
179
+ };
180
+ visitCompExpr = async (ctx) => {
181
+ let result = this.#stringSmartCast(await this.visitAddExpr(ctx.addExpr(0)));
182
+ for (let i = 1; i < ctx.addExpr_list().length; i++) {
183
+ const right = this.#stringSmartCast(await this.visitAddExpr(ctx.addExpr(i)));
184
+ const operator = ctx.compOp(i - 1).getText();
185
+ switch (operator) {
186
+ case "=":
187
+ result = result === right;
188
+ break;
189
+ case "<>":
190
+ result = result !== right;
191
+ break;
192
+ case ">":
193
+ if ((typeof result == "string" && typeof right == "string") ||
194
+ (typeof result == "number" && typeof right == "number")) {
195
+ result = result > right;
196
+ }
197
+ else {
198
+ throw new PSCOperationValueTypeMismatchError(ctx, ">", "string or number", `${result} > ${right}`);
199
+ }
200
+ break;
201
+ case "<":
202
+ if ((typeof result == "string" && typeof right == "string") ||
203
+ (typeof result == "number" && typeof right == "number")) {
204
+ result = result < right;
205
+ }
206
+ else {
207
+ throw new PSCOperationValueTypeMismatchError(ctx, "<", "string or number", `${result} < ${right}`);
208
+ }
209
+ break;
210
+ case ">=":
211
+ if ((typeof result == "string" && typeof right == "string") ||
212
+ (typeof result == "number" && typeof right == "number")) {
213
+ result = result >= right;
214
+ }
215
+ else {
216
+ throw new PSCOperationValueTypeMismatchError(ctx, ">=", "string or number", `${result} >= ${right}`);
217
+ }
218
+ break;
219
+ case "<=":
220
+ if ((typeof result == "string" && typeof right == "string") ||
221
+ (typeof result == "number" && typeof right == "number")) {
222
+ result = result <= right;
223
+ }
224
+ else {
225
+ throw new PSCOperationValueTypeMismatchError(ctx, "<=", "string or number", `${result} <= ${right}`);
226
+ }
227
+ break;
228
+ default:
229
+ throw new PSCImpossibleError(ctx, `Unknown comparison operator: ${operator}`);
230
+ }
231
+ }
232
+ return result;
233
+ };
234
+ visitAddExpr = async (ctx) => {
235
+ let result = this.#stringSmartCast(await this.visitMulExpr(ctx.mulExpr(0)));
236
+ for (let i = 1; i < ctx.mulExpr_list().length; i++) {
237
+ const right = this.#stringSmartCast(await this.visitMulExpr(ctx.mulExpr(i)));
238
+ if (typeof result !== "number" || typeof right !== "number") {
239
+ throw new PSCOperationValueTypeMismatchError(ctx, "Addition or subtraction", "number", `${this.#asString(ctx, result)} and ${this.#asString(ctx, right)}`);
240
+ }
241
+ const operator = ctx.addOp(i - 1).getText();
242
+ switch (operator) {
243
+ case "+":
244
+ result += right;
245
+ break;
246
+ case "-":
247
+ result -= right;
248
+ break;
249
+ default:
250
+ throw new PSCImpossibleError(ctx, `Unknown addition/subtraction operator: ${operator}`);
251
+ }
252
+ }
253
+ return result;
254
+ };
255
+ visitMulExpr = async (ctx) => {
256
+ let result = this.#stringSmartCast(await this.visitExpExpr(ctx.expExpr(0)));
257
+ for (let i = 1; i < ctx.expExpr_list().length; i++) {
258
+ const right = this.#stringSmartCast(await this.visitExpExpr(ctx.expExpr(i)));
259
+ if (typeof result !== "number" || typeof right !== "number") {
260
+ throw new PSCOperationValueTypeMismatchError(ctx, "Multiplication/division/modulo", "number", `${this.#asString(ctx, result)} and ${this.#asString(ctx, right)}`);
261
+ }
262
+ const operator = ctx.mulOp(i - 1).getText();
263
+ switch (operator) {
264
+ case "*":
265
+ result *= right;
266
+ break;
267
+ case "/":
268
+ result /= right;
269
+ break;
270
+ case "mod":
271
+ case "%":
272
+ result %= right;
273
+ break;
274
+ default:
275
+ throw new PSCImpossibleError(ctx, `Unknown multiplication operator: ${operator}`);
276
+ }
277
+ }
278
+ return result;
279
+ };
280
+ visitExpExpr = async (ctx) => {
281
+ let result = await this.visitUnaryExpr(ctx.unaryExpr(0));
282
+ for (let i = 1; i < ctx.unaryExpr_list().length; i++) {
283
+ const right = this.#stringSmartCast(await this.visitUnaryExpr(ctx.unaryExpr(i)));
284
+ if (typeof result !== "number" || typeof right !== "number") {
285
+ throw new PSCOperationValueTypeMismatchError(ctx, "Exponential", "number", `${this.#asString(ctx, result)} and ${this.#asString(ctx, right)}`);
286
+ }
287
+ const operator = ctx.expOp(i - 1).getText();
288
+ switch (operator) {
289
+ case "**":
290
+ case "^":
291
+ result **= right;
292
+ break;
293
+ default:
294
+ throw new PSCImpossibleError(ctx, `Unknown exponentiation operator: ${operator}`);
295
+ }
296
+ }
297
+ return result;
298
+ };
299
+ visitUnaryExpr = async (ctx) => {
300
+ const minusCount = ctx.MINUS_list().length;
301
+ const plusCount = ctx.PLUS_list().length;
302
+ const value = this.#stringSmartCast(await this.visitNotExpr(ctx.notExpr()));
303
+ if (typeof value !== "number" && (minusCount > 0 || plusCount > 0)) {
304
+ throw new PSCOperationValueTypeMismatchError(ctx, "Unary negation/affirmation operator", "number", this.#asString(ctx, value));
305
+ }
306
+ if (minusCount % 2 === 0) {
307
+ return value;
308
+ }
309
+ else {
310
+ if (typeof value !== "number") {
311
+ // This should never happen, just for type safety
312
+ throw new PSCOperationValueTypeMismatchError(ctx, "Negative sign operator", "number", this.#asString(ctx, value));
313
+ }
314
+ else {
315
+ return -value;
316
+ }
317
+ }
318
+ };
319
+ visitNotExpr = async (ctx) => {
320
+ const notCount = ctx.NOT_list().length;
321
+ const value = this.#stringSmartCast(await this.visitPrimaryExpr(ctx.primaryExpr()));
322
+ if (typeof value !== "boolean" && notCount > 0) {
323
+ throw new PSCOperationValueTypeMismatchError(ctx, "NOT", "boolean", this.#asString(ctx, value));
324
+ }
325
+ if (notCount % 2 === 0) {
326
+ return value;
327
+ }
328
+ else {
329
+ if (typeof value !== "boolean") {
330
+ // This should never happen, just for type safety
331
+ throw new PSCOperationValueTypeMismatchError(ctx, "NOT", "boolean", this.#asString(ctx, value));
332
+ }
333
+ return !value;
334
+ }
335
+ };
336
+ visitPrimaryExpr = async (ctx) => {
337
+ if (ctx.LPAREN() && ctx.RPAREN()) {
338
+ const args = await Promise.all(ctx.expr_list().map((expr) => this.visitExpr(expr)));
339
+ const func = await this.visitPrimaryExpr(ctx.primaryExpr());
340
+ if (typeof func !== "function") {
341
+ throw new PSCOperationValueTypeMismatchError(ctx, "Function call", "function", this.#asString(ctx, func));
342
+ }
343
+ // Function must return a value
344
+ return await func(args);
345
+ }
346
+ else if (ctx.LSQUARE() && ctx.RSQUARE()) {
347
+ const indices = await Promise.all(ctx.expr_list().map((expr) => this.visitExpr(expr)));
348
+ const leftArr = await this.visitPrimaryExpr(ctx.primaryExpr());
349
+ return indices.reduce((arr, index) => {
350
+ if (!Array.isArray(arr)) {
351
+ throw new PSCArrayAccessNotArrayError(ctx, this.#asString(ctx, arr));
352
+ }
353
+ const normalizedIndex = this.#normalizeArrayIndexOrThrow(ctx, index);
354
+ if (normalizedIndex > arr.length - 1) {
355
+ throw new PSCInvalidArrayIndexError(ctx, this.#asString(ctx, index));
356
+ }
357
+ return arr[normalizedIndex];
358
+ }, leftArr);
359
+ }
360
+ else if (ctx.groupExpr()) {
361
+ return await this.visitGroupExpr(ctx.groupExpr());
362
+ }
363
+ throw new PSCImpossibleError(ctx, "Invalid primary");
364
+ };
365
+ visitGroupExpr = async (ctx) => {
366
+ if (ctx.expr()) {
367
+ return await this.visitExpr(ctx.expr());
368
+ }
369
+ else if (ctx.atom()) {
370
+ return await this.visitAtom(ctx.atom());
371
+ }
372
+ throw new PSCImpossibleError(ctx, "Invalid group expression");
373
+ };
374
+ visitAtom = async (ctx) => {
375
+ if (ctx.lits()) {
376
+ return await this.visitLits(ctx.lits());
377
+ }
378
+ else if (ctx.ID()) {
379
+ // Handle variable lookup here
380
+ return this.#readVariable(ctx, ctx.ID().getText());
381
+ }
382
+ throw new PSCImpossibleError(ctx, "Invalid atom");
383
+ };
384
+ visitLits = async (ctx) => {
385
+ if (ctx.floatLits()) {
386
+ return await this.visitFloatLits(ctx.floatLits());
387
+ }
388
+ else if (ctx.intLits()) {
389
+ return await this.visitIntLits(ctx.intLits());
390
+ }
391
+ else if (ctx.arrayLits()) {
392
+ return await this.visitArrayLits(ctx.arrayLits());
393
+ }
394
+ else if (ctx.STRING()) {
395
+ return this.#stringSmartCast(ctx.STRING().getText().slice(1, -1)); // Remove quotes
396
+ }
397
+ else if (ctx.BOOLEAN()) {
398
+ return ctx.BOOLEAN().getText().toLowerCase() === "true";
399
+ }
400
+ else if (ctx.NULL()) {
401
+ return null;
402
+ }
403
+ throw new PSCImpossibleError(ctx, "Invalid literal");
404
+ };
405
+ visitIntLits = async (ctx) => {
406
+ const sign = ctx.MINUS() !== null ? -1 : 1;
407
+ return parseInt(ctx.INTEGER().getText(), 10) * sign;
408
+ };
409
+ visitFloatLits = async (ctx) => {
410
+ const sign = ctx.MINUS() !== null ? -1 : 1;
411
+ return parseFloat(ctx.FLOAT().getText()) * sign;
412
+ };
413
+ visitArrayLits = async (ctx) => {
414
+ const elements = [];
415
+ if (ctx.expr_list()) {
416
+ for (const expr of ctx.expr_list()) {
417
+ elements.push(await this.visitExpr(expr));
418
+ }
419
+ }
420
+ return elements;
421
+ };
422
+ visitStmts = async (ctx) => {
423
+ for (const stmt of ctx.stmt_list()) {
424
+ await this.visitStmt(stmt);
425
+ if (this.#isReturning()) {
426
+ // If a return statement was executed, stop executing further statements
427
+ break;
428
+ }
429
+ }
430
+ };
431
+ visitStmt = async (ctx) => {
432
+ if (ctx.children == null ||
433
+ ctx.children.length != 1 ||
434
+ ctx.children[0] == null)
435
+ throw new PSCImpossibleError(ctx, "Statement must have exactly one child");
436
+ const child = ctx.children[0];
437
+ let ruleIndex;
438
+ try {
439
+ ruleIndex = child.ruleIndex;
440
+ }
441
+ catch (e) {
442
+ throw new PSCImpossibleError(ctx, `Failed to get ruleIndex from child: ${e}`);
443
+ }
444
+ const ruleName = PSCParser.ruleNames[ruleIndex];
445
+ if (!ruleName) {
446
+ throw new PSCImpossibleError(ctx, `Undefined rule name. ruleIndex ${ruleIndex} constructor.name ${child.constructor.name}`);
447
+ }
448
+ const eventParams = {
449
+ startLine: child.start.line,
450
+ startCol: child.start.column,
451
+ endLine: child.stop?.line,
452
+ endCol: child.stop !== undefined
453
+ ? child.stop.column + child.stop.stop - child.stop.start
454
+ : undefined,
455
+ stmtType: ruleName,
456
+ };
457
+ this.#eventBus.emit("pre_exec_stmt", eventParams);
458
+ // Dispatch appropriate visit method
459
+ await this.visit(ctx.children[0]);
460
+ this.#eventBus.emit("post_exec_stmt", eventParams);
461
+ };
462
+ visitBlock = async (ctx) => {
463
+ if (this.#options.strictVariableScope) {
464
+ this.#newVariableStack(ctx);
465
+ }
466
+ await this.visitStmts(ctx.stmts());
467
+ if (this.#options.strictVariableScope) {
468
+ this.#popVariableStack(ctx);
469
+ }
470
+ };
471
+ visitIfStmt = async (ctx) => {
472
+ // Validate statement structure
473
+ if (ctx.expr() === null) {
474
+ throw new PSCImpossibleError(ctx, "If statement must have a condition expression.");
475
+ }
476
+ if (ctx.block(0) === null) {
477
+ throw new PSCImpossibleError(ctx, "If statement must have a 'then' block.");
478
+ }
479
+ if (ctx.ELSE() && ctx.block(1) === null && ctx.ifStmt() === null) {
480
+ throw new PSCImpossibleError(ctx, "If statement with 'else' must have an 'else' block or an 'else if' statement.");
481
+ }
482
+ const condition = ctx.expr();
483
+ if (condition) {
484
+ // Evaluate the condition
485
+ const result = await this.visitExpr(condition);
486
+ // Ensure the result is evaluated to a boolean value
487
+ if (typeof result !== "boolean") {
488
+ throw new PSCConditionNotBooleanError(ctx.expr(), this.#asString(ctx, result));
489
+ }
490
+ if (result) {
491
+ // Execute the 'then' block
492
+ await this.visitBlock(ctx.block(0));
493
+ }
494
+ else if (ctx.ELSE()) {
495
+ if (ctx.block_list().length == 2) {
496
+ // Execute the block after else
497
+ // Execute the 'else' block if it exists
498
+ await this.visitBlock(ctx.block(1));
499
+ }
500
+ else if (ctx.ifStmt()) {
501
+ // Execute the ifStmt after else
502
+ await this.visitIfStmt(ctx.ifStmt());
503
+ }
504
+ }
505
+ }
506
+ };
507
+ visitWhileStmt = async (ctx) => {
508
+ // Validate statement structure
509
+ if (ctx.expr() === null) {
510
+ throw new PSCImpossibleError(ctx, "While statement must have a condition expression.");
511
+ }
512
+ if (ctx.block() === null) {
513
+ throw new PSCImpossibleError(ctx, "While statement must have a block to execute.");
514
+ }
515
+ const condition = ctx.expr();
516
+ while (true) {
517
+ const eventParams = {
518
+ startLine: ctx.WHILE().symbol.line,
519
+ startCol: ctx.WHILE().symbol.column,
520
+ endLine: condition.stop?.line,
521
+ endCol: condition.stop !== undefined
522
+ ? condition.stop.column + condition.stop.stop - condition.stop.start
523
+ : undefined,
524
+ };
525
+ this.#eventBus.emit("pre_while_condition", eventParams);
526
+ const result = await this.visitExpr(condition);
527
+ // Ensure the result is evaluated to a boolean value
528
+ if (typeof result !== "boolean") {
529
+ throw new PSCConditionNotBooleanError(ctx.expr(), `Condition must evaluate to a boolean value, got: ${result}`);
530
+ }
531
+ this.#eventBus.emit("post_while_condition", {
532
+ ...eventParams,
533
+ shouldContinue: result,
534
+ });
535
+ if (!result) {
536
+ break;
537
+ }
538
+ await this.visitBlock(ctx.block());
539
+ }
540
+ };
541
+ visitDoWhileStmt = async (ctx) => {
542
+ if (ctx.expr() === null) {
543
+ throw new PSCImpossibleError(ctx, "Do-While statement must have a condition expression.");
544
+ }
545
+ if (ctx.block() === null) {
546
+ throw new PSCImpossibleError(ctx, "Do-While statement must have a block to execute.");
547
+ }
548
+ const condition = ctx.expr();
549
+ let result;
550
+ do {
551
+ await this.visitBlock(ctx.block());
552
+ const eventParams = {
553
+ startLine: ctx.WHILE().symbol.line,
554
+ startCol: ctx.WHILE().symbol.column,
555
+ endLine: condition.stop?.line,
556
+ endCol: condition.stop !== undefined
557
+ ? condition.stop.column + condition.stop.stop - condition.stop.start
558
+ : undefined,
559
+ };
560
+ this.#eventBus.emit("pre_do_while_condition", eventParams);
561
+ result = await this.visitExpr(condition);
562
+ // Ensure the result is evaluated to a boolean value
563
+ if (typeof result !== "boolean") {
564
+ throw new PSCConditionNotBooleanError(ctx.expr(), `Condition must evaluate to a boolean value, got: ${result}`);
565
+ }
566
+ this.#eventBus.emit("post_do_while_condition", {
567
+ ...eventParams,
568
+ shouldContinue: result,
569
+ });
570
+ } while (result);
571
+ };
572
+ visitRepeatUntilStmt = async (ctx) => {
573
+ if (ctx.expr() === null) {
574
+ throw new PSCImpossibleError(ctx, "Do-While statement must have a condition expression.");
575
+ }
576
+ if (ctx.block() === null) {
577
+ throw new PSCImpossibleError(ctx, "Do-While statement must have a block to execute.");
578
+ }
579
+ const condition = ctx.expr();
580
+ let result;
581
+ do {
582
+ await this.visitBlock(ctx.block());
583
+ const eventParams = {
584
+ startLine: ctx.UNTIL().symbol.line,
585
+ startCol: ctx.UNTIL().symbol.column,
586
+ endLine: condition.stop?.line,
587
+ endCol: condition.stop !== undefined
588
+ ? condition.stop.column + condition.stop.stop - condition.stop.start
589
+ : undefined,
590
+ };
591
+ this.#eventBus.emit("pre_repeat_until_condition", eventParams);
592
+ result = await this.visitExpr(condition);
593
+ // Ensure the result is evaluated to a boolean value
594
+ if (typeof result !== "boolean") {
595
+ throw new PSCConditionNotBooleanError(ctx.expr(), `Condition must evaluate to a boolean value, got: ${result}`);
596
+ }
597
+ this.#eventBus.emit("post_repeat_until_condition", {
598
+ ...eventParams,
599
+ shouldContinue: !result,
600
+ });
601
+ } while (!result);
602
+ };
603
+ visitForStmt = async (ctx) => {
604
+ if (ctx.ID() === null) {
605
+ throw new PSCImpossibleError(ctx, "For statement must have a loop variable.");
606
+ }
607
+ if (ctx.expr(0) === null || ctx.expr(1) === null) {
608
+ throw new PSCImpossibleError(ctx, "For statement must have both 'from' and 'to' expressions.");
609
+ }
610
+ if (ctx.block() === null) {
611
+ throw new PSCImpossibleError(ctx, "For statement must have a block to execute.");
612
+ }
613
+ const loopVar = ctx.ID().getText();
614
+ const fromExpr = ctx.expr(0);
615
+ const toExpr = ctx.expr(1);
616
+ const fromValue = await this.visitExpr(fromExpr);
617
+ const toValue = await this.visitExpr(toExpr);
618
+ const isDown = ctx.DOWN() !== null;
619
+ if (this.#variableExists(loopVar)) {
620
+ throw new PSCForVariableReuseError(ctx, loopVar);
621
+ }
622
+ if (typeof fromValue !== "number" ||
623
+ typeof toValue !== "number" ||
624
+ !Number.isInteger(fromValue) ||
625
+ !Number.isInteger(toValue)) {
626
+ throw new PSCForRangeNotIntegerError(ctx, `${fromValue} and ${toValue}`);
627
+ }
628
+ let oldValue = undefined;
629
+ for (let i = fromValue; isDown ? i >= toValue : i <= toValue; isDown ? i-- : i++) {
630
+ this.#eventBus.emit("for_variable_change", {
631
+ startLine: ctx.FOR().symbol.line,
632
+ startCol: ctx.FOR().symbol.column,
633
+ endLine: toExpr.stop?.line,
634
+ endCol: toExpr.stop !== undefined
635
+ ? toExpr.stop.column + toExpr.stop.stop - toExpr.stop.start
636
+ : undefined,
637
+ variableName: loopVar,
638
+ oldValue: oldValue,
639
+ newValue: i,
640
+ });
641
+ this.#assignVariable(ctx, loopVar, i);
642
+ await this.visitBlock(ctx.block());
643
+ this.#deleteVariable(ctx, loopVar);
644
+ oldValue = i;
645
+ }
646
+ };
647
+ visitAsmStmt = async (ctx) => {
648
+ const ref = await this.visitLvalue(ctx.lvalue());
649
+ const value = await this.visitExpr(ctx.expr());
650
+ ref.set(value);
651
+ };
652
+ visitLvalue = async (ctx) => {
653
+ if (ctx.ID()) {
654
+ const varName = ctx.ID().getText();
655
+ return {
656
+ get: () => this.#variableExists(varName)
657
+ ? this.#readVariable(ctx, varName)
658
+ : undefined,
659
+ set: (value) => this.#assignVariable(ctx, varName, value),
660
+ };
661
+ }
662
+ else if (ctx.lvalue() && ctx.LSQUARE() && ctx.RSQUARE()) {
663
+ const leftRef = await this.visitLvalue(ctx.lvalue());
664
+ const leftVal = leftRef.get();
665
+ const unnormalizedIndices = await Promise.all(ctx.expr_list().map((expr) => this.visitExpr(expr)));
666
+ return {
667
+ get: () => {
668
+ // In lvalue array access, since we allow implicit array creation,
669
+ // if leftVal is undefined, we return undefined instead of throwing.
670
+ if (leftVal === undefined) {
671
+ return undefined;
672
+ }
673
+ let current = leftVal;
674
+ for (const unnormalizedIndex of unnormalizedIndices) {
675
+ if (!Array.isArray(current)) {
676
+ throw new PSCArrayAccessNotArrayError(ctx, this.#asString(ctx, current));
677
+ }
678
+ const normalizedIndex = this.#normalizeArrayIndexOrThrow(ctx, unnormalizedIndex);
679
+ // Since we allow implicit array extension,
680
+ // if the index is out of bounds, we return undefined instead of throwing.
681
+ if (normalizedIndex >= current.length) {
682
+ return undefined;
683
+ }
684
+ current = current[normalizedIndex];
685
+ }
686
+ return current;
687
+ },
688
+ set: (value) => {
689
+ const arr = leftVal != undefined ? leftVal : [];
690
+ if (!Array.isArray(arr)) {
691
+ throw new PSCArrayAccessNotArrayError(ctx, this.#asString(ctx, arr));
692
+ }
693
+ // Walk to the parent of the final index, creating intermediate arrays as needed
694
+ let current = arr;
695
+ for (let i = 0; i < unnormalizedIndices.length - 1; i++) {
696
+ const normalizedIndex = this.#normalizeArrayIndexOrThrow(ctx, unnormalizedIndices[i]);
697
+ // Implicit extension of the array if the index is out of bounds
698
+ while (current.length <= normalizedIndex) {
699
+ current.push(undefined);
700
+ }
701
+ if (current[normalizedIndex] === undefined) {
702
+ current[normalizedIndex] = [];
703
+ }
704
+ if (!Array.isArray(current[normalizedIndex])) {
705
+ throw new PSCArrayAccessNotArrayError(ctx, this.#asString(ctx, current[normalizedIndex]));
706
+ }
707
+ current = current[normalizedIndex];
708
+ }
709
+ // Set the final index
710
+ const lastNormalizedIndex = this.#normalizeArrayIndexOrThrow(ctx, unnormalizedIndices[unnormalizedIndices.length - 1]);
711
+ while (current.length <= lastNormalizedIndex) {
712
+ current.push(undefined);
713
+ }
714
+ current[lastNormalizedIndex] = value;
715
+ leftRef.set(arr);
716
+ },
717
+ };
718
+ }
719
+ throw new PSCImpossibleError(ctx, "Invalid assignment left-hand side.");
720
+ };
721
+ visitInputStmt = async (ctx) => {
722
+ const ref = await this.visitLvalue(ctx.lvalue());
723
+ const value = this.#stringSmartCast(await this.#options.inputFunction?.());
724
+ ref.set(value);
725
+ };
726
+ visitOutputStmt = async (ctx) => {
727
+ const val = this.#asString(ctx, await this.visitExpr(ctx.expr()));
728
+ this.#options.outputFunction?.(val);
729
+ };
730
+ visitSubprogram = async (ctx) => {
731
+ const name = ctx.ID(0).getText();
732
+ const paramNames = ctx
733
+ .ID_list()
734
+ ?.slice(1, ctx.ID_list().length)
735
+ .map((id) => id.getText()) ?? [];
736
+ // Subprogram is stored as a variable in the current variable stack
737
+ this.#assignVariable(ctx, name, async (params) => {
738
+ if (params.length !== paramNames.length) {
739
+ throw new PSCUnmatchedArgumentsError(ctx, name, paramNames.length, params.length);
740
+ }
741
+ // Create a new variable stack for the subprogram execution ignoring strictVariableScope option.
742
+ // This ensures that variables defined within the subprogram do not interfere with those in the calling context.
743
+ // If strictVariableScope is true, there is no need to create a new variable stack here
744
+ // since that is automatically handled by the visitBlock method
745
+ if (!this.#options.strictVariableScope) {
746
+ this.#newVariableStack(ctx);
747
+ }
748
+ // Assign arguments to the new variable stack
749
+ for (let i = 0; i < paramNames.length; i++) {
750
+ this.#assignVariable(ctx, paramNames[i], params[i]);
751
+ }
752
+ await this.visitBlock(ctx.block());
753
+ if (!this.#options.strictVariableScope) {
754
+ this.#popVariableStack(ctx);
755
+ }
756
+ // Terminate the returning state here
757
+ return this.#terminateReturn() ?? null;
758
+ });
759
+ };
760
+ visitReturnStmt = async (ctx) => {
761
+ const value = await this.visitExpr(ctx.expr());
762
+ this.#initiateReturn(value);
763
+ };
764
+ }
765
+ //# sourceMappingURL=visitor.js.map