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