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 AssemblyScript#2889.
  • Loading branch information
CountBleck committed Dec 17, 2024
1 parent 66a29fc commit b47dc99
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 30 deletions.
113 changes: 85 additions & 28 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 @@ -2683,17 +2719,27 @@ export class Compiler extends DiagnosticEmitter {
);
let condKind = this.evaluateCondition(condExprTrueish);

let flow = this.currentFlow;
let label = -1;
let labelNode = statement.label;
let breakLabel: string | null = null;
if (labelNode) {
label = flow.pushControlFlowLabel();
breakLabel = `if-break|${label}`;
flow.addUserLabel(labelNode.text, breakLabel, null, labelNode);
}

// Shortcut if the condition is constant
switch (condKind) {
case ConditionKind.True: {
return module.block(null, [
return module.block(breakLabel, [
module.drop(condExprTrueish),
this.compileStatement(ifTrue)
]);
}
case ConditionKind.False: {
return ifFalse
? module.block(null, [
? module.block(breakLabel, [
module.drop(condExprTrueish),
this.compileStatement(ifFalse)
])
Expand All @@ -2703,8 +2749,6 @@ export class Compiler extends DiagnosticEmitter {

// From here on condition is always unknown

let flow = this.currentFlow;

// Compile ifTrue assuming the condition turned out true
let thenStmts = new Array<ExpressionRef>();
let thenFlow = flow.forkThen(condExpr);
Expand All @@ -2717,6 +2761,7 @@ export class Compiler extends DiagnosticEmitter {
this.currentFlow = flow;

// Compile ifFalse assuming the condition turned out false, if present
let expr: ExpressionRef;
let elseFlow = flow.forkElse(condExpr);
if (ifFalse) {
this.currentFlow = elseFlow;
Expand All @@ -2728,7 +2773,7 @@ export class Compiler extends DiagnosticEmitter {
}
flow.inheritAlternatives(thenFlow, elseFlow); // terminates if both do
this.currentFlow = flow;
return module.if(condExprTrueish,
expr = module.if(condExprTrueish,
module.flatten(thenStmts),
module.flatten(elseStmts)
);
Expand All @@ -2742,10 +2787,15 @@ export class Compiler extends DiagnosticEmitter {
flow.inheritAlternatives(thenFlow, elseFlow);
}
this.currentFlow = flow;
return module.if(condExprTrueish,
expr = module.if(condExprTrueish,
module.flatten(thenStmts)
);
}

if (!labelNode) return expr;
flow.popControlFlowLabel(label);
flow.removeUserLabel(labelNode.text);
return module.block(breakLabel, [expr]);
}

private compileReturnStatement(
Expand Down Expand Up @@ -2802,6 +2852,7 @@ export class Compiler extends DiagnosticEmitter {
): ExpressionRef {
let module = this.module;
let cases = statement.cases;
let labelNode = statement.label;
let numCases = cases.length;

// Compile the condition (always executes)
Expand All @@ -2824,6 +2875,9 @@ export class Compiler extends DiagnosticEmitter {
let breakIndex = 1;
let defaultIndex = -1;
let label = outerFlow.pushControlFlowLabel();
let breakLabel = `break|${label}`;
if (labelNode) outerFlow.addUserLabel(labelNode.text, breakLabel, null, labelNode);

for (let i = 0; i < numCases; ++i) {
let case_ = cases[i];
if (case_.isDefault) {
Expand All @@ -2843,7 +2897,7 @@ export class Compiler extends DiagnosticEmitter {
// If there is a default case, break to it, otherwise break out of the switch
breaks[breakIndex] = module.br(defaultIndex >= 0
? `case${defaultIndex}|${label}`
: `break|${label}`
: breakLabel
);

// Nest the case blocks in order, to be targeted by the br_if sequence
Expand All @@ -2859,7 +2913,6 @@ export class Compiler extends DiagnosticEmitter {
let innerFlow = outerFlow.fork(/* newBreakContext */ true, /* newContinueContext */ false);
if (fallThroughFlow) innerFlow.mergeBranch(fallThroughFlow);
this.currentFlow = innerFlow;
let breakLabel = `break|${label}`;
innerFlow.breakLabel = breakLabel;

let isLast = i == numCases - 1;
Expand Down Expand Up @@ -2897,6 +2950,7 @@ export class Compiler extends DiagnosticEmitter {
currentBlock = module.block(nextLabel, stmts, TypeRef.None); // must be a labeled block
}
outerFlow.popControlFlowLabel(label);
if (labelNode) outerFlow.removeUserLabel(labelNode.text);

// If the switch has a default, we only get past through any breaking flow
if (defaultIndex >= 0) {
Expand Down Expand Up @@ -3208,6 +3262,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 +3276,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 b47dc99

Please sign in to comment.