@ball-lang/compiler 1.18.1 → 1.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ball-lang/compiler",
3
- "version": "1.18.1",
3
+ "version": "1.19.1",
4
4
  "description": "Ball → TypeScript compiler. Consumes a Ball protobuf Program and emits idiomatic TypeScript via ts-morph. The canonical TS compiler for Ball — lives in TS land so TS syntax knowledge doesn't leak into other languages.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/compiler.ts CHANGED
@@ -112,6 +112,36 @@ export class BallCompiler {
112
112
  */
113
113
  private activeGotoLabels: string[] = [];
114
114
 
115
+ /**
116
+ * Stack of enclosing *goto-switch* lowerings (see `emitGotoSwitchStmt`). A
117
+ * Ball `switch` with labelled cases (`case 0: continue one; one: case 1: …`)
118
+ * is Dart's goto-via-switch: `continue <caseLabel>` transfers control to that
119
+ * case's body with NO subject re-check. TS has no labelled switch cases, so
120
+ * such a switch lowers to a `<loopLabel>: while (…) switch (<stateVar>) { … }`
121
+ * state machine; a `continue <caseLabel>` inside a case body becomes
122
+ * `<stateVar> = <armIndex>; continue <loopLabel>;`. `labelToIndex` maps each
123
+ * case label to its arm index, `depth` records `loopNest` at the case-body
124
+ * level (so an unlabeled `break`/`continue` inside a *nested* loop is left
125
+ * verbatim for that loop rather than hijacked into the switch's own loop).
126
+ */
127
+ private switchLabelStack: Array<{
128
+ loopLabel: string;
129
+ stateVar: string;
130
+ labelToIndex: Map<string, number>;
131
+ depth: number;
132
+ }> = [];
133
+
134
+ /**
135
+ * Count of enclosing break/continue-capturing scopes (loops + switches)
136
+ * currently open during body emission. Consulted only when a
137
+ * `switchLabelStack` entry is active, to tell a case-body-level break from a
138
+ * nested-loop break (see `switchLabelStack`).
139
+ */
140
+ private loopNest = 0;
141
+
142
+ /** Monotonic id for uniquely naming goto-switch loop labels / state vars. */
143
+ private switchUid = 0;
144
+
115
145
  constructor(program: Program) {
116
146
  this.program = program;
117
147
  }
@@ -3188,12 +3218,42 @@ function __isUnknownFnError(e: any): boolean {
3188
3218
  }
3189
3219
  case "break": {
3190
3220
  const label = stringField(call, "label");
3191
- this.writeln(label ? `break ${label};` : "break;");
3221
+ if (label) {
3222
+ // A labelled break targets an enclosing labelled loop, never a
3223
+ // switch case — emit verbatim.
3224
+ this.writeln(`break ${label};`);
3225
+ } else {
3226
+ // An unlabeled break directly inside a goto-switch case body exits
3227
+ // the switch (its lowered state-machine loop). Inside a *nested* loop
3228
+ // it belongs to that loop, so leave it verbatim (see switchLabelStack).
3229
+ const ctx = this.switchLabelStack[this.switchLabelStack.length - 1];
3230
+ this.writeln(
3231
+ ctx && this.loopNest === ctx.depth ? `break ${ctx.loopLabel};` : "break;",
3232
+ );
3233
+ }
3192
3234
  break;
3193
3235
  }
3194
3236
  case "continue": {
3195
3237
  const label = stringField(call, "label");
3196
- this.writeln(label ? `continue ${label};` : "continue;");
3238
+ if (label) {
3239
+ // `continue <caseLabel>` naming a labelled case of an enclosing
3240
+ // goto-switch is a goto: jump to that case's arm with no subject
3241
+ // re-check (innermost matching switch wins). Any other label targets
3242
+ // an enclosing labelled loop and is emitted verbatim.
3243
+ let handled = false;
3244
+ for (let i = this.switchLabelStack.length - 1; i >= 0; i--) {
3245
+ const ctx = this.switchLabelStack[i];
3246
+ const idx = ctx.labelToIndex.get(label);
3247
+ if (idx !== undefined) {
3248
+ this.writeln(`${ctx.stateVar} = ${idx}; continue ${ctx.loopLabel};`);
3249
+ handled = true;
3250
+ break;
3251
+ }
3252
+ }
3253
+ if (!handled) this.writeln(`continue ${label};`);
3254
+ } else {
3255
+ this.writeln("continue;");
3256
+ }
3197
3257
  break;
3198
3258
  }
3199
3259
  case "labeled": {
@@ -3233,101 +3293,26 @@ function __isUnknownFnError(e: any): boolean {
3233
3293
  break;
3234
3294
  }
3235
3295
  const subjectStr = this.expr(subjectExpr);
3296
+ const caseExprs = casesField.literal?.listValue?.elements ?? [];
3297
+ const parsed = this.parseSwitchCases(caseExprs);
3298
+ // A switch whose cases carry labels (`case 0: continue one; one: case
3299
+ // 1: …`) is Dart's goto-via-switch — lower it to a state machine so
3300
+ // `continue <caseLabel>` can jump between arms. Plain switches keep the
3301
+ // if/else-chain (zero change for the non-labelled common case).
3302
+ if (parsed.labelToArmIndex.size > 0) {
3303
+ this.emitGotoSwitchStmt(subjectStr, parsed);
3304
+ break;
3305
+ }
3306
+ const { parsedCases, defaultBody } = parsed;
3307
+ let first = true;
3236
3308
  // Wrap in do-while(false) so case `break;` exits the switch,
3237
3309
  // not an enclosing for loop (switch is compiled as if/else chain).
3238
3310
  this.writeln(`do { const __sw = ${subjectStr};`);
3239
3311
  this.depth++;
3240
- const caseExprs = casesField.literal?.listValue?.elements ?? [];
3241
- let defaultBody: Expression | undefined;
3242
- let first = true;
3243
- // Parse all cases, detecting fall-through (empty body = merge
3244
- // with next case via ||).
3245
- const parsedCases: Array<{ conds: string[]; body?: Expression; patText?: string; structuredBindings?: Array<{ varName: string; expr: string }>; guard?: Expression }> = [];
3246
- const pendingConds: string[] = [];
3247
- let lastPatText: string | undefined;
3248
- for (const ce of caseExprs) {
3249
- if (!ce.messageCreation) continue;
3250
- let pattern: Expression | undefined;
3251
- let body: Expression | undefined;
3252
- let isDefaultFlag = false;
3253
- let patternExprField: Expression | undefined;
3254
- let guardField: Expression | undefined;
3255
- for (const fd of ce.messageCreation.fields ?? []) {
3256
- if (fd.name === "pattern") pattern = fd.value;
3257
- if (fd.name === "body") body = fd.value;
3258
- if (fd.name === "is_default" && fd.value?.literal?.boolValue === true) isDefaultFlag = true;
3259
- if (fd.name === "pattern_expr") patternExprField = fd.value;
3260
- if (fd.name === "guard") guardField = fd.value;
3261
- }
3262
- // Check for is_default flag
3263
- if (isDefaultFlag) { defaultBody = body; continue; }
3264
- // Handle structured pattern_expr (only for known pattern kinds)
3265
- if (patternExprField) {
3266
- const result = compileStructuredPattern(patternExprField, "__sw", (e) => this.expr(e));
3267
- if (result) {
3268
- const cond = result.condition;
3269
- // A catch-all with no guard ends the chain; with a guard the
3270
- // branch stays refutable (a false guard falls through).
3271
- if (cond === "true" && !guardField) { defaultBody = body; break; }
3272
- const isEmpty = body && body.block &&
3273
- (body.block.statements ?? []).length === 0 &&
3274
- body.block.result === undefined;
3275
- if (!guardField && (!body || isEmpty)) {
3276
- pendingConds.push(cond);
3277
- continue;
3278
- }
3279
- pendingConds.push(cond);
3280
- parsedCases.push({ conds: [...pendingConds], body, structuredBindings: result.bindings, guard: guardField });
3281
- pendingConds.length = 0;
3282
- continue;
3283
- }
3284
- // Unknown pattern kind -- fall through to text-based pattern handling
3285
- }
3286
- if (!pattern) { defaultBody = body; continue; }
3287
- const patText = patternLiteralText(pattern);
3288
- const cond = patText !== undefined
3289
- ? patternToTsCondition(patText, "__sw")
3290
- : `((__sw) === ${this.expr(pattern)})`;
3291
- if (cond === "true" && !guardField) { defaultBody = body; break; }
3292
- // Empty body = fall-through: accumulate conditions.
3293
- const isEmpty = body && body.block &&
3294
- (body.block.statements ?? []).length === 0 &&
3295
- body.block.result === undefined;
3296
- if (!guardField && (!body || isEmpty)) {
3297
- pendingConds.push(cond);
3298
- lastPatText = patText;
3299
- continue;
3300
- }
3301
- pendingConds.push(cond);
3302
- parsedCases.push({ conds: [...pendingConds], body, patText: patText ?? lastPatText, guard: guardField });
3303
- pendingConds.length = 0;
3304
- lastPatText = undefined;
3305
- }
3312
+ this.loopNest++;
3306
3313
  for (const pc of parsedCases) {
3307
- // Gather this case's pattern bindings.
3308
- const caseBindings: Array<{ varName: string; expr: string }> = [];
3309
- if (pc.structuredBindings) caseBindings.push(...pc.structuredBindings);
3310
- if (pc.patText) caseBindings.push(...patternBindings(pc.patText, "__sw"));
3311
- // When a `when` guard is present the bindings must be visible to the
3312
- // guard, which is part of the `if` condition. Hoist them to temps
3313
- // BEFORE the `if` (so the guard can read them) and reference the temps
3314
- // inside. Without this, an entered `if` block swallows the arm and
3315
- // later cases never get tested when the guard is false.
3316
- let combinedCond = pc.conds.join(" || ");
3317
- if (pc.guard) {
3318
- // Evaluate the guard inside an IIFE bound to the pattern variables,
3319
- // gated by the match condition via `&&` (so the IIFE only runs when
3320
- // the pattern matched and the temps are valid). The guard's binders
3321
- // are passed positionally; the guard expression itself is untouched.
3322
- const matchCond = `(${pc.conds.join(" || ")})`;
3323
- if (caseBindings.length > 0) {
3324
- const params = caseBindings.map(b => b.varName).join(", ");
3325
- const args = caseBindings.map(b => b.expr).join(", ");
3326
- combinedCond = `${matchCond} && ((${params}) => (${this.expr(pc.guard)}))(${args})`;
3327
- } else {
3328
- combinedCond = `${matchCond} && (${this.expr(pc.guard)})`;
3329
- }
3330
- }
3314
+ const caseBindings = this.switchArmBindings(pc);
3315
+ const combinedCond = this.buildSwitchArmCond(pc, caseBindings);
3331
3316
  const kw = first ? "if" : "else if";
3332
3317
  this.writeln(`${kw} (${combinedCond}) {`);
3333
3318
  this.depth++;
@@ -3351,6 +3336,7 @@ function __isUnknownFnError(e: any): boolean {
3351
3336
  this.writeln("}");
3352
3337
  }
3353
3338
  }
3339
+ this.loopNest--;
3354
3340
  this.depth--;
3355
3341
  this.writeln("} while (false);");
3356
3342
  break;
@@ -3358,6 +3344,240 @@ function __isUnknownFnError(e: any): boolean {
3358
3344
  }
3359
3345
  }
3360
3346
 
3347
+ /**
3348
+ * Parse a switch's `cases[]` into if/else-chain arms, merging empty-body
3349
+ * fall-through cases (`case 'a': case 'b': body`) into the following arm via
3350
+ * `||`. Also records `label → arm index` for labelled cases so a
3351
+ * goto-via-switch (`emitGotoSwitchStmt`) can jump between arms; a label on an
3352
+ * empty fall-through case maps to the arm that absorbs it (jumping there and
3353
+ * immediately falling through are equivalent). Conditions reference the
3354
+ * subject as `__sw`, so callers must bind `const __sw = <subject>`.
3355
+ */
3356
+ private parseSwitchCases(caseExprs: Expression[]): {
3357
+ parsedCases: Array<{ conds: string[]; body?: Expression; patText?: string; structuredBindings?: Array<{ varName: string; expr: string }>; guard?: Expression }>;
3358
+ defaultBody?: Expression;
3359
+ labelToArmIndex: Map<string, number>;
3360
+ defaultArmIndex: number;
3361
+ } {
3362
+ const parsedCases: Array<{ conds: string[]; body?: Expression; patText?: string; structuredBindings?: Array<{ varName: string; expr: string }>; guard?: Expression }> = [];
3363
+ const labelToArmIndex = new Map<string, number>();
3364
+ let defaultBody: Expression | undefined;
3365
+ // Labels waiting to be bound to the arm that absorbs them (labels on empty
3366
+ // fall-through cases, and the label of a non-empty case, both attach when
3367
+ // that case's arm is pushed). Labels pending when `default` is reached bind
3368
+ // to the default arm.
3369
+ const pendingConds: string[] = [];
3370
+ const pendingLabels: string[] = [];
3371
+ const defaultLabels: string[] = [];
3372
+ let lastPatText: string | undefined;
3373
+ // Push the current arm, binding any labels accumulated for it.
3374
+ const pushArm = (arm: { conds: string[]; body?: Expression; patText?: string; structuredBindings?: Array<{ varName: string; expr: string }>; guard?: Expression }) => {
3375
+ const armIndex = parsedCases.length;
3376
+ for (const l of pendingLabels) labelToArmIndex.set(l, armIndex);
3377
+ pendingLabels.length = 0;
3378
+ parsedCases.push(arm);
3379
+ };
3380
+ // Record the default arm's body and drain any labels accumulated up to it
3381
+ // (a `continue <label>` naming a labelled case that falls into `default`
3382
+ // jumps to the default arm).
3383
+ const markDefault = (b: Expression | undefined) => {
3384
+ defaultBody = b;
3385
+ defaultLabels.push(...pendingLabels);
3386
+ pendingLabels.length = 0;
3387
+ };
3388
+ for (const ce of caseExprs) {
3389
+ if (!ce.messageCreation) continue;
3390
+ let pattern: Expression | undefined;
3391
+ let body: Expression | undefined;
3392
+ let isDefaultFlag = false;
3393
+ let patternExprField: Expression | undefined;
3394
+ let guardField: Expression | undefined;
3395
+ let caseLabel: string | undefined;
3396
+ for (const fd of ce.messageCreation.fields ?? []) {
3397
+ if (fd.name === "pattern") pattern = fd.value;
3398
+ if (fd.name === "body") body = fd.value;
3399
+ if (fd.name === "is_default" && fd.value?.literal?.boolValue === true) isDefaultFlag = true;
3400
+ if (fd.name === "pattern_expr") patternExprField = fd.value;
3401
+ if (fd.name === "guard") guardField = fd.value;
3402
+ if (fd.name === "label") {
3403
+ const l = fd.value?.literal?.stringValue;
3404
+ if (l) caseLabel = l;
3405
+ }
3406
+ }
3407
+ if (caseLabel) pendingLabels.push(caseLabel);
3408
+ // Check for is_default flag
3409
+ if (isDefaultFlag) { markDefault(body); continue; }
3410
+ // Handle structured pattern_expr (only for known pattern kinds)
3411
+ if (patternExprField) {
3412
+ const result = compileStructuredPattern(patternExprField, "__sw", (e) => this.expr(e));
3413
+ if (result) {
3414
+ const cond = result.condition;
3415
+ // A catch-all with no guard ends the chain; with a guard the
3416
+ // branch stays refutable (a false guard falls through).
3417
+ if (cond === "true" && !guardField) { markDefault(body); break; }
3418
+ const isEmpty = body && body.block &&
3419
+ (body.block.statements ?? []).length === 0 &&
3420
+ body.block.result === undefined;
3421
+ if (!guardField && (!body || isEmpty)) {
3422
+ pendingConds.push(cond);
3423
+ continue;
3424
+ }
3425
+ pendingConds.push(cond);
3426
+ pushArm({ conds: [...pendingConds], body, structuredBindings: result.bindings, guard: guardField });
3427
+ pendingConds.length = 0;
3428
+ continue;
3429
+ }
3430
+ // Unknown pattern kind -- fall through to text-based pattern handling
3431
+ }
3432
+ if (!pattern) { markDefault(body); continue; }
3433
+ const patText = patternLiteralText(pattern);
3434
+ const cond = patText !== undefined
3435
+ ? patternToTsCondition(patText, "__sw")
3436
+ : `((__sw) === ${this.expr(pattern)})`;
3437
+ if (cond === "true" && !guardField) { markDefault(body); break; }
3438
+ // Empty body = fall-through: accumulate conditions.
3439
+ const isEmpty = body && body.block &&
3440
+ (body.block.statements ?? []).length === 0 &&
3441
+ body.block.result === undefined;
3442
+ if (!guardField && (!body || isEmpty)) {
3443
+ pendingConds.push(cond);
3444
+ lastPatText = patText;
3445
+ continue;
3446
+ }
3447
+ pendingConds.push(cond);
3448
+ pushArm({ conds: [...pendingConds], body, patText: patText ?? lastPatText, guard: guardField });
3449
+ pendingConds.length = 0;
3450
+ lastPatText = undefined;
3451
+ }
3452
+ const defaultArmIndex = defaultBody !== undefined ? parsedCases.length : -1;
3453
+ if (defaultArmIndex >= 0) {
3454
+ for (const l of defaultLabels) labelToArmIndex.set(l, defaultArmIndex);
3455
+ }
3456
+ return { parsedCases, defaultBody, labelToArmIndex, defaultArmIndex };
3457
+ }
3458
+
3459
+ /**
3460
+ * Lower a goto-via-switch (a Ball `switch` with labelled cases) to a
3461
+ * state-machine loop. TS has no labelled switch cases, so
3462
+ *
3463
+ * switch (s) { case 0: …; continue one; one: case 1: …; break; default: … }
3464
+ *
3465
+ * becomes
3466
+ *
3467
+ * { const __sw = s; let __swst = -1;
3468
+ * if (__sw === 0) __swst = 0; else if (__sw === 1) __swst = 1;
3469
+ * if (__swst === -1) __swst = <default arm>;
3470
+ * __swl: while (__swst >= 0) { switch (__swst) {
3471
+ * case 0: { …; __swst = 1; continue __swl; } // continue one → arm 1
3472
+ * case 1: { …; break __swl; }
3473
+ * case <default>: { …; break __swl; }
3474
+ * } break __swl; } }
3475
+ *
3476
+ * Phase 1 picks the entry arm by matching the subject (default only if
3477
+ * nothing matched); phase 2 runs from that arm, where a `continue <label>`
3478
+ * inside a body re-enters the loop at the labelled arm with NO subject
3479
+ * re-check (see the `break`/`continue` cases in `emitControlFlowStatement`).
3480
+ * A body that falls off its arm exits via the trailing `break __swl`.
3481
+ *
3482
+ * Known limitation: pattern bindings are recomputed from `__sw` on each arm
3483
+ * (so a goto INTO a case with different bindings would rebind from the
3484
+ * subject rather than leave them unbound as the reference engine does) — not
3485
+ * reachable from valid Dart, where a `continue` target can't use a preceding
3486
+ * case's binders.
3487
+ */
3488
+ private emitGotoSwitchStmt(
3489
+ subjectStr: string,
3490
+ parsed: {
3491
+ parsedCases: Array<{ conds: string[]; body?: Expression; patText?: string; structuredBindings?: Array<{ varName: string; expr: string }>; guard?: Expression }>;
3492
+ defaultBody?: Expression;
3493
+ labelToArmIndex: Map<string, number>;
3494
+ defaultArmIndex: number;
3495
+ },
3496
+ ): void {
3497
+ const { parsedCases, defaultBody, labelToArmIndex, defaultArmIndex } = parsed;
3498
+ const uid = this.switchUid++;
3499
+ const loopLabel = `__swl${uid}`;
3500
+ const stateVar = `__swst${uid}`;
3501
+
3502
+ this.writeln("{");
3503
+ this.depth++;
3504
+ this.writeln(`const __sw = ${subjectStr};`);
3505
+ this.writeln(`let ${stateVar} = -1;`);
3506
+ // Phase 1 — entry arm selection (non-default arms, in order).
3507
+ let first = true;
3508
+ for (let i = 0; i < parsedCases.length; i++) {
3509
+ const pc = parsedCases[i];
3510
+ const combinedCond = this.buildSwitchArmCond(pc, this.switchArmBindings(pc));
3511
+ this.writeln(`${first ? "if" : "else if"} (${combinedCond}) { ${stateVar} = ${i}; }`);
3512
+ first = false;
3513
+ }
3514
+ if (defaultArmIndex >= 0) {
3515
+ this.writeln(`if (${stateVar} === -1) ${stateVar} = ${defaultArmIndex};`);
3516
+ }
3517
+ // Phase 2 — run from the entry arm, honoring goto/break.
3518
+ this.writeln(`${loopLabel}: while (${stateVar} >= 0) {`);
3519
+ this.depth++;
3520
+ this.writeln(`switch (${stateVar}) {`);
3521
+ this.depth++;
3522
+ this.switchLabelStack.push({ loopLabel, stateVar, labelToIndex: labelToArmIndex, depth: this.loopNest + 1 });
3523
+ this.loopNest++;
3524
+ const emitArm = (idx: number, body: Expression | undefined, bindings: Array<{ varName: string; expr: string }>) => {
3525
+ this.writeln(`case ${idx}: {`);
3526
+ this.depth++;
3527
+ for (const b of bindings) this.writeln(`const ${b.varName} = ${b.expr};`);
3528
+ if (body) this.emitStatementOrExpression(body, false);
3529
+ // A body that falls off its arm (or an inner `break` that exited a nested
3530
+ // loop) ends the switch — arms never fall through to the next.
3531
+ this.writeln(`break ${loopLabel};`);
3532
+ this.depth--;
3533
+ this.writeln("}");
3534
+ };
3535
+ for (let i = 0; i < parsedCases.length; i++) {
3536
+ emitArm(i, parsedCases[i].body, this.switchArmBindings(parsedCases[i]));
3537
+ }
3538
+ if (defaultArmIndex >= 0) emitArm(defaultArmIndex, defaultBody, []);
3539
+ this.loopNest--;
3540
+ this.switchLabelStack.pop();
3541
+ this.depth--;
3542
+ this.writeln("}");
3543
+ this.writeln(`break ${loopLabel};`);
3544
+ this.depth--;
3545
+ this.writeln("}");
3546
+ this.depth--;
3547
+ this.writeln("}");
3548
+ }
3549
+
3550
+ /** A switch arm's pattern bindings (`case int n`, `case {'k': var v}`, …),
3551
+ * read off the subject alias `__sw`. Shared by the if/else-chain and
3552
+ * goto-switch lowerings. */
3553
+ private switchArmBindings(pc: { patText?: string; structuredBindings?: Array<{ varName: string; expr: string }> }): Array<{ varName: string; expr: string }> {
3554
+ const b: Array<{ varName: string; expr: string }> = [];
3555
+ if (pc.structuredBindings) b.push(...pc.structuredBindings);
3556
+ if (pc.patText) b.push(...patternBindings(pc.patText, "__sw"));
3557
+ return b;
3558
+ }
3559
+
3560
+ /**
3561
+ * A switch arm's match condition over `__sw`, folding a `when` guard in. The
3562
+ * guard runs inside an IIFE bound to the pattern variables, gated by the
3563
+ * match condition via `&&` (so it only runs once the pattern matched and the
3564
+ * temps are valid); its binders are passed positionally, the guard expression
3565
+ * itself untouched. Shared by the if/else-chain and goto-switch lowerings.
3566
+ */
3567
+ private buildSwitchArmCond(
3568
+ pc: { conds: string[]; guard?: Expression },
3569
+ caseBindings: Array<{ varName: string; expr: string }>,
3570
+ ): string {
3571
+ if (!pc.guard) return pc.conds.join(" || ");
3572
+ const matchCond = `(${pc.conds.join(" || ")})`;
3573
+ if (caseBindings.length > 0) {
3574
+ const params = caseBindings.map(b => b.varName).join(", ");
3575
+ const args = caseBindings.map(b => b.expr).join(", ");
3576
+ return `${matchCond} && ((${params}) => (${this.expr(pc.guard)}))(${args})`;
3577
+ }
3578
+ return `${matchCond} && (${this.expr(pc.guard)})`;
3579
+ }
3580
+
3361
3581
  private emitIfStmt(call: FunctionCall): void {
3362
3582
  const cond = field(call, "condition");
3363
3583
  const then_ = field(call, "then");
@@ -3419,7 +3639,9 @@ function __isUnknownFnError(e: any): boolean {
3419
3639
  const updateStr = update ? this.expr(unwrapLambda(update)) : "";
3420
3640
  this.writeln(`for (${initStr}; ${condStr}; ${updateStr}) {`);
3421
3641
  this.depth++;
3642
+ this.loopNest++;
3422
3643
  if (body) this.emitStatementOrExpression(unwrapLambda(body), false);
3644
+ this.loopNest--;
3423
3645
  this.depth--;
3424
3646
  this.writeln(`}`);
3425
3647
  }
@@ -3430,7 +3652,9 @@ function __isUnknownFnError(e: any): boolean {
3430
3652
  const body = field(call, "body");
3431
3653
  this.writeln(`for (const ${variable} of ${this.expr(iterable)}) {`);
3432
3654
  this.depth++;
3655
+ this.loopNest++;
3433
3656
  if (body) this.emitStatementOrExpression(unwrapLambda(body), false);
3657
+ this.loopNest--;
3434
3658
  this.depth--;
3435
3659
  this.writeln(`}`);
3436
3660
  }
@@ -3440,7 +3664,9 @@ function __isUnknownFnError(e: any): boolean {
3440
3664
  const body = field(call, "body");
3441
3665
  this.writeln(`while (${this.expr(unwrapLambda(cond!))}) {`);
3442
3666
  this.depth++;
3667
+ this.loopNest++;
3443
3668
  if (body) this.emitStatementOrExpression(unwrapLambda(body), false);
3669
+ this.loopNest--;
3444
3670
  this.depth--;
3445
3671
  this.writeln(`}`);
3446
3672
  }
@@ -3464,7 +3690,9 @@ function __isUnknownFnError(e: any): boolean {
3464
3690
  this.activeGotoLabels.push(name);
3465
3691
  this.writeln(`${name}: while (true) {`);
3466
3692
  this.depth++;
3693
+ this.loopNest++;
3467
3694
  this.emitStatementOrExpression(body, false);
3695
+ this.loopNest--;
3468
3696
  this.writeln(`break ${name};`);
3469
3697
  this.depth--;
3470
3698
  this.writeln(`}`);
@@ -3491,7 +3719,9 @@ function __isUnknownFnError(e: any): boolean {
3491
3719
  const body = field(call, "body");
3492
3720
  this.writeln(`do {`);
3493
3721
  this.depth++;
3722
+ this.loopNest++;
3494
3723
  if (body) this.emitStatementOrExpression(unwrapLambda(body), false);
3724
+ this.loopNest--;
3495
3725
  this.depth--;
3496
3726
  this.writeln(`} while (${this.expr(unwrapLambda(cond!))});`);
3497
3727
  }