blob: 4ed78f4644f8eb25f02292ebcf95998a14f3879e [file] [log] [blame] [edit]
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:kernel/ast.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
import 'closures.dart';
import 'code_generator.dart';
import 'translator.dart';
/// Placement of a control flow graph target within a statement. This
/// distinction is necessary since some statements need to have two targets
/// associated with them.
///
/// The meanings of the variants are:
///
/// - [Inner]: Loop entry of a [DoStatement], condition of a [ForStatement] or
/// [WhileStatement], the `else` branch of an [IfStatement], or the
/// initial entry target for a function body.
///
/// - [After]: After a statement, the resumption point of a suspension point
/// ([YieldStatement] or [AwaitExpression]), or the final state
/// (iterator done) of a function body.
enum _StateTargetPlacement { Inner, After }
/// Representation of target in the `sync*` control flow graph.
class StateTarget {
final int index;
final TreeNode node;
final _StateTargetPlacement _placement;
StateTarget._(this.index, this.node, this._placement);
@override
String toString() {
String place = _placement == _StateTargetPlacement.Inner ? "in" : "after";
return "$index: $place $node";
}
}
/// Identify which statements contain `await` or `yield` statements, and assign
/// target indices to all control flow targets of these.
///
/// Target indices are assigned in program order.
class _YieldFinder extends RecursiveVisitor {
final List<StateTarget> targets = [];
final bool enableAsserts;
// The number of `await` statements seen so far.
int yieldCount = 0;
_YieldFinder(this.enableAsserts);
List<StateTarget> find(FunctionNode function) {
// Initial state
addTarget(function.body!, _StateTargetPlacement.Inner);
assert(function.body is Block || function.body is ReturnStatement);
recurse(function.body!);
// Final state
addTarget(function.body!, _StateTargetPlacement.After);
return targets;
}
/// Recurse into a statement and then remove any targets added by the
/// statement if it doesn't contain any `await` statements.
void recurse(Statement statement) {
final yieldCountIn = yieldCount;
final targetsIn = targets.length;
statement.accept(this);
if (yieldCount == yieldCountIn) {
targets.length = targetsIn;
}
}
void addTarget(TreeNode node, _StateTargetPlacement placement) {
targets.add(StateTarget._(targets.length, node, placement));
}
@override
void visitBlock(Block node) {
for (Statement statement in node.statements) {
recurse(statement);
}
}
@override
void visitDoStatement(DoStatement node) {
addTarget(node, _StateTargetPlacement.Inner);
recurse(node.body);
}
@override
void visitForStatement(ForStatement node) {
addTarget(node, _StateTargetPlacement.Inner);
recurse(node.body);
addTarget(node, _StateTargetPlacement.After);
}
@override
void visitIfStatement(IfStatement node) {
recurse(node.then);
if (node.otherwise != null) {
addTarget(node, _StateTargetPlacement.Inner);
recurse(node.otherwise!);
}
addTarget(node, _StateTargetPlacement.After);
}
@override
void visitLabeledStatement(LabeledStatement node) {
recurse(node.body);
addTarget(node, _StateTargetPlacement.After);
}
@override
void visitSwitchStatement(SwitchStatement node) {
for (SwitchCase c in node.cases) {
addTarget(c, _StateTargetPlacement.Inner);
recurse(c.body);
}
addTarget(node, _StateTargetPlacement.After);
}
@override
void visitTryFinally(TryFinally node) {
// [TryFinally] blocks are always compiled to as CFG, even when they don't
// have awaits. This is to keep the code size small: with normal
// compilation finalizer blocks need to be duplicated based on
// continuations, which we don't need in the CFG implementation.
yieldCount++;
recurse(node.body);
addTarget(node, _StateTargetPlacement.Inner);
recurse(node.finalizer);
addTarget(node, _StateTargetPlacement.After);
}
@override
void visitTryCatch(TryCatch node) {
// Also always compile [TryCatch] blocks to the CFG to be able to set
// finalizer continuations.
yieldCount++;
recurse(node.body);
for (Catch c in node.catches) {
addTarget(c, _StateTargetPlacement.Inner);
recurse(c.body);
}
addTarget(node, _StateTargetPlacement.After);
}
@override
void visitWhileStatement(WhileStatement node) {
addTarget(node, _StateTargetPlacement.Inner);
recurse(node.body);
addTarget(node, _StateTargetPlacement.After);
}
@override
void visitYieldStatement(YieldStatement node) {
yieldCount++;
addTarget(node, _StateTargetPlacement.After);
}
// Handle awaits. After the await transformation await can only appear in a
// RHS of a top-level assignment, or as a top-level statement.
@override
void visitExpressionStatement(ExpressionStatement node) {
final expression = node.expression;
// Handle awaits in RHS of assignments.
if (expression is VariableSet) {
final value = expression.value;
if (value is AwaitExpression) {
yieldCount++;
addTarget(value, _StateTargetPlacement.After);
return;
}
}
// Handle top-level awaits.
if (expression is AwaitExpression) {
yieldCount++;
addTarget(node, _StateTargetPlacement.After);
return;
}
super.visitExpressionStatement(node);
}
@override
void visitFunctionExpression(FunctionExpression node) {}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {}
// Any other await expression means the await transformer is buggy and didn't
// transform the expression as expected.
@override
void visitAwaitExpression(AwaitExpression node) {
// Await expressions should've been converted to `VariableSet` statements
// by `_AwaitTransformer`.
throw 'Unexpected await expression: $node (${node.location})';
}
@override
void visitAssertStatement(AssertStatement node) {
if (enableAsserts) {
super.visitAssertStatement(node);
}
}
@override
void visitAssertBlock(AssertBlock node) {
if (enableAsserts) {
super.visitAssertBlock(node);
}
}
}
class ExceptionHandlerStack {
/// Current exception handler stack. A CFG block generated when this is not
/// empty should have a Wasm `try` instruction wrapping the block.
///
/// A `catch` block will jump to the next handler on the stack (the last
/// handler in the list), which then jumps to the next if the exception type
/// test fails.
///
/// Because CFG blocks for [Catch] blocks and finalizers will have Wasm `try`
/// blocks for the parent handlers, we can use a Wasm `throw` instruction
/// (instead of jumping to the parent handler) in [Catch] blocks and
/// finalizers for rethrowing.
final List<_ExceptionHandler> _handlers = [];
/// Maps Wasm `try` blocks to number of handlers in [_handlers] that they
/// cover for.
final List<int> _tryBlockNumHandlers = [];
final StateMachineCodeGenerator codeGen;
ExceptionHandlerStack(this.codeGen);
void _pushTryCatch(TryCatch node) {
final catcher = _Catcher.fromTryCatch(
codeGen, node, codeGen.innerTargets[node.catches.first]!);
_handlers.add(catcher);
}
Finalizer _pushTryFinally(TryFinally node) {
final finalizer =
Finalizer._(codeGen, node, _nextFinalizer, codeGen.innerTargets[node]!);
_handlers.add(finalizer);
return finalizer;
}
void _pop() {
_handlers.removeLast();
}
int get numHandlers => _handlers.length;
int get coveredHandlers => _tryBlockNumHandlers.fold(0, (i1, i2) => i1 + i2);
int get _numFinalizers {
int i = 0;
for (final handler in _handlers) {
if (handler is Finalizer) {
i += 1;
}
}
return i;
}
Finalizer? get _nextFinalizer {
for (final handler in _handlers.reversed) {
if (handler is Finalizer) {
return handler;
}
}
return null;
}
void forEachFinalizer(
void Function(Finalizer finalizer, bool lastFinalizer) f) {
Finalizer? finalizer = _nextFinalizer;
while (finalizer != null) {
Finalizer? next = finalizer.parentFinalizer;
f(finalizer, next == null);
finalizer = next;
}
}
/// Generates Wasm `try` blocks for Dart `try` blocks wrapping the current
/// CFG block.
///
/// Call this when generating a new CFG block.
void _generateTryBlocks(w.InstructionsBuilder b) {
final handlersToCover = _handlers.length - coveredHandlers;
if (handlersToCover == 0) {
return;
}
b.try_();
_tryBlockNumHandlers.add(handlersToCover);
}
/// Terminates Wasm `try` blocks generated by [generateTryBlocks].
///
/// Call this right before terminating a CFG block.
void _terminateTryBlocks() {
int nextHandlerIdx = _handlers.length - 1;
final b = codeGen.b;
for (final int nCoveredHandlers in _tryBlockNumHandlers.reversed) {
final stackTraceLocal =
b.addLocal(codeGen.translator.stackTraceInfo.repr.nonNullableType);
final exceptionLocal =
b.addLocal(codeGen.translator.topInfo.nonNullableType);
void generateCatchBody() {
// Set continuations of finalizers that can be reached by this `catch`
// (or `catch_all`) as "rethrow".
for (int i = 0; i < nCoveredHandlers; i += 1) {
final handler = _handlers[nextHandlerIdx - i];
if (handler is Finalizer) {
handler.setContinuationRethrow(() => b.local_get(exceptionLocal),
() => b.local_get(stackTraceLocal));
}
}
// Set the untyped "current exception" variable. Catch blocks will do the
// type tests as necessary using this variable and set their exception
// and stack trace locals.
codeGen
.setSuspendStateCurrentException(() => b.local_get(exceptionLocal));
codeGen.setSuspendStateCurrentStackTrace(
() => b.local_get(stackTraceLocal));
codeGen._jumpToTarget(_handlers[nextHandlerIdx].target);
}
b.catch_(codeGen.translator.getExceptionTag(b.module));
b.local_set(stackTraceLocal);
b.local_set(exceptionLocal);
generateCatchBody();
// Generate a `catch_all` to catch JS exceptions if any of the covered
// handlers can catch JS exceptions.
bool canHandleJSExceptions = false;
for (int handlerIdx = nextHandlerIdx;
handlerIdx > nextHandlerIdx - nCoveredHandlers;
handlerIdx -= 1) {
final handler = _handlers[handlerIdx];
canHandleJSExceptions |= handler.canHandleJSExceptions;
}
if (canHandleJSExceptions) {
b.catch_all();
// We can't inspect the thrown object in a `catch_all` and get a stack
// trace, so we just attach the current stack trace.
codeGen.call(codeGen.translator.stackTraceCurrent.reference);
b.local_set(stackTraceLocal);
// We create a generic JavaScript error.
codeGen.call(codeGen.translator.javaScriptErrorFactory.reference);
b.local_set(exceptionLocal);
generateCatchBody();
}
b.end(); // end catch
nextHandlerIdx -= nCoveredHandlers;
}
_tryBlockNumHandlers.clear();
}
}
/// Represents an exception handler (`catch` or `finally`).
///
/// Note: for a [TryCatch] with multiple [Catch] blocks we jump to the first
/// [Catch] block on exception, which checks the exception type and jumps to
/// the next one if necessary.
abstract class _ExceptionHandler {
/// CFG block for the `catch` or `finally` block.
final StateTarget target;
_ExceptionHandler(this.target);
bool get canHandleJSExceptions;
}
class _Catcher extends _ExceptionHandler {
final List<VariableDeclaration> _exceptionVars = [];
final List<VariableDeclaration> _stackTraceVars = [];
final StateMachineCodeGenerator codeGen;
bool _canHandleJSExceptions = false;
_Catcher.fromTryCatch(this.codeGen, TryCatch node, super.target) {
for (Catch catch_ in node.catches) {
_exceptionVars.add(catch_.exception!);
_stackTraceVars.add(catch_.stackTrace!);
_canHandleJSExceptions |=
guardCanMatchJSException(codeGen.translator, catch_.guard);
}
}
@override
bool get canHandleJSExceptions => _canHandleJSExceptions;
void setException(void Function() pushException) {
for (final exceptionVar in _exceptionVars) {
codeGen.setVariable(exceptionVar, pushException);
}
}
void setStackTrace(void Function() pushStackTrace) {
for (final stackTraceVar in _stackTraceVars) {
codeGen.setVariable(stackTraceVar, pushStackTrace);
}
}
}
const int continuationFallthrough = 0;
const int continuationReturn = 1;
const int continuationRethrow = 2;
// For larger continuation values, `value - continuationJump` gives us the
// target block index to jump.
const int continuationJump = 3;
class Finalizer extends _ExceptionHandler {
final VariableDeclaration _continuationVar;
final VariableDeclaration _exceptionVar;
final VariableDeclaration _stackTraceVar;
final Finalizer? parentFinalizer;
final StateMachineCodeGenerator codeGen;
Finalizer._(this.codeGen, TryFinally node, this.parentFinalizer, super.target)
: _continuationVar =
(node.parent as Block).statements[0] as VariableDeclaration,
_exceptionVar =
(node.parent as Block).statements[1] as VariableDeclaration,
_stackTraceVar =
(node.parent as Block).statements[2] as VariableDeclaration;
@override
bool get canHandleJSExceptions => true;
void setContinuationFallthrough() {
codeGen.setVariable(_continuationVar, () {
codeGen.b.i64_const(continuationFallthrough);
});
}
void setContinuationReturn() {
codeGen.setVariable(_continuationVar, () {
codeGen.b.i64_const(continuationReturn);
});
}
void setContinuationRethrow(
void Function() pushException, void Function() pushStackTrace) {
codeGen.setVariable(_continuationVar, () {
codeGen.b.i64_const(continuationRethrow);
});
codeGen.setVariable(_exceptionVar, pushException);
codeGen.setVariable(_stackTraceVar, pushStackTrace);
}
void setContinuationJump(int index) {
codeGen.setVariable(_continuationVar, () {
codeGen.b.i64_const(index + continuationJump);
});
}
/// Push continuation of the finalizer block onto the stack as `i32`.
void pushContinuation() {
codeGen.visitVariableGet(VariableGet(_continuationVar), w.NumType.i64);
codeGen.b.i32_wrap_i64();
}
void pushException() {
codeGen._getVariable(_exceptionVar);
}
void pushStackTrace() {
codeGen._getVariable(_stackTraceVar);
}
}
/// Represents target of a `break` statement.
abstract class LabelTarget {
void jump(StateMachineCodeGenerator codeGen);
}
/// Target of a [BreakStatement] that can be implemented with a Wasm `br`
/// instruction.
///
/// This [LabelTarget] is used when the [LabeledStatement] is compiled using
/// the normal code generator (instead of async code generator).
class _DirectLabelTarget implements LabelTarget {
final w.Label label;
_DirectLabelTarget(this.label);
@override
void jump(StateMachineCodeGenerator codeGen) {
codeGen.b.br(label);
}
}
/// Target of a [BreakStatement] when the [LabeledStatement] is compiled to
/// CFG.
class _IndirectLabelTarget implements LabelTarget {
/// Number of finalizers wrapping the [LabeledStatement].
final int finalizerDepth;
/// CFG state for the [LabeledStatement] continuation.
final StateTarget stateTarget;
_IndirectLabelTarget(this.finalizerDepth, this.stateTarget);
@override
void jump(StateMachineCodeGenerator codeGen) {
final currentFinalizerDepth = codeGen.exceptionHandlers._numFinalizers;
final finalizersToRun = currentFinalizerDepth - finalizerDepth;
// Control flow overridden by a `break`, reset finalizer continuations.
var i = finalizersToRun;
codeGen.exceptionHandlers.forEachFinalizer((finalizer, last) {
if (i <= 0) {
// Finalizer won't be run by the `break`, reset continuation.
finalizer.setContinuationFallthrough();
} else {
// Finalizer will be run by the `break`. Each finalizer jumps to the
// next, last finalizer jumps to the `break` target.
finalizer.setContinuationJump(i == 1
? stateTarget.index
: finalizer.parentFinalizer!.target.index);
}
i -= 1;
});
if (finalizersToRun == 0) {
codeGen._jumpToTarget(stateTarget);
} else {
codeGen._jumpToTarget(codeGen.exceptionHandlers._nextFinalizer!.target);
}
}
}
/// Exception and stack trace variables of a [Catch] block. These variables are
/// used to get the exception and stack trace to throw when compiling
/// [Rethrow].
class CatchVariables {
final VariableDeclaration exception;
final VariableDeclaration stackTrace;
CatchVariables._(this.exception, this.stackTrace);
}
abstract class StateMachineEntryAstCodeGenerator extends AstCodeGenerator {
final w.FunctionBuilder function;
StateMachineEntryAstCodeGenerator(
Translator translator, Member enclosingMember, this.function)
: super(translator, function.type, enclosingMember);
/// Generate the outer function.
///
/// - Outer function: the `async` or `sync*` function.
///
/// In case of `async` this function should return a future.
///
/// In case of `sync*`, this function should return an iterable.
///
void generateOuter(
FunctionNode functionNode, Context? context, Source functionSource);
}
abstract class ProcedureStateMachineEntryCodeGenerator
extends StateMachineEntryAstCodeGenerator {
final Procedure member;
ProcedureStateMachineEntryCodeGenerator(
Translator translator, w.FunctionBuilder function, this.member)
: super(translator, member, function);
@override
void generateInternal() {
final source = member.enclosingComponent!.uriToSource[member.fileUri]!;
setSourceMapSource(source);
setSourceMapFileOffset(member.fileOffset);
closures = translator.getClosures(member);
// We don't support inlining state machine functions atm. Only when we
// inline and have call-site guarantees we would use the unchecked entry.
setupParametersAndContexts(member, useUncheckedEntry: false);
Context? context = closures.contexts[member.function];
if (context != null && context.isEmpty) context = context.parent;
generateOuter(member.function, context, source);
}
}
abstract class LambdaStateMachineEntryCodeGenerator
extends StateMachineEntryAstCodeGenerator {
final Lambda lambda;
LambdaStateMachineEntryCodeGenerator(Translator translator,
Member enclosingMember, this.lambda, Closures closures)
: super(translator, enclosingMember, lambda.function) {
this.closures = closures;
}
@override
void generateInternal() {
final source = lambda.functionNodeSource;
setSourceMapSource(source);
setSourceMapFileOffset(lambda.functionNode.fileOffset);
setupLambdaParametersAndContexts(lambda);
Context? context = closures.contexts[lambda.functionNode];
if (context != null && context.isEmpty) context = context.parent;
generateOuter(lambda.functionNode, context, source);
}
}
/// A [CodeGenerator] that compiles the function to a state machine based on
/// the suspension points in the function (`await` expressions and `yield`
/// statements).
///
/// This is used to compile `async` and `sync*` functions.
abstract class StateMachineCodeGenerator extends AstCodeGenerator {
final w.FunctionBuilder function;
final FunctionNode functionNode;
final Source functionSource;
StateMachineCodeGenerator(
Translator translator,
this.function,
Member enclosingMember,
this.functionNode,
this.functionSource,
Closures closures)
: super(translator, function.type, enclosingMember) {
this.closures = closures;
}
/// Targets of the CFG, indexed by target index.
late final List<StateTarget> targets;
// Targets categorized by placement and indexed by node.
final Map<TreeNode, StateTarget> innerTargets = {};
final Map<TreeNode, StateTarget> afterTargets = {};
/// The loop around the switch.
late final w.Label masterLoop;
/// The target labels of the switch, indexed by target index.
late final List<w.Label> labels;
/// The target index of the entry label for the current CFG node.
int currentTargetIndex = -1;
/// The local for the CFG target block index.
late final w.Local targetIndexLocal;
/// Exception handlers wrapping the current CFG block. Used to generate Wasm
/// `try` and `catch` blocks around the CFG blocks.
late final ExceptionHandlerStack exceptionHandlers;
/// Maps jump targets to their CFG targets. Used when jumping to a CFG block
/// on `break`. Keys are [LabeledStatement]s or [SwitchCase]s.
final Map<TreeNode, LabelTarget> labelTargets = {};
/// Current [Catch] block stack, used to compile [Rethrow].
///
/// Because there can be an `await` in a [Catch] block before a [Rethrow], we
/// can't compile [Rethrow] to Wasm `rethrow`. Instead we `throw` using the
/// [Rethrow]'s parent [Catch] block's exception and stack variables.
List<CatchVariables> catchVariableStack = [];
@override
void generateInternal() {
setSourceMapSource(functionSource);
setSourceMapFileOffset(functionNode.fileOffset);
// Number and categorize CFG targets.
targets = _YieldFinder(translator.options.enableAsserts).find(functionNode);
for (final target in targets) {
switch (target._placement) {
case _StateTargetPlacement.Inner:
innerTargets[target.node] = target;
break;
case _StateTargetPlacement.After:
afterTargets[target.node] = target;
break;
}
}
exceptionHandlers = ExceptionHandlerStack(this);
Context? context = closures.contexts[functionNode];
if (context != null && context.isEmpty) context = context.parent;
generateInner(functionNode, context);
}
/// Store the exception value emitted by [emitValue] in suspension state.
/// [getSuspendStateCurrentException] should return the value even after
/// suspending the function and continuing it later.
void setSuspendStateCurrentException(void Function() emitValue);
/// Get the value set by [setSuspendStateCurrentException].
void getSuspendStateCurrentException();
/// Same as [setSuspendStateCurrentException], but for the exception stack
/// trace.
void setSuspendStateCurrentStackTrace(void Function() emitValue);
/// Same as [getSuspendStateCurrentException], but for the exception stack
/// trace.
void getSuspendStateCurrentStackTrace();
/// Store the return value emitted by [emitValue] in suspension state.
/// [getSuspendStateReturnValue] should return the value ven after suspending
/// the function and continuing it later.
void setSuspendStateCurrentReturnValue(void Function() emitValue);
/// Get the value set by [setSuspendStateCurrentReturnValue].
void getSuspendStateCurrentReturnValue();
/// Generate a return from the function. For `async` functions, this should
/// call the completer and return. For `sync*`, this should terminate
/// iteration by returning `false`.
void emitReturn(void Function() emitValue);
/// Generate the inner functions.
///
/// - Inner function: the function that will be called for resumption.
void generateInner(FunctionNode functionNode, Context? context);
void emitTargetLabel(StateTarget target) {
currentTargetIndex++;
assert(
target.index == currentTargetIndex,
'target.index = ${target.index}, '
'currentTargetIndex = $currentTargetIndex, '
'target.node.location = ${target.node.location}');
exceptionHandlers._terminateTryBlocks();
b.end();
exceptionHandlers._generateTryBlocks(b);
}
void _jumpToTarget(StateTarget target,
{Expression? condition, bool negated = false}) {
if (condition == null && negated) return;
if (target.index > currentTargetIndex) {
// Forward jump directly to the label.
branchIf(condition, labels[target.index], negated: negated);
} else {
// Backward jump via the switch.
w.Label block = b.block();
branchIf(condition, block, negated: !negated);
b.i32_const(target.index);
b.local_set(targetIndexLocal);
b.br(masterLoop);
b.end(); // block
}
}
@override
void visitDoStatement(DoStatement node) {
StateTarget? inner = innerTargets[node];
if (inner == null) return super.visitDoStatement(node);
emitTargetLabel(inner);
allocateContext(node);
translateStatement(node.body);
_jumpToTarget(inner, condition: node.condition);
}
@override
void visitForStatement(ForStatement node) {
StateTarget? inner = innerTargets[node];
if (inner == null) return super.visitForStatement(node);
StateTarget after = afterTargets[node]!;
allocateContext(node);
for (VariableDeclaration variable in node.variables) {
translateStatement(variable);
}
emitTargetLabel(inner);
_jumpToTarget(after, condition: node.condition, negated: true);
translateStatement(node.body);
emitForStatementUpdate(node);
_jumpToTarget(inner);
emitTargetLabel(after);
}
@override
void visitIfStatement(IfStatement node) {
StateTarget? after = afterTargets[node];
if (after == null) return super.visitIfStatement(node);
StateTarget? inner = innerTargets[node];
_jumpToTarget(inner ?? after, condition: node.condition, negated: true);
translateStatement(node.then);
if (node.otherwise != null) {
_jumpToTarget(after);
emitTargetLabel(inner!);
translateStatement(node.otherwise!);
}
emitTargetLabel(after);
}
@override
void visitLabeledStatement(LabeledStatement node) {
StateTarget? after = afterTargets[node];
if (after == null) {
final w.Label label = b.block();
labelTargets[node] = _DirectLabelTarget(label);
translateStatement(node.body);
labelTargets.remove(node);
b.end();
} else {
labelTargets[node] =
_IndirectLabelTarget(exceptionHandlers._numFinalizers, after);
translateStatement(node.body);
labelTargets.remove(node);
emitTargetLabel(after);
}
}
@override
void visitBreakStatement(BreakStatement node) {
labelTargets[node.target]!.jump(this);
}
@override
void visitSwitchStatement(SwitchStatement node) {
StateTarget? after = afterTargets[node];
if (after == null) return super.visitSwitchStatement(node);
final switchInfo = SwitchInfo(this, node);
bool isNullable = dartTypeOf(node.expression).isPotentiallyNullable;
// Special cases
final SwitchCase? defaultCase = switchInfo.defaultCase;
final SwitchCase? nullCase = switchInfo.nullCase;
// When the type is nullable we use two variables: one for the nullable
// value, one after the null check, with non-nullable type.
w.Local switchValueNonNullableLocal = addLocal(switchInfo.nonNullableType);
w.Local? switchValueNullableLocal =
isNullable ? addLocal(switchInfo.nullableType) : null;
// Initialize switch value local
translateExpression(node.expression,
isNullable ? switchInfo.nullableType : switchInfo.nonNullableType);
b.local_set(
isNullable ? switchValueNullableLocal! : switchValueNonNullableLocal);
// Compute value and handle null
if (isNullable) {
final StateTarget nullTarget = nullCase != null
? innerTargets[nullCase]!
: defaultCase != null
? innerTargets[defaultCase]!
: after;
b.local_get(switchValueNullableLocal!);
b.ref_is_null();
b.if_();
_jumpToTarget(nullTarget);
b.end();
b.local_get(switchValueNullableLocal);
b.ref_as_non_null();
// Unbox if necessary
translator.convertType(
b, switchValueNullableLocal.type, switchValueNonNullableLocal.type);
b.local_set(switchValueNonNullableLocal);
}
// Compare against all case values
for (SwitchCase c in node.cases) {
for (Expression exp in c.expressions) {
if (exp is NullLiteral ||
exp is ConstantExpression && exp.constant is NullConstant) {
// Null already checked, skip
} else {
translateExpression(exp, switchInfo.nonNullableType);
b.local_get(switchValueNonNullableLocal);
switchInfo.compare(
switchValueNonNullableLocal,
() => translateExpression(exp, switchInfo.nonNullableType),
);
b.if_();
_jumpToTarget(innerTargets[c]!);
b.end();
}
}
}
// No explicit cases matched
if (node.isExplicitlyExhaustive) {
b.unreachable();
} else {
final StateTarget defaultLabel =
defaultCase != null ? innerTargets[defaultCase]! : after;
_jumpToTarget(defaultLabel);
}
// Add jump infos
for (final SwitchCase case_ in node.cases) {
labelTargets[case_] = _IndirectLabelTarget(
exceptionHandlers._numFinalizers, innerTargets[case_]!);
}
// Emit case bodies
for (SwitchCase c in node.cases) {
emitTargetLabel(innerTargets[c]!);
translateStatement(c.body);
_jumpToTarget(after);
}
// Remove jump infos
for (final SwitchCase case_ in node.cases) {
labelTargets.remove(case_);
}
emitTargetLabel(after);
}
@override
void visitContinueSwitchStatement(ContinueSwitchStatement node) {
final labelTarget = labelTargets[node.target];
// The CFE does not attach labeled statements to targets of
// ContinueSwitchStatement nodes. If the enclosing switch statement does not
// include an await or yield then the label target may not be recorded and
// so we should use the normal code generator.
if (labelTarget == null) return super.visitContinueSwitchStatement(node);
labelTarget.jump(this);
}
@override
void visitTryCatch(TryCatch node) {
StateTarget? after = afterTargets[node];
if (after == null) return super.visitTryCatch(node);
allocateContext(node);
for (Catch c in node.catches) {
if (c.exception != null) {
visitVariableDeclaration(c.exception!);
}
if (c.stackTrace != null) {
visitVariableDeclaration(c.stackTrace!);
}
}
exceptionHandlers._pushTryCatch(node);
exceptionHandlers._generateTryBlocks(b);
translateStatement(node.body);
_jumpToTarget(after);
exceptionHandlers._terminateTryBlocks();
exceptionHandlers._pop();
void emitCatchBlock(Catch catch_, Catch? nextCatch, bool emitGuard) {
if (emitGuard) {
getSuspendStateCurrentException();
b.ref_as_non_null();
types.emitIsTest(this, catch_.guard,
translator.coreTypes.objectNonNullableRawType, catch_.location);
b.i32_eqz();
// When generating guards we can't generate the catch body inside the
// `if` block for the guard as the catch body can have suspension
// points and generate target labels.
b.if_();
if (nextCatch != null) {
_jumpToTarget(innerTargets[nextCatch]!);
} else {
// Rethrow.
getSuspendStateCurrentException();
b.ref_as_non_null();
getSuspendStateCurrentStackTrace();
b.ref_as_non_null();
// TODO (omersa): When there is a finalizer we can jump to it
// directly, instead of via throw/catch. Would that be faster?
exceptionHandlers.forEachFinalizer(
(finalizer, last) => finalizer.setContinuationRethrow(
() => _getVariableBoxed(catch_.exception!),
() => _getVariable(catch_.stackTrace!),
));
b.throw_(translator.getExceptionTag(b.module));
}
b.end();
}
// Set exception and stack trace variables.
setVariable(catch_.exception!, () {
getSuspendStateCurrentException();
// Type test already passed, convert the exception.
translator.convertType(b, translator.topInfo.nullableType,
translator.translateType(catch_.exception!.type));
});
setVariable(catch_.stackTrace!, () => getSuspendStateCurrentStackTrace());
catchVariableStack
.add(CatchVariables._(catch_.exception!, catch_.stackTrace!));
translateStatement(catch_.body);
catchVariableStack.removeLast();
_jumpToTarget(after);
}
for (int catchIdx = 0; catchIdx < node.catches.length; catchIdx += 1) {
final Catch catch_ = node.catches[catchIdx];
final nextCatchIdx = catchIdx + 1;
final Catch? nextCatch = nextCatchIdx < node.catches.length
? node.catches[nextCatchIdx]
: null;
emitTargetLabel(innerTargets[catch_]!);
final bool shouldEmitGuard =
catch_.guard != translator.coreTypes.objectNonNullableRawType;
emitCatchBlock(catch_, nextCatch, shouldEmitGuard);
if (!shouldEmitGuard) {
break;
}
}
// Rethrow. Note that we don't override finalizer continuations here, they
// should be set by the original `throw` site.
getSuspendStateCurrentException();
b.ref_as_non_null();
getSuspendStateCurrentStackTrace();
b.ref_as_non_null();
b.throw_(translator.getExceptionTag(b.module));
emitTargetLabel(after);
}
@override
void visitTryFinally(TryFinally node) {
allocateContext(node);
final StateTarget finalizerTarget = innerTargets[node]!;
final StateTarget fallthroughContinuationTarget = afterTargets[node]!;
// Body
final finalizer = exceptionHandlers._pushTryFinally(node);
exceptionHandlers._generateTryBlocks(b);
translateStatement(node.body);
// Set continuation of the finalizer.
finalizer.setContinuationFallthrough();
_jumpToTarget(finalizerTarget);
exceptionHandlers._terminateTryBlocks();
exceptionHandlers._pop();
// Finalizer
{
emitTargetLabel(finalizerTarget);
translateStatement(node.finalizer);
// Check continuation.
// Fallthrough
assert(continuationFallthrough == 0); // update eqz below if changed
finalizer.pushContinuation();
b.i32_eqz();
b.if_();
_jumpToTarget(fallthroughContinuationTarget);
b.end();
// Return
finalizer.pushContinuation();
b.i32_const(continuationReturn);
b.i32_eq();
b.if_();
emitReturn(() => getSuspendStateCurrentReturnValue());
b.end();
// Rethrow
finalizer.pushContinuation();
b.i32_const(continuationRethrow);
b.i32_eq();
b.if_();
finalizer.pushException();
b.ref_as_non_null();
finalizer.pushStackTrace();
b.ref_as_non_null();
b.throw_(translator.getExceptionTag(b.module));
b.end();
// Any other value: jump to the target.
finalizer.pushContinuation();
b.i32_const(continuationJump);
b.i32_sub();
b.local_set(targetIndexLocal);
b.br(masterLoop);
}
emitTargetLabel(fallthroughContinuationTarget);
}
@override
void visitWhileStatement(WhileStatement node) {
StateTarget? inner = innerTargets[node];
if (inner == null) return super.visitWhileStatement(node);
StateTarget after = afterTargets[node]!;
allocateContext(node);
emitTargetLabel(inner);
_jumpToTarget(after, condition: node.condition, negated: true);
translateStatement(node.body);
_jumpToTarget(inner);
emitTargetLabel(after);
}
@override
void visitYieldStatement(YieldStatement node) {
// This should be overriddenin `sync*` code generator.
throw 'Unexpected yield statement: $node (${node.location})';
}
@override
void visitReturnStatement(ReturnStatement node) {
final Finalizer? firstFinalizer = exceptionHandlers._nextFinalizer;
final value = node.expression;
if (firstFinalizer == null) {
emitReturn(() {
if (value == null) {
b.ref_null(translator.topInfo.struct);
} else {
translateExpression(value, translator.topInfo.nullableType);
}
});
return;
}
if (value == null) {
b.ref_null(translator.topInfo.struct);
} else {
translateExpression(value, translator.topInfo.nullableType);
}
final returnValueLocal = addLocal(translator.topInfo.nullableType);
b.local_set(returnValueLocal);
// Set return value for the last finalizer to return.
setSuspendStateCurrentReturnValue(() => b.local_get(returnValueLocal));
// Update continuation variables of finalizers. Last finalizer returns
// the value.
exceptionHandlers.forEachFinalizer((finalizer, last) {
if (last) {
finalizer.setContinuationReturn();
} else {
finalizer.setContinuationJump(finalizer.parentFinalizer!.target.index);
}
});
// Jump to the first finalizer
_jumpToTarget(firstFinalizer.target);
}
@override
w.ValueType visitThrow(Throw node, w.ValueType expectedType) {
final exceptionLocal = addLocal(translator.topInfo.nonNullableType);
translateExpression(node.expression, translator.topInfo.nonNullableType);
b.local_set(exceptionLocal);
final stackTraceLocal =
addLocal(translator.stackTraceInfo.repr.nonNullableType);
call(translator.stackTraceCurrent.reference);
b.local_set(stackTraceLocal);
exceptionHandlers.forEachFinalizer((finalizer, last) {
finalizer.setContinuationRethrow(() => b.local_get(exceptionLocal),
() => b.local_get(stackTraceLocal));
});
// TODO (omersa): An alternative would be to directly jump to the parent
// handler, or call `completeOnError` if we're not in a try-catch or
// try-finally. Would that be more efficient?
b.local_get(exceptionLocal);
b.local_get(stackTraceLocal);
call(translator.errorThrow.reference);
b.unreachable();
return expectedType;
}
@override
w.ValueType visitRethrow(Rethrow node, w.ValueType expectedType) {
final catchVars = catchVariableStack.last;
exceptionHandlers.forEachFinalizer((finalizer, last) {
finalizer.setContinuationRethrow(
() => _getVariableBoxed(catchVars.exception),
() => _getVariable(catchVars.stackTrace),
);
});
// TODO (omersa): Similar to `throw` compilation above, we could directly
// jump to the target block or call `completeOnError`.
getSuspendStateCurrentException();
b.ref_as_non_null();
getSuspendStateCurrentStackTrace();
b.ref_as_non_null();
b.throw_(translator.getExceptionTag(b.module));
b.unreachable();
return expectedType;
}
/// Similar to the [VariableSet] visitor, but the value is pushed to the
/// stack by the callback [pushValue].
void setVariable(VariableDeclaration variable, void Function() pushValue) {
final w.Local? local = locals[variable];
final Capture? capture = closures.captures[variable];
if (capture != null) {
assert(capture.written);
b.local_get(capture.context.currentLocal);
pushValue();
b.struct_set(capture.context.struct, capture.fieldIndex);
} else {
if (local == null) {
throw "Write of undefined variable $variable";
}
pushValue();
b.local_set(local);
}
}
w.ValueType _getVariable(VariableDeclaration variable) {
final w.Local? local = locals[variable];
final Capture? capture = closures.captures[variable];
if (capture != null) {
if (!capture.written && local != null) {
b.local_get(local);
return local.type;
} else {
b.local_get(capture.context.currentLocal);
b.struct_get(capture.context.struct, capture.fieldIndex);
return capture.context.struct.fields[capture.fieldIndex].type.unpacked;
}
} else {
if (local == null) {
throw "Write of undefined variable $variable";
}
b.local_get(local);
return local.type;
}
}
/// Same as [_getVariable], but boxes the value if it's not already boxed.
void _getVariableBoxed(VariableDeclaration variable) {
final varType = _getVariable(variable);
translator.convertType(b, varType, translator.topInfo.nullableType);
}
}