Skip to content

Commit

Permalink
Add support for labeled break/continue
Browse files Browse the repository at this point in the history
This requires an additional field to Flow that maps user-defined
statement labels to the internal Binaryen labels passed to module.br().
Thanks to the existing logic to handle unlabeled break/continue, adding
support for labeled break/continue is a breeze.

Fixes #2889.
  • Loading branch information
CountBleck committed Dec 16, 2024
1 parent c9b169f commit 60e57da
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 22 deletions.
79 changes: 59 additions & 20 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2290,6 +2290,8 @@ export class Compiler extends DiagnosticEmitter {
private compileBlockStatement(
statement: BlockStatement
): ExpressionRef {
if (statement.label) return this.compileLabeledBlockStatement(statement);

let statements = statement.statements;
let outerFlow = this.currentFlow;
let innerFlow = outerFlow.fork();
Expand All @@ -2301,6 +2303,30 @@ export class Compiler extends DiagnosticEmitter {
return this.module.flatten(stmts);
}

private compileLabeledBlockStatement(
statement: BlockStatement
): ExpressionRef {
let statements = statement.statements;
let outerFlow = this.currentFlow;
let innerFlow = outerFlow.fork();

let labelNode = assert(statement.label);
let label = innerFlow.pushControlFlowLabel();
let breakLabel = `block-break|${label}`;
innerFlow.addUserLabel(labelNode.text, breakLabel, null, labelNode);
this.currentFlow = innerFlow;

let stmts = this.compileStatements(statements);
innerFlow.popControlFlowLabel(label);
innerFlow.removeUserLabel(labelNode.text);

outerFlow.inherit(innerFlow);
this.currentFlow = outerFlow;
return innerFlow.isAny(FlowFlags.Breaks | FlowFlags.ConditionallyBreaks)
? this.module.block(breakLabel, stmts)
: this.module.flatten(stmts);
}

private compileTypeDeclaration(statement: TypeDeclaration): ExpressionRef {
let flow = this.currentFlow;
let name = statement.name.text;
Expand All @@ -2324,23 +2350,25 @@ export class Compiler extends DiagnosticEmitter {
): ExpressionRef {
let module = this.module;
let labelNode = statement.label;
let flow = this.currentFlow;
let breakLabel: string | null = null;
if (labelNode) {
this.error(
DiagnosticCode.Not_implemented_0,
labelNode.range,
"Break label"
);
return module.unreachable();
const userLabel = flow.getUserLabel(labelNode.text);
if (userLabel) breakLabel = userLabel.breakLabel;
} else {
breakLabel = flow.breakLabel;
}
let flow = this.currentFlow;
let breakLabel = flow.breakLabel;

if (breakLabel == null) {
this.error(
DiagnosticCode.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement,
labelNode
? DiagnosticCode.A_break_statement_can_only_jump_to_a_label_of_an_enclosing_statement
: DiagnosticCode.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement,
statement.range
);
return module.unreachable();
}

flow.set(FlowFlags.Breaks);
return module.br(breakLabel);
}
Expand All @@ -2349,25 +2377,27 @@ export class Compiler extends DiagnosticEmitter {
statement: ContinueStatement
): ExpressionRef {
let module = this.module;
let label = statement.label;
if (label) {
this.error(
DiagnosticCode.Not_implemented_0,
label.range,
"Continue label"
);
return module.unreachable();
let labelNode = statement.label;
let flow = this.currentFlow;
let continueLabel: string | null = null;
if (labelNode) {
const userLabel = flow.getUserLabel(labelNode.text);
if (userLabel) continueLabel = userLabel.continueLabel;
} else {
continueLabel = flow.continueLabel;
}

// Check if 'continue' is allowed here
let flow = this.currentFlow;
let continueLabel = flow.continueLabel;
if (continueLabel == null) {
this.error(
DiagnosticCode.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement,
labelNode
? DiagnosticCode.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement
: DiagnosticCode.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement,
statement.range
);
return module.unreachable();
}

flow.set(FlowFlags.Continues | FlowFlags.Terminates);
return module.br(continueLabel);
}
Expand Down Expand Up @@ -2409,6 +2439,8 @@ export class Compiler extends DiagnosticEmitter {
let continueLabel = `do-continue|${label}`;
flow.continueLabel = continueLabel;
let loopLabel = `do-loop|${label}`;
let labelNode = statement.label;
if (labelNode) flow.addUserLabel(labelNode.text, breakLabel, continueLabel, labelNode);
this.currentFlow = flow;
let bodyStmts = new Array<ExpressionRef>();
let body = statement.body;
Expand All @@ -2418,6 +2450,7 @@ export class Compiler extends DiagnosticEmitter {
bodyStmts.push(this.compileStatement(body));
}
flow.popControlFlowLabel(label);
if (labelNode) flow.removeUserLabel(labelNode.text);

let possiblyContinues = flow.isAny(FlowFlags.Continues | FlowFlags.ConditionallyContinues);
let possiblyBreaks = flow.isAny(FlowFlags.Breaks | FlowFlags.ConditionallyBreaks);
Expand Down Expand Up @@ -2573,6 +2606,8 @@ export class Compiler extends DiagnosticEmitter {
bodyFlow.breakLabel = breakLabel;
let continueLabel = `for-continue|${label}`;
bodyFlow.continueLabel = continueLabel;
let labelNode = statement.label;
if (labelNode) bodyFlow.addUserLabel(labelNode.text, breakLabel, continueLabel, labelNode);
let loopLabel = `for-loop|${label}`;
this.currentFlow = bodyFlow;
let bodyStmts = new Array<ExpressionRef>();
Expand All @@ -2583,6 +2618,7 @@ export class Compiler extends DiagnosticEmitter {
bodyStmts.push(this.compileStatement(body));
}
bodyFlow.popControlFlowLabel(label);
if (labelNode) bodyFlow.removeUserLabel(labelNode.text);
bodyFlow.breakLabel = null;
bodyFlow.continueLabel = null;

Expand Down Expand Up @@ -3208,6 +3244,8 @@ export class Compiler extends DiagnosticEmitter {
thenFlow.breakLabel = breakLabel;
let continueLabel = `while-continue|${label}`;
thenFlow.continueLabel = continueLabel;
let labelNode = statement.label;
if (labelNode) thenFlow.addUserLabel(labelNode.text, breakLabel, continueLabel, labelNode);
this.currentFlow = thenFlow;
let bodyStmts = new Array<ExpressionRef>();
let body = statement.body;
Expand All @@ -3220,6 +3258,7 @@ export class Compiler extends DiagnosticEmitter {
module.br(continueLabel)
);
thenFlow.popControlFlowLabel(label);
if (labelNode) thenFlow.removeUserLabel(labelNode.text);

let possiblyContinues = thenFlow.isAny(FlowFlags.Continues | FlowFlags.ConditionallyContinues);
let possiblyBreaks = thenFlow.isAny(FlowFlags.Breaks | FlowFlags.ConditionallyBreaks);
Expand Down
2 changes: 2 additions & 0 deletions src/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
"Type expected.": 1110,
"A 'default' clause cannot appear more than once in a 'switch' statement.": 1113,
"Duplicate label '{0}'.": 1114,
"A 'continue' statement can only jump to a label of an enclosing iteration statement.": 1115,
"A 'break' statement can only jump to a label of an enclosing statement": 1116,
"An export assignment cannot have modifiers.": 1120,
"Octal literals are not allowed in strict mode.": 1121,
"Digit expected.": 1124,
Expand Down
45 changes: 43 additions & 2 deletions src/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ export const enum ConditionKind {
False
}

class UserLabels {
constructor(
/** The label we break to when encountering a break statement. */
readonly breakLabel: string,
/** The label we break to when encountering a continue statement. */
readonly continueLabel: string | null
) {}
}

/** A control flow evaluator. */
export class Flow {

Expand Down Expand Up @@ -245,10 +254,12 @@ export class Flow {
outer: Flow | null = null;
/** Flow flags indicating specific conditions. */
flags: FlowFlags = FlowFlags.None;
/** The label we break to when encountering a continue statement. */
/** The label we break to when encountering an unlabeled continue statement. */
continueLabel: string | null = null;
/** The label we break to when encountering a break statement. */
/** The label we break to when encountering an unlabeled break statement. */
breakLabel: string | null = null;
/** Map of user-declared statement label names to internal label names */
userLabelMap: Map<string,UserLabels> | null = null;
/** Scoped local variables. */
scopedLocals: Map<string,Local> | null = null;
/** Scoped type alias. */
Expand Down Expand Up @@ -351,6 +362,9 @@ export class Flow {
} else {
branch.continueLabel = this.continueLabel;
}
let userLabelMap = this.userLabelMap;
if (userLabelMap) userLabelMap = cloneMap(userLabelMap);
branch.userLabelMap = userLabelMap;
branch.localFlags = this.localFlags.slice();
if (this.sourceFunction.is(CommonFlags.Constructor)) {
let thisFieldFlags = assert(this.thisFieldFlags);
Expand Down Expand Up @@ -447,6 +461,33 @@ export class Flow {
return local;
}


/** Gets the internal labels associated with a user-declared label name. */
getUserLabel(name: string): UserLabels | null {
const userLabelMap = this.userLabelMap;
if (userLabelMap && userLabelMap.has(name)) return assert(userLabelMap.get(name));
return null;
}

/** Associates a user-declared label name with internal labels. */
addUserLabel(name: string, breakLabel: string, continueLabel: string | null, declarationNode: Node): void {
let userLabelMap = this.userLabelMap;
if (!userLabelMap) {
this.userLabelMap = userLabelMap = new Map();
} else if (userLabelMap.has(name)) {
this.program.error(DiagnosticCode.Duplicate_label_0, declarationNode.range, name);
}

userLabelMap.set(name, new UserLabels(breakLabel, continueLabel));
}

/** Remove a user-declared label name. */
removeUserLabel(name: string): void {
let userLabelMap = assert(this.userLabelMap);
assert(userLabelMap.has(name));
userLabelMap.delete(name);
}

/** Gets the scoped local of the specified name. */
getScopedLocal(name: string): Local | null {
let scopedLocals = this.scopedLocals;
Expand Down

0 comments on commit 60e57da

Please sign in to comment.