@ball-lang/compiler 1.19.0 → 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/dist/compiler.d.ts +74 -0
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +322 -113
- package/dist/compiler.js.map +1 -1
- package/package.json +1 -1
- package/src/compiler.ts +322 -92
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ball-lang/compiler",
|
|
3
|
-
"version": "1.19.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3308
|
-
const
|
|
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
|
}
|