diff --git a/pkg/dart2wasm/README.md b/pkg/dart2wasm/README.md index cb1cce1d316..3e0c04c102b 100644 --- a/pkg/dart2wasm/README.md +++ b/pkg/dart2wasm/README.md @@ -44,7 +44,7 @@ where *options* include: Dart2Wasm will output a `wasm` file, containing Dart compiled to Wasm, as well as an `mjs` file containing the runtime. The result can be run with: -`d8 --experimental-wasm-gc --experimental-wasm-stack-switching --experimental-wasm-type-reflection pkg/dart2wasm/bin/run_wasm.js -- `*outfile*`.wasm` /abs/path/to/`*outfile*`.mjs +`d8 --experimental-wasm-gc --experimental-wasm-type-reflection pkg/dart2wasm/bin/run_wasm.js -- `*outfile*`.wasm` /abs/path/to/`*outfile*`.mjs Where `d8` is the [V8 developer shell](https://v8.dev/docs/d8). diff --git a/pkg/dart2wasm/bin/run_wasm.js b/pkg/dart2wasm/bin/run_wasm.js index 19b61cd1705..bd8c692525b 100644 --- a/pkg/dart2wasm/bin/run_wasm.js +++ b/pkg/dart2wasm/bin/run_wasm.js @@ -6,7 +6,7 @@ // // Run as follows: // -// $> d8 --experimental-wasm-gc --experimental-wasm-stack-switching \ +// $> d8 --experimental-wasm-gc \ // --experimental-wasm-type-reflection run_wasm.js \ // -- /abs/path/to/.mjs .wasm [.wasm] \ // [-- Dart commandline arguments...] diff --git a/pkg/dart2wasm/lib/async.dart b/pkg/dart2wasm/lib/async.dart new file mode 100644 index 00000000000..5ae734be1bc --- /dev/null +++ b/pkg/dart2wasm/lib/async.dart @@ -0,0 +1,1459 @@ +// Copyright (c) 2023, 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:dart2wasm/class_info.dart'; +import 'package:dart2wasm/closures.dart'; +import 'package:dart2wasm/code_generator.dart'; +import 'package:dart2wasm/sync_star.dart' + show StateTarget, StateTargetPlacement; + +import 'package:kernel/ast.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +/// Identify which statements contain `await` 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 targets = []; + final bool enableAsserts; + + // The number of `await` statements seen so far. + int yieldCount = 0; + + _YieldFinder(this.enableAsserts); + + List 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 this.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 += 1; + 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 += 1; + 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) { + throw 'Yield statement in async function: $node (${node.location})'; + } + + // 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 visitVariableSet(VariableSet node) { + if (node.value is AwaitExpression) { + yieldCount++; + addTarget(node, StateTargetPlacement.After); + } else { + super.visitVariableSet(node); + } + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + if (node.expression is AwaitExpression) { + yieldCount++; + addTarget(node, StateTargetPlacement.After); + } else { + 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) { + 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 last handler, which then jumps to the + /// next if the exception type test fails. + /// + /// Because the 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 _tryBlockNumHandlers = []; + + final AsyncCodeGenerator 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 f(Finalizer finalizer, bool lastFinalizer)) { + 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.Instructions 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 handlerIdx = _handlers.length - 1; + while (_tryBlockNumHandlers.isNotEmpty) { + int nCoveredHandlers = _tryBlockNumHandlers.removeLast(); + + codeGen.b.catch_(codeGen.translator.exceptionTag); + + final stackTraceLocal = + codeGen.addLocal(codeGen.translator.stackTraceInfo.nonNullableType); + codeGen.b.local_set(stackTraceLocal); + + final exceptionLocal = + codeGen.addLocal(codeGen.translator.topInfo.nonNullableType); + codeGen.b.local_set(exceptionLocal); + + final nextHandler = _handlers[handlerIdx]; + + while (nCoveredHandlers != 0) { + final handler = _handlers[handlerIdx]; + handlerIdx -= 1; + if (handler is Finalizer) { + handler.setContinuationRethrow( + () => codeGen.b.local_get(exceptionLocal), + () => codeGen.b.local_get(stackTraceLocal)); + } + nCoveredHandlers -= 1; + } + + // 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._setCurrentException(() => codeGen.b.local_get(exceptionLocal)); + codeGen._setCurrentExceptionStackTrace( + () => codeGen.b.local_get(stackTraceLocal)); + + codeGen.jumpToTarget(nextHandler.target); + + codeGen.b.end(); // end catch + } + } +} + +/// 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); +} + +class Catcher extends _ExceptionHandler { + final List _exceptionVars = []; + final List _stackTraceVars = []; + final AsyncCodeGenerator codeGen; + + Catcher.fromTryCatch(this.codeGen, TryCatch node, super.target) { + for (Catch catch_ in node.catches) { + _exceptionVars.add(catch_.exception!); + _stackTraceVars.add(catch_.stackTrace!); + } + } + + void setException(void pushException()) { + for (final exceptionVar in _exceptionVars) { + codeGen._setVariable(exceptionVar, pushException); + } + } + + void setStackTrace(void 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 AsyncCodeGenerator 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; + + void setContinuationFallthrough() { + codeGen._setVariable(_continuationVar, () { + codeGen.b.i64_const(continuationFallthrough); + }); + } + + void setContinuationReturn() { + codeGen._setVariable(_continuationVar, () { + codeGen.b.i64_const(continuationReturn); + }); + } + + void setContinuationRethrow(void pushException(), void 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(AsyncCodeGenerator codeGen, BreakStatement node); +} + +/// 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(AsyncCodeGenerator codeGen, BreakStatement node) { + 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(AsyncCodeGenerator codeGen, BreakStatement node) { + 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); +} + +class AsyncCodeGenerator extends CodeGenerator { + AsyncCodeGenerator(super.translator, super.function, super.reference); + + /// Targets of the CFG, indexed by target index. + late final List targets; + + // Targets categorized by placement and indexed by node. + final Map innerTargets = {}; + final Map afterTargets = {}; + + /// The loop around the switch. + late final w.Label masterLoop; + + /// The target labels of the switch, indexed by target index. + late final List labels; + + /// The target index of the entry label for the current CFG node. + int currentTargetIndex = -1; + + /// The local in the inner function for the async state, with type + /// `ref _AsyncSuspendState`. + late final w.Local suspendStateLocal; + + /// The local in the inner function for the value of the last awaited future, + /// with type `ref null #Top`. + late final w.Local awaitValueLocal; + + /// 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 labelled statements to their CFG targets. Used when jumping to a CFG + /// block on `break`. + final Map labelTargets = {}; + + late final ClassInfo asyncSuspendStateInfo = + translator.classInfo[translator.asyncSuspendStateClass]!; + + /// 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 catchVariableStack = []; + + @override + void generate() { + closures = Closures(this); + setupParametersAndContexts(member); + generateTypeChecks(member.function!.typeParameters, member.function!, + translator.paramInfoFor(reference)); + _generateBodies(member.function!); + } + + @override + w.DefinedFunction generateLambda(Lambda lambda, Closures closures) { + this.closures = closures; + setupLambdaParametersAndContexts(lambda); + _generateBodies(lambda.functionNode); + return function; + } + + void _generateBodies(FunctionNode functionNode) { + // 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); + + // Wasm function containing the body of the `async` function + // (`_AyncResumeFun`). + final w.DefinedFunction resumeFun = m.addFunction( + m.addFunctionType([ + asyncSuspendStateInfo.nonNullableType, // _AsyncSuspendState + translator.topInfo.nullableType, // Object?, await value + translator.topInfo.nullableType, // Object?, error value + translator + .stackTraceInfo.nullableType // StackTrace?, error stack trace + ], [ + // Inner function does not return a value, but it's Dart type is + // `void Function(...)` and all Dart functions return a value, so we + // add a return type. + translator.topInfo.nullableType + ]), + "${function.functionName} inner"); + + Context? context = closures.contexts[functionNode]; + if (context != null && context.isEmpty) context = context.parent; + + _generateOuter(functionNode, context, resumeFun); + + // Forget about the outer function locals containing the type arguments, + // so accesses to the type arguments in the inner function will fetch them + // from the context. + typeLocals.clear(); + + _generateInner(functionNode, context, resumeFun); + } + + void _generateOuter(FunctionNode functionNode, Context? context, + w.DefinedFunction resumeFun) { + // Outer (wrapper) function creates async state, calls the inner function + // (which runs until first suspension point, i.e. `await`), and returns the + // completer's future. + + // (1) Create async state. + + final asyncStateLocal = function + .addLocal(w.RefType(asyncSuspendStateInfo.struct, nullable: false)); + + // AsyncResumeFun _resume + b.global_get(translator.makeFunctionRef(resumeFun)); + + // WasmStructRef? _context + if (context != null) { + assert(!context.isEmpty); + b.local_get(context.currentLocal); + } else { + b.ref_null(w.HeapType.struct); + } + + // _AsyncCompleter _completer + final DartType returnType = functionNode.returnType; + final DartType innerType = returnType is InterfaceType && + returnType.classNode == translator.coreTypes.futureClass + ? returnType.typeArguments.single + : const DynamicType(); + types.makeType(this, innerType); + b.call(translator.functions + .getFunction(translator.makeAsyncCompleter.reference)); + + // Allocate `_AsyncSuspendState` + b.call(translator.functions + .getFunction(translator.newAsyncSuspendState.reference)); + b.local_set(asyncStateLocal); + + // (2) Call inner function. + // + // Note: the inner function does not throw, so we don't need a `try` block + // here. + + b.local_get(asyncStateLocal); + b.ref_null(translator.topInfo.struct); // await value + b.ref_null(translator.topInfo.struct); // error value + b.ref_null(translator.stackTraceInfo.struct); // stack trace + b.call(resumeFun); + b.drop(); // drop null + + // (3) Return the completer's future. + + b.local_get(asyncStateLocal); + final completerFutureGetter = translator.functions + .getFunction(translator.completerFuture.getterReference); + b.struct_get( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateCompleter); + translator.convertType( + function, + asyncSuspendStateInfo.struct.fields[5].type.unpacked, + completerFutureGetter.type.inputs[0]); + b.call(completerFutureGetter); + b.end(); + } + + /// Clones the context pointed to by the [srcContext] local. Returns a local + /// pointing to the cloned context. + /// + /// It is assumed that the context is the function-level context for the + /// `async` function. + w.Local _cloneContext( + FunctionNode functionNode, Context context, w.Local srcContext) { + assert(context.owner == functionNode); + + final w.Local destContext = addLocal(context.currentLocal.type); + b.struct_new_default(context.struct); + b.local_set(destContext); + + void copyCapture(TreeNode node) { + Capture? capture = closures.captures[node]; + if (capture != null) { + assert(capture.context == context); + b.local_get(destContext); + b.local_get(srcContext); + b.struct_get(context.struct, capture.fieldIndex); + b.struct_set(context.struct, capture.fieldIndex); + } + } + + if (context.containsThis) { + b.local_get(destContext); + b.local_get(srcContext); + b.struct_get(context.struct, context.thisFieldIndex); + b.struct_set(context.struct, context.thisFieldIndex); + } + if (context.parent != null) { + b.local_get(destContext); + b.local_get(srcContext); + b.struct_get(context.struct, context.parentFieldIndex); + b.struct_set(context.struct, context.parentFieldIndex); + } + functionNode.positionalParameters.forEach(copyCapture); + functionNode.namedParameters.forEach(copyCapture); + functionNode.typeParameters.forEach(copyCapture); + + return destContext; + } + + void _generateInner(FunctionNode functionNode, Context? context, + w.DefinedFunction resumeFun) { + // void Function(_AsyncSuspendState, Object?) + + // Set the current Wasm function for the code generator to the inner + // function of the `async`, which is to contain the body. + function = resumeFun; + + suspendStateLocal = function.locals[0]; // ref _AsyncSuspendState + awaitValueLocal = function.locals[1]; // ref null #Top + + // Set up locals for contexts and `this`. + thisLocal = null; + Context? localContext = context; + while (localContext != null) { + if (!localContext.isEmpty) { + localContext.currentLocal = function + .addLocal(w.RefType.def(localContext.struct, nullable: true)); + if (localContext.containsThis) { + assert(thisLocal == null); + thisLocal = function.addLocal(localContext + .struct.fields[localContext.thisFieldIndex].type.unpacked + .withNullability(false)); + translator.globals.instantiateDummyValue(b, thisLocal!.type); + b.local_set(thisLocal!); + + preciseThisLocal = thisLocal; + } + } + localContext = localContext.parent; + } + + // Read target index from the suspend state. + targetIndexLocal = addLocal(w.NumType.i32); + b.local_get(suspendStateLocal); + b.struct_get( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateTargetIndex); + b.local_set(targetIndexLocal); + + // The outer `try` block calls `completeOnError` on exceptions. + b.try_(); + + // Switch on the target index. + masterLoop = b.loop(const [], const []); + labels = List.generate(targets.length, (_) => b.block()).reversed.toList(); + w.Label defaultLabel = b.block(); + b.local_get(targetIndexLocal); + b.br_table(labels, defaultLabel); + b.end(); // defaultLabel + b.unreachable(); + + // Initial state + final StateTarget initialTarget = targets.first; + _emitTargetLabel(initialTarget); + + // Clone context on first execution. + _restoreContextsAndThis(context, cloneContextFor: functionNode); + + visitStatement(functionNode.body!); + + // Final state: return. + _emitTargetLabel(targets.last); + b.local_get(suspendStateLocal); + b.struct_get( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateCompleter); + // Non-null Dart field represented as nullable Wasm field. + b.ref_as_non_null(); + b.ref_null(translator.topInfo.struct); + b.call(translator.functions + .getFunction(translator.completerComplete.reference)); + b.return_(); + b.end(); // masterLoop + + b.catch_(translator.exceptionTag); + + final stackTraceLocal = addLocal(translator.stackTraceInfo.nonNullableType); + b.local_set(stackTraceLocal); + + final exceptionLocal = addLocal(translator.topInfo.nonNullableType); + b.local_set(exceptionLocal); + + b.local_get(suspendStateLocal); + b.struct_get( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateCompleter); + b.ref_as_non_null(); + b.local_get(exceptionLocal); + b.local_get(stackTraceLocal); + b.call(translator.functions + .getFunction(translator.completerCompleteError.reference)); + b.return_(); + + b.end(); // end try + + b.unreachable(); + b.end(); + } + + // Note: These two locals are only available in "inner" functions. + w.Local get pendingExceptionLocal => function.locals[2]; + w.Local get pendingStackTraceLocal => function.locals[3]; + + 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 + } + } + + void _restoreContextsAndThis(Context? context, + {FunctionNode? cloneContextFor}) { + if (context != null) { + assert(!context.isEmpty); + b.local_get(suspendStateLocal); + b.struct_get( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateContext); + b.ref_cast(context.currentLocal.type as w.RefType); + b.local_set(context.currentLocal); + + if (context.owner == cloneContextFor) { + context.currentLocal = + _cloneContext(cloneContextFor!, context, context.currentLocal); + } + + while (context!.parent != null) { + assert(!context.parent!.isEmpty); + b.local_get(context.currentLocal); + b.struct_get(context.struct, context.parentFieldIndex); + b.ref_as_non_null(); + context = context.parent!; + b.local_set(context.currentLocal); + } + if (context.containsThis) { + b.local_get(context.currentLocal); + b.struct_get(context.struct, context.thisFieldIndex); + b.ref_as_non_null(); + b.local_set(thisLocal!); + } + } + } + + @override + void visitDoStatement(DoStatement node) { + StateTarget? inner = innerTargets[node]; + if (inner == null) return super.visitDoStatement(node); + + _emitTargetLabel(inner); + allocateContext(node); + visitStatement(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) { + visitStatement(variable); + } + _emitTargetLabel(inner); + jumpToTarget(after, condition: node.condition, negated: true); + visitStatement(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); + visitStatement(node.then); + if (node.otherwise != null) { + jumpToTarget(after); + _emitTargetLabel(inner!); + visitStatement(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); + visitStatement(node.body); + labelTargets.remove(node); + b.end(); + } else { + labelTargets[node] = + IndirectLabelTarget(exceptionHandlers.numFinalizers, after); + visitStatement(node.body); + labelTargets.remove(node); + _emitTargetLabel(after); + } + } + + @override + void visitBreakStatement(BreakStatement node) { + labelTargets[node.target]!.jump(this, node); + } + + @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 + wrap(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(function, 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 { + wrap(exp, switchInfo.nonNullableType); + b.local_get(switchValueNonNullableLocal); + switchInfo.compare(); + 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); + } + + // Emit case bodies + for (SwitchCase c in node.cases) { + _emitTargetLabel(innerTargets[c]!); + visitStatement(c.body); + jumpToTarget(after); + } + + _emitTargetLabel(after); + } + + @override + void visitContinueSwitchStatement(ContinueSwitchStatement node) { + jumpToTarget(innerTargets[node.target]!); + } + + @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); + visitStatement(node.body); + jumpToTarget(after); + exceptionHandlers.terminateTryBlocks(); + exceptionHandlers.pop(); + + void emitCatchBlock(Catch catch_, Catch? nextCatch, bool emitGuard) { + if (emitGuard) { + _getCurrentException(); + b.ref_as_non_null(); + types.emitTypeTest( + this, catch_.guard, translator.coreTypes.objectNonNullableRawType); + 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. + _getCurrentException(); + b.ref_as_non_null(); + _getCurrentExceptionStackTrace(); + 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( + () => _getVariable(catch_.exception!), + () => _getVariable(catch_.stackTrace!), + )); + b.throw_(translator.exceptionTag); + } + b.end(); + } + + // Set exception and stack trace variables. + _setVariable(catch_.exception!, () { + _getCurrentException(); + // Type test already passed, convert the exception. + translator.convertType( + function, + asyncSuspendStateInfo + .struct + .fields[FieldIndex.asyncSuspendStateCurrentException] + .type + .unpacked, + translator.translateType(catch_.exception!.type)); + }); + _setVariable(catch_.stackTrace!, () => _getCurrentExceptionStackTrace()); + + catchVariableStack + .add(CatchVariables(catch_.exception!, catch_.stackTrace!)); + + visitStatement(catch_.body); + + catchVariableStack.removeLast(); + + jumpToTarget(after); + } + + for (int catchIdx = 0; catchIdx < node.catches.length; catchIdx += 1) { + final Catch catch_ = node.catches[catchIdx]; + final Catch? nextCatch = + node.catches.length < catchIdx ? node.catches[catchIdx + 1] : 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. + _getCurrentException(); + b.ref_as_non_null(); + _getCurrentExceptionStackTrace(); + b.ref_as_non_null(); + b.throw_(translator.exceptionTag); + + _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); + visitStatement(node.body); + + // Set continuation of the finalizer. + finalizer.setContinuationFallthrough(); + + jumpToTarget(finalizerTarget); + exceptionHandlers.terminateTryBlocks(); + exceptionHandlers.pop(); + + // Finalizer + { + _emitTargetLabel(finalizerTarget); + visitStatement(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_(); + b.local_get(suspendStateLocal); + b.struct_get( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateCompleter); + // Non-null Dart field represented as nullable Wasm field. + b.ref_as_non_null(); + b.local_get(suspendStateLocal); + b.struct_get(asyncSuspendStateInfo.struct, + FieldIndex.asyncSuspendStateCurrentReturnValue); + b.call(translator.functions + .getFunction(translator.completerComplete.reference)); + b.return_(); + 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.exceptionTag); + 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]!; + + _emitTargetLabel(inner); + jumpToTarget(after, condition: node.condition, negated: true); + allocateContext(node); + visitStatement(node.body); + jumpToTarget(inner); + _emitTargetLabel(after); + } + + @override + void visitYieldStatement(YieldStatement node) { + throw 'Yield statement in async function: $node (${node.location})'; + } + + @override + void visitReturnStatement(ReturnStatement node) { + final Finalizer? firstFinalizer = exceptionHandlers.nextFinalizer; + + if (firstFinalizer == null) { + b.local_get(suspendStateLocal); + b.struct_get( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateCompleter); + // Non-null Dart field represented as nullable Wasm field. + b.ref_as_non_null(); + } + + final value = node.expression; + if (value == null) { + b.ref_null(translator.topInfo.struct); + } else { + wrap(value, translator.topInfo.nullableType); + } + + if (firstFinalizer == null) { + b.call(translator.functions + .getFunction(translator.completerComplete.reference)); + b.return_(); + } else { + final returnValueLocal = addLocal(translator.topInfo.nullableType); + b.local_set(returnValueLocal); + + // Set return value + b.local_get(suspendStateLocal); + b.local_get(returnValueLocal); + b.struct_set(asyncSuspendStateInfo.struct, + FieldIndex.asyncSuspendStateCurrentReturnValue); + + // 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); + wrap(node.expression, translator.topInfo.nonNullableType); + b.local_set(exceptionLocal); + + final stackTraceLocal = addLocal(translator.stackTraceInfo.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); + b.throw_(translator.exceptionTag); + + b.unreachable(); + return expectedType; + } + + @override + w.ValueType visitRethrow(Rethrow node, w.ValueType expectedType) { + final catchVars = catchVariableStack.last; + + exceptionHandlers.forEachFinalizer((finalizer, _last) { + finalizer.setContinuationRethrow( + () => _getVariable(catchVars.exception), + () => _getVariable(catchVars.stackTrace), + ); + }); + + // TODO (omersa): Similar to `throw` compilation above, we could directly + // jump to the target block or call `completeOnError`. + _getCurrentException(); + b.ref_as_non_null(); + _getCurrentExceptionStackTrace(); + b.ref_as_non_null(); + b.throw_(translator.exceptionTag); + b.unreachable(); + return expectedType; + } + + // Handle awaits + @override + void visitExpressionStatement(ExpressionStatement node) { + final expression = node.expression; + if (expression is VariableSet) { + final value = expression.value; + if (value is AwaitExpression) { + _generateAwait(value, expression.variable); + return; + } + } + + super.visitExpressionStatement(node); + } + + void _generateAwait(AwaitExpression node, VariableDeclaration awaitValueVar) { + // Find the current context. + Context? context; + TreeNode contextOwner = node; + do { + contextOwner = contextOwner.parent!; + context = closures.contexts[contextOwner]; + } while ( + contextOwner.parent != null && (context == null || context.isEmpty)); + + // Store context. + if (context != null) { + assert(!context.isEmpty); + b.local_get(suspendStateLocal); + b.local_get(context.currentLocal); + b.struct_set( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateContext); + } + + // Set state target to label after await. + final StateTarget after = afterTargets[node.parent]!; + b.local_get(suspendStateLocal); + b.i32_const(after.index); + b.struct_set( + asyncSuspendStateInfo.struct, FieldIndex.asyncSuspendStateTargetIndex); + + b.local_get(suspendStateLocal); + wrap(node.operand, translator.topInfo.nullableType); + b.call(translator.functions.getFunction(translator.awaitHelper.reference)); + b.return_(); + + // Generate resume label + _emitTargetLabel(after); + + _restoreContextsAndThis(context); + + // Handle exceptions + final exceptionBlock = b.block(); + b.local_get(pendingExceptionLocal); + b.br_on_null(exceptionBlock); + + exceptionHandlers.forEachFinalizer((finalizer, _last) { + finalizer.setContinuationRethrow(() { + b.local_get(pendingExceptionLocal); + b.ref_as_non_null(); + }, () => b.local_get(pendingStackTraceLocal)); + }); + + b.local_get(pendingStackTraceLocal); + b.ref_as_non_null(); + + b.throw_(translator.exceptionTag); + b.end(); // exceptionBlock + + _setVariable(awaitValueVar, () { + b.local_get(awaitValueLocal); + translator.convertType( + function, awaitValueLocal.type, translateType(awaitValueVar.type)); + }); + } + + void _setVariable(VariableDeclaration variable, void 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); + } + } + + void _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); + } else { + b.local_get(capture.context.currentLocal); + b.struct_get(capture.context.struct, capture.fieldIndex); + } + } else { + if (local == null) { + throw "Write of undefined variable ${variable}"; + } + b.local_get(local); + } + } + + void _getCurrentException() { + b.local_get(suspendStateLocal); + b.struct_get(asyncSuspendStateInfo.struct, + FieldIndex.asyncSuspendStateCurrentException); + } + + void _setCurrentException(void Function() emitValue) { + b.local_get(suspendStateLocal); + emitValue(); + b.struct_set(asyncSuspendStateInfo.struct, + FieldIndex.asyncSuspendStateCurrentException); + } + + void _getCurrentExceptionStackTrace() { + b.local_get(suspendStateLocal); + b.struct_get(asyncSuspendStateInfo.struct, + FieldIndex.asyncSuspendStateCurrentExceptionStackTrace); + } + + void _setCurrentExceptionStackTrace(void Function() emitValue) { + b.local_get(suspendStateLocal); + emitValue(); + b.struct_set(asyncSuspendStateInfo.struct, + FieldIndex.asyncSuspendStateCurrentExceptionStackTrace); + } +} diff --git a/pkg/dart2wasm/lib/await_transformer.dart b/pkg/dart2wasm/lib/await_transformer.dart new file mode 100644 index 00000000000..f0ebf611ee2 --- /dev/null +++ b/pkg/dart2wasm/lib/await_transformer.dart @@ -0,0 +1,1277 @@ +// Copyright (c) 2023, 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:dart2wasm/async.dart' as asyncCodeGen; + +import 'package:kernel/ast.dart'; +import 'package:kernel/class_hierarchy.dart'; +import 'package:kernel/core_types.dart'; +import 'package:kernel/type_environment.dart'; + +/// This pass lifts `await` expressions to the top-level. After the pass, all +/// `await` expressions will have the form: +/// +/// final $temp = await ; +/// +/// where `` is an expression without `await`. +/// +/// `await`s in block expressions are also lifted to the statement level. +/// +/// The idea is that after this pass every `await` will have a simple +/// continuation of "assign the awaited value to the variable, continue with +/// the next statement". This allows simple code generation for async inner +/// functions. +/// +/// The implementation is mostly copied from the old VM async/await transformer +/// with some changes. The old pass was removed in commit 94c120a. +void transformLibraries( + List libraries, ClassHierarchy hierarchy, CoreTypes coreTypes) { + final typeEnv = TypeEnvironment(coreTypes, hierarchy); + + var rewriter = + _AwaitTransformer(StatefulStaticTypeContext.stacked(typeEnv), coreTypes); + + for (var library in libraries) { + rewriter.transform(library); + } +} + +class _AwaitTransformer extends Transformer { + final StatefulStaticTypeContext staticTypeContext; + + final CoreTypes coreTypes; + + List statements = []; + + late final _ExpressionTransformer expressionTransformer; + + _AwaitTransformer(this.staticTypeContext, this.coreTypes) { + expressionTransformer = + _ExpressionTransformer(this, staticTypeContext, coreTypes); + } + + @override + TreeNode visitField(Field node) { + staticTypeContext.enterMember(node); + super.visitField(node); + staticTypeContext.leaveMember(node); + return node; + } + + @override + TreeNode visitConstructor(Constructor node) { + staticTypeContext.enterMember(node); + final result = super.visitConstructor(node); + staticTypeContext.leaveMember(node); + return result; + } + + @override + TreeNode visitProcedure(Procedure node) { + staticTypeContext.enterMember(node); + final result = node.isAbstract ? node : super.visitProcedure(node); + staticTypeContext.leaveMember(node); + return result; + } + + @override + TreeNode visitFunctionNode(FunctionNode node) { + final Statement? body = node.body; + if (body != null) { + final transformer = _AwaitTransformer(staticTypeContext, coreTypes); + Statement newBody = transformer.transform(body); + + final List newStatements = [ + ...transformer.expressionTransformer.variables, + ...transformer.statements, + ]; + + if (newStatements.isNotEmpty) { + newBody = Block([ + ...newStatements, + ...newBody is Block ? newBody.statements : [newBody] + ]); + } + + node.body = newBody..parent = node; + } + return node; + } + + @override + TreeNode visitAssertBlock(AssertBlock stmt) { + final savedStatements = statements; + statements = []; + for (final stmt in stmt.statements) { + statements.add(transform(stmt)); + } + final newBlock = AssertBlock(statements); + statements = savedStatements; + return newBlock; + } + + @override + TreeNode visitAssertStatement(AssertStatement stmt) { + final List condEffects = []; + final cond = expressionTransformer.rewrite(stmt.condition, condEffects); + final msg = stmt.message; + if (msg == null) { + stmt.condition = cond..parent = stmt; + // If the translation of the condition produced a non-empty list of + // statements, ensure they are guarded by whether asserts are enabled. + return condEffects.isEmpty ? stmt : AssertBlock(condEffects..add(stmt)); + } + + // The translation depends on the translation of the message. + final List msgEffects = []; + stmt.message = expressionTransformer.rewrite(msg, msgEffects) + ..parent = stmt; + if (condEffects.isEmpty) { + if (msgEffects.isEmpty) { + // The condition rewrote to ([], C) and the message rewrote to ([], M). + // The result is + // + // assert(C, M) + stmt.condition = cond..parent = stmt; + return stmt; + } else { + // The condition rewrote to ([], C) and the message rewrote to (S*, M) + // where S* is non-empty. The result is + // + // assert { if (C) {} else { S*; assert(false, M); }} + stmt.condition = BoolLiteral(false)..parent = stmt; + return AssertBlock([ + IfStatement(cond, EmptyStatement(), Block(msgEffects..add(stmt))) + ]); + } + } else { + if (msgEffects.isEmpty) { + // The condition rewrote to (S*, C) where S* is non-empty and the + // message rewrote to ([], M). The result is + // + // assert { S*; assert(C, M); } + stmt.condition = cond..parent = stmt; + condEffects.add(stmt); + } else { + // The condition rewrote to (S0*, C) and the message rewrote to (S1*, M) + // where both S0* and S1* are non-empty. The result is + // + // assert { S0*; if (C) {} else { S1*; assert(false, M); }} + stmt.condition = BoolLiteral(false)..parent = stmt; + condEffects.add( + IfStatement(cond, EmptyStatement(), Block(msgEffects..add(stmt)))); + } + return AssertBlock(condEffects); + } + } + + @override + TreeNode visitBlock(Block stmt) { + final savedStatements = statements; + statements = []; + for (final statement in stmt.statements) { + final newStatement = transform(statement); + statements.add(newStatement); + } + final newBlock = Block(statements); + statements = savedStatements; + return newBlock; + } + + @override + TreeNode visitBreakStatement(BreakStatement stmt) => stmt; + + @override + TreeNode visitContinueSwitchStatement(ContinueSwitchStatement stmt) => stmt; + + Statement visitDelimited(Statement stmt) { + final saved = statements; + statements = []; + statements.add(transform(stmt)); + final result = + statements.length == 1 ? statements.first : Block(statements); + statements = saved; + return result; + } + + @override + TreeNode visitDoStatement(DoStatement stmt) { + Statement body = visitDelimited(stmt.body); // block or single statement + final List effects = []; + stmt.condition = expressionTransformer.rewrite(stmt.condition, effects) + ..parent = stmt; + if (effects.isNotEmpty) { + // The condition rewrote to a non-empty sequence of statements S* and + // value V. Add the statements to the end of the loop body. + final Block block = body is Block ? body : body = Block([body]); + for (final effect in effects) { + block.statements.add(effect); + effect.parent = body; + } + } + stmt.body = body..parent = stmt; + return stmt; + } + + @override + TreeNode visitEmptyStatement(EmptyStatement stmt) => stmt; + + @override + TreeNode visitExpressionStatement(ExpressionStatement stmt) { + stmt.expression = expressionTransformer.rewrite(stmt.expression, statements) + ..parent = stmt; + return stmt; + } + + @override + TreeNode visitForInStatement(ForInStatement stmt) { + throw 'For statement at ${stmt.location}'; + } + + @override + TreeNode visitForStatement(ForStatement stmt) { + // Because of for-loop scoping and variable capture, it is tricky to deal + // with await in the loop's variable initializers or update expressions. + bool isSimple = true; + int length = stmt.variables.length; + List> initEffects = + List>.generate(length, (int i) { + VariableDeclaration decl = stmt.variables[i]; + List statements = []; + if (decl.initializer != null) { + decl.initializer = expressionTransformer.rewrite( + decl.initializer!, statements) + ..parent = decl; + } + isSimple = isSimple && statements.isEmpty; + return statements; + }); + + length = stmt.updates.length; + List> updateEffects = + List>.generate(length, (int i) { + List statements = []; + stmt.updates[i] = expressionTransformer.rewrite( + stmt.updates[i], statements) + ..parent = stmt; + isSimple = isSimple && statements.isEmpty; + return statements; + }); + + Statement body = visitDelimited(stmt.body); + Expression? cond = stmt.condition; + List? condEffects; + if (cond != null) { + condEffects = []; + cond = expressionTransformer.rewrite(stmt.condition!, condEffects); + } + + if (isSimple) { + // If the condition contains await, we use a translation like the one for + // while loops, but leaving the variable declarations and the update + // expressions in place. + if (condEffects == null || condEffects.isEmpty) { + if (cond != null) stmt.condition = cond..parent = stmt; + stmt.body = body..parent = stmt; + return stmt; + } else { + LabeledStatement labeled = LabeledStatement(stmt); + // No condition in a for loop is the same as true. + stmt.condition = null; + condEffects.add(IfStatement(cond!, body, BreakStatement(labeled))); + stmt.body = Block(condEffects)..parent = stmt; + return labeled; + } + } + + // If the rewrite of the initializer or update expressions produces a + // non-empty sequence of statements then the loop is desugared. If the loop + // has the form: + // + // label: for (Type x = init; cond; update) body + // + // it is translated as if it were: + // + // { + // bool first = true; + // Type temp; + // label: while (true) { + // Type x; + // if (first) { + // first = false; + // x = init; + // } else { + // x = temp; + // update; + // } + // if (cond) { + // body; + // temp = x; + // } else { + // break; + // } + // } + // } + + // Place the loop variable declarations at the beginning of the body + // statements and move their initializers to a guarded list of statements. + // Add assignments to the loop variables from the previous iterations temp + // variables before the updates. + // + // temps.first is the flag 'first'. + List temps = [ + VariableDeclaration.forValue(BoolLiteral(true), isFinal: false) + ]; + List loopBody = []; + List initializers = [ + ExpressionStatement(VariableSet(temps.first, BoolLiteral(false))) + ]; + List updates = []; + List newBody = [body]; + for (int i = 0; i < stmt.variables.length; ++i) { + VariableDeclaration decl = stmt.variables[i]; + temps + .add(VariableDeclaration(null, type: decl.type, isSynthesized: true)); + loopBody.add(decl); + if (decl.initializer != null) { + initializers.addAll(initEffects[i]); + initializers + .add(ExpressionStatement(VariableSet(decl, decl.initializer!))); + decl.initializer = null; + } + updates + .add(ExpressionStatement(VariableSet(decl, VariableGet(temps.last)))); + newBody + .add(ExpressionStatement(VariableSet(temps.last, VariableGet(decl)))); + } + // Add the updates to their guarded list of statements. + for (int i = 0; i < stmt.updates.length; ++i) { + updates.addAll(updateEffects[i]); + updates.add(ExpressionStatement(stmt.updates[i])); + } + // Initializers or updates could be empty. + loopBody.add(IfStatement( + VariableGet(temps.first), Block(initializers), Block(updates))); + + LabeledStatement labeled = LabeledStatement(null); + if (cond != null) { + loopBody.addAll(condEffects!); + } else { + cond = BoolLiteral(true); + } + loopBody.add(IfStatement(cond, Block(newBody), BreakStatement(labeled))); + labeled.body = WhileStatement(BoolLiteral(true), Block(loopBody)) + ..parent = labeled; + return Block([] + ..addAll(temps) + ..add(labeled)); + } + + @override + TreeNode visitFunctionDeclaration(FunctionDeclaration stmt) { + stmt.function = transform(stmt.function)..parent = stmt; + return stmt; + } + + @override + TreeNode visitIfStatement(IfStatement stmt) { + stmt.condition = expressionTransformer.rewrite(stmt.condition, statements) + ..parent = stmt; + stmt.then = visitDelimited(stmt.then)..parent = stmt; + if (stmt.otherwise != null) { + stmt.otherwise = visitDelimited(stmt.otherwise!)..parent = stmt; + } + return stmt; + } + + @override + TreeNode visitLabeledStatement(LabeledStatement stmt) { + stmt.body = visitDelimited(stmt.body)..parent = stmt; + return stmt; + } + + @override + TreeNode visitReturnStatement(ReturnStatement stmt) { + if (stmt.expression != null) { + stmt.expression = expressionTransformer.rewrite( + stmt.expression!, statements) + ..parent = stmt; + } + + return stmt; + } + + @override + TreeNode visitSwitchStatement(SwitchStatement stmt) { + stmt.expression = expressionTransformer.rewrite(stmt.expression, statements) + ..parent = stmt; + for (final switchCase in stmt.cases) { + // Expressions in switch cases cannot contain await so they do not need to + // be translated. + switchCase.body = visitDelimited(switchCase.body)..parent = switchCase; + } + return stmt; + } + + @override + TreeNode visitTryCatch(TryCatch stmt) { + stmt.body = visitDelimited(stmt.body)..parent = stmt; + for (final catch_ in stmt.catches) { + // Create a fresh variable for the exception and stack trace: when a + // catch block has an `await` we use the catch block variables to restore + // the current exception after the `await`. + // + // TODO (omersa): We could mark [TreeNode]s with `await`s and only do this + if (catch_.exception == null) { + catch_.exception = VariableDeclaration(null, + type: InterfaceType(coreTypes.objectClass, Nullability.nonNullable), + isSynthesized: true) + ..parent = catch_; + } + if (catch_.stackTrace == null) { + catch_.stackTrace = VariableDeclaration(null, + type: InterfaceType( + coreTypes.stackTraceClass, Nullability.nonNullable), + isSynthesized: true) + ..parent = catch_; + } + + var body = visitDelimited(catch_.body); + + // Add uses to exception and stack trace vars so that they will be added + // to the context if the catch block has an await. + if (body is Block) { + body.statements.add( + ExpressionStatement(VariableGet(catch_.exception!))..parent = body); + body.statements.add(ExpressionStatement(VariableGet(catch_.stackTrace!)) + ..parent = body); + } else { + body = Block([ + body, + ExpressionStatement(VariableGet(catch_.exception!)), + ExpressionStatement(VariableGet(catch_.stackTrace!)), + ]); + } + + catch_.body = body..parent = catch_; + } + return stmt; + } + + @override + TreeNode visitTryFinally(TryFinally stmt) { + // TODO (omersa): Wrapped in a block to be able to get the variable + // declarations using `parent.statements[0]` etc. when compiling the node. + // Ideally we may want to create these variables not in kernel but during + // code generation. + + // Variable for the finalizer block continuation. + final continuationVar = VariableDeclaration(null, + initializer: IntLiteral(asyncCodeGen.continuationFallthrough), + type: InterfaceType(coreTypes.intClass, Nullability.nonNullable), + isSynthesized: true); + + // When the finalizer continuation is "rethrow", this stores the exception + // to rethrow. + final exceptionVar = VariableDeclaration(null, + type: InterfaceType(coreTypes.objectClass, Nullability.nonNullable), + isSynthesized: true); + + // When the finalizer continuation is "rethrow", this stores the stack + // trace of the exception in [exceptionVar]. + final stackTraceVar = VariableDeclaration(null, + type: InterfaceType(coreTypes.stackTraceClass, Nullability.nonNullable), + isSynthesized: true); + + final body = visitDelimited(stmt.body); + var finalizer = visitDelimited(stmt.finalizer); + + // Add a use of `continuationVar` in finally so that it will be added to + // the context + if (finalizer is Block) { + finalizer.statements.add(ExpressionStatement(VariableGet(continuationVar)) + ..parent = finalizer); + finalizer.statements.add( + ExpressionStatement(VariableGet(exceptionVar))..parent = finalizer); + finalizer.statements.add( + ExpressionStatement(VariableGet(stackTraceVar))..parent = finalizer); + } else { + finalizer = Block([ + finalizer, + ExpressionStatement(VariableGet(continuationVar)), + ExpressionStatement(VariableGet(exceptionVar)), + ExpressionStatement(VariableGet(stackTraceVar)), + ]); + } + + return Block([ + continuationVar, + exceptionVar, + stackTraceVar, + TryFinally(body, finalizer) + ]); + } + + @override + TreeNode visitVariableDeclaration(VariableDeclaration stmt) { + final initializer = stmt.initializer; + if (initializer != null) { + stmt.initializer = expressionTransformer.rewrite(initializer, statements) + ..parent = stmt; + } + return stmt; + } + + @override + TreeNode visitWhileStatement(WhileStatement stmt) { + final Statement body = visitDelimited(stmt.body); + final List effects = []; + final Expression cond = + expressionTransformer.rewrite(stmt.condition, effects); + if (effects.isEmpty) { + stmt.condition = cond..parent = stmt; + stmt.body = body..parent = stmt; + return stmt; + } else { + // The condition rewrote to a non-empty sequence of statements S* and + // value V. Rewrite the loop to: + // + // L: while (true) { + // S* + // if (V) { + // [body] + // else { + // break L; + // } + // } + final LabeledStatement labeled = LabeledStatement(stmt); + stmt.condition = BoolLiteral(true)..parent = stmt; + effects.add(IfStatement(cond, body, BreakStatement(labeled))); + stmt.body = Block(effects)..parent = stmt; + return labeled; + } + } + + @override + TreeNode visitYieldStatement(YieldStatement stmt) { + stmt.expression = expressionTransformer.rewrite(stmt.expression, statements) + ..parent = stmt; + return stmt; + } + + @override + TreeNode defaultStatement(Statement stmt) => + throw 'Unhandled statement: $stmt (${stmt.location})'; + + @override + TreeNode defaultExpression(Expression expr) { + // This visits initializer expressions, annotations etc. + final List effects = []; + final Expression transformedExpr = + expressionTransformer.rewrite(expr, effects); + if (effects.isEmpty) { + return transformedExpr; + } else { + return BlockExpression(Block(effects), expr); + } + } +} + +class _ExpressionTransformer extends Transformer { + /// Whether we have seen an await to the right in the expression tree. + /// + /// Subexpressions are visited right-to-left in the reverse of evaluation + /// order. + /// + /// On entry to an expression's visit method, [seenAwait] indicates whether a + /// sibling to the right contains an await. If so the expression will be + /// named in a temporary variable because it is potentially live across an + /// await. + /// + /// On exit from an expression's visit method, [seenAwait] indicates whether + /// the expression itself or a sibling to the right contains an await. + bool seenAwait = false; + + /// The (reverse order) sequence of statements that have been emitted. + /// + /// Transformation of an expression produces a transformed expression and a + /// sequence of statements which are assignments to local variables, calls to + /// helper functions, and yield points. Only the yield points need to be a + /// statements, and they are statements so an implementation does not have to + /// handle unnamed expression intermediate live across yield points. + /// + /// The visit methods return the transformed expression and build a sequence + /// of statements by emitting statements into this list. This list is built + /// in reverse because children are visited right-to-left. + /// + /// If an expression should be named it is named before visiting its children + /// so the naming assignment appears in the list before all statements + /// implementing the translation of the children. + /// + /// Children that are conditionally evaluated, such as some parts of logical + /// and conditional expressions, must be delimited so that they do not emit + /// unguarded statements into [statements]. This is implemented by setting + /// [statements] to a fresh empty list before transforming those children. + List statements = []; + + /// The number of currently live named intermediate values. + /// + /// This index is used to allocate names to temporary values. Because + /// children are visited right-to-left, names are assigned in reverse order + /// of index. + /// + /// When an assignment is emitted into [statements] to name an expression + /// before visiting its children, the index is not immediately reserved + /// because a child can freely use the same name as its parent. In practice, + /// this will be the rightmost named child. + /// + /// After visiting the children of a named expression, [nameIndex] is set to + /// indicate one more live value (the value of the expression) than before + /// visiting the expression. + /// + /// After visiting the children of an expression that is not named, + /// [nameIndex] may still account for names of subexpressions. + int nameIndex = 0; + + /// Variables created for temporaries. + final List variables = []; + + final _AwaitTransformer _statementTransformer; + + final StatefulStaticTypeContext staticTypeContext; + + final CoreTypes coreTypes; + + _ExpressionTransformer( + this._statementTransformer, this.staticTypeContext, this.coreTypes); + + // Helpers + + /// Name an expression by emitting an assignment to a temporary variable. + Expression name(Expression expr) { + final DartType type = expr.getStaticType(staticTypeContext); + final VariableDeclaration temp = allocateTemporary(nameIndex, type); + statements.add(ExpressionStatement(VariableSet(temp, expr))); + return castVariableGet(temp, type); + } + + VariableDeclaration allocateTemporary(int index, + [DartType type = const DynamicType()]) { + if (variables.length > index) { + // Re-using a temporary. Re-type it to dynamic if we detect reuse with + // different type. + if (variables[index].type != const DynamicType() && + variables[index].type != type) { + variables[index].type = const DynamicType(); + } + return variables[index]; + } + for (var i = variables.length; i <= index; i++) { + variables.add(VariableDeclaration(":async_temporary_${i}", type: type)); + } + return variables[index]; + } + + /// Casts a [VariableGet] with `as dynamic` if its type is not `dynamic`. + Expression castVariableGet(VariableDeclaration variable, DartType type) { + Expression expr = VariableGet(variable); + if (type != const DynamicType()) { + expr = AsExpression(expr, DynamicType()); + } + return expr; + } + + // Expressions + + /// Rewrite a top-level expression (top-level wrt. a statement). This is the + /// entry-point from [_AwaitTransformer]. + /// + /// Rewriting an expression produces a sequence of statements and an + /// expression. The sequence of statements are added to the given list. Pass + /// an empty list if the rewritten expression should be delimited from the + /// surrounding context. + // + // TODO (omersa): We should be able to maintain the state for temporaries + // (`nameIndex`, `variables`) in a separate class and create a new expression + // transformer every time we transform a top-level expression. Would that + // make the code clearer? + Expression rewrite(Expression expression, List outer) { + assert(statements.isEmpty); + final saved = seenAwait; + seenAwait = false; + final Expression result = transform(expression); + outer.addAll(statements.reversed); + statements.clear(); + seenAwait = seenAwait || saved; + return result; + } + + @override + TreeNode defaultExpression(Expression expr) => + throw 'Unhandled expression: $expr (${expr.location})'; + + @override + TreeNode visitFunctionExpression(FunctionExpression expr) { + expr.transformChildren(this); + return expr; + } + + // Simple literals. These are pure expressions so they can be evaluated after + // an await to their right. + @override + TreeNode visitSymbolLiteral(SymbolLiteral expr) => expr; + + @override + TreeNode visitTypeLiteral(TypeLiteral expr) => expr; + + @override + TreeNode visitThisExpression(ThisExpression expr) => expr; + + @override + TreeNode visitStringLiteral(StringLiteral expr) => expr; + + @override + TreeNode visitIntLiteral(IntLiteral expr) => expr; + + @override + TreeNode visitDoubleLiteral(DoubleLiteral expr) => expr; + + @override + TreeNode visitBoolLiteral(BoolLiteral expr) => expr; + + @override + TreeNode visitNullLiteral(NullLiteral expr) => expr; + + @override + TreeNode visitConstantExpression(ConstantExpression expr) => expr; + + @override + TreeNode visitCheckLibraryIsLoaded(CheckLibraryIsLoaded expr) => expr; + + @override + TreeNode visitLoadLibrary(LoadLibrary expr) => expr; + + /// Transform expressions with no child expressions. + Expression nullary(Expression expr) { + if (seenAwait) { + expr = name(expr); + ++nameIndex; + } + return expr; + } + + @override + TreeNode visitSuperPropertyGet(SuperPropertyGet expr) => nullary(expr); + + @override + TreeNode visitStaticGet(StaticGet expr) => nullary(expr); + + @override + TreeNode visitStaticTearOff(StaticTearOff expr) => nullary(expr); + + @override + TreeNode visitRethrow(Rethrow expr) => nullary(expr); + + @override + TreeNode visitVariableGet(VariableGet expr) { + Expression result = expr; + // Getting a final or const variable is not an effect so it can be + // evaluated after an await to its right. + if (seenAwait && !expr.variable.isFinal && !expr.variable.isConst) { + result = name(expr); + ++nameIndex; + } + return result; + } + + /// Transform an expression given an action to transform the children. For + /// this purposes of the await transformer the children should generally be + /// translated from right to left, in the reverse of evaluation order. + Expression transformTreeNode(Expression expr, void action(), + {bool alwaysName = false}) { + final bool shouldName = alwaysName || seenAwait; + + // 1. If there is an await in a sibling to the right, emit an assignment to + // a temporary variable before transforming the children. + final Expression result = shouldName ? name(expr) : expr; + + // 2. Remember the number of live temporaries before transforming the + // children. + final int index = nameIndex; + + // 3. Transform the children. Initially they do not have an await in a + // sibling to their right. + seenAwait = false; + action(); + + // 4. If the expression was named then the variables used for children are + // no longer live but the variable used for the expression is. On the other + // hand, a sibling to the left (yet to be processed) cannot reuse any of + // the variables used here, as the assignments in the children (here) would + // overwrite assignments in the siblings to the left, possibly before the + // use of the overwritten values. + if (shouldName) { + if (index + 1 > nameIndex) { + nameIndex = index + 1; + } + seenAwait = true; + } + + return result; + } + + /// Transform expressions with one child expression. + Expression unary(Expression expr) { + return transformTreeNode(expr, () { + expr.transformChildren(this); + }); + } + + @override + TreeNode visitInvalidExpression(InvalidExpression expr) => unary(expr); + + @override + TreeNode visitVariableSet(VariableSet expr) => unary(expr); + + @override + TreeNode visitInstanceGet(InstanceGet expr) => unary(expr); + + @override + TreeNode visitDynamicGet(DynamicGet expr) => unary(expr); + + @override + TreeNode visitInstanceTearOff(InstanceTearOff expr) => unary(expr); + + @override + TreeNode visitFunctionTearOff(FunctionTearOff expr) => unary(expr); + + @override + TreeNode visitSuperPropertySet(SuperPropertySet expr) => unary(expr); + + @override + TreeNode visitStaticSet(StaticSet expr) => unary(expr); + + @override + TreeNode visitNot(Not expr) => unary(expr); + + @override + TreeNode visitIsExpression(IsExpression expr) => unary(expr); + + @override + TreeNode visitAsExpression(AsExpression expr) => unary(expr); + + @override + TreeNode visitThrow(Throw expr) => unary(expr); + + @override + TreeNode visitEqualsNull(EqualsNull expr) => unary(expr); + + @override + TreeNode visitRecordIndexGet(RecordIndexGet expr) => unary(expr); + + @override + TreeNode visitRecordNameGet(RecordNameGet expr) => unary(expr); + + @override + TreeNode visitNullCheck(NullCheck expr) => unary(expr); + + @override + TreeNode visitInstantiation(Instantiation expr) => unary(expr); + + @override + TreeNode visitInstanceSet(InstanceSet expr) { + return transformTreeNode(expr, () { + expr.value = transform(expr.value)..parent = expr; + expr.receiver = transform(expr.receiver)..parent = expr; + }); + } + + @override + TreeNode visitDynamicSet(DynamicSet expr) { + return transformTreeNode(expr, () { + expr.value = transform(expr.value)..parent = expr; + expr.receiver = transform(expr.receiver)..parent = expr; + }); + } + + @override + TreeNode visitArguments(Arguments args) { + for (final named in args.named.reversed) { + named.value = transform(named.value)..parent = named; + } + final positional = args.positional; + for (var i = positional.length - 1; i >= 0; --i) { + positional[i] = transform(positional[i])..parent = args; + } + return args; + } + + @override + TreeNode visitInstanceInvocation(InstanceInvocation expr) { + return transformTreeNode(expr, () { + visitArguments(expr.arguments); + expr.receiver = transform(expr.receiver)..parent = expr; + }); + } + + @override + TreeNode visitLocalFunctionInvocation(LocalFunctionInvocation expr) { + return transformTreeNode(expr, () { + visitArguments(expr.arguments); + }); + } + + @override + TreeNode visitDynamicInvocation(DynamicInvocation expr) { + return transformTreeNode(expr, () { + visitArguments(expr.arguments); + expr.receiver = transform(expr.receiver)..parent = expr; + }); + } + + @override + TreeNode visitFunctionInvocation(FunctionInvocation expr) { + return transformTreeNode(expr, () { + visitArguments(expr.arguments); + expr.receiver = transform(expr.receiver)..parent = expr; + }); + } + + @override + TreeNode visitEqualsCall(EqualsCall expr) { + return transformTreeNode(expr, () { + expr.right = transform(expr.right)..parent = expr; + expr.left = transform(expr.left)..parent = expr; + }); + } + + @override + TreeNode visitSuperMethodInvocation(SuperMethodInvocation expr) { + return transformTreeNode(expr, () { + visitArguments(expr.arguments); + }); + } + + @override + TreeNode visitStaticInvocation(StaticInvocation expr) { + return transformTreeNode(expr, () { + visitArguments(expr.arguments); + }); + } + + @override + TreeNode visitConstructorInvocation(ConstructorInvocation expr) { + return transformTreeNode(expr, () { + visitArguments(expr.arguments); + }); + } + + @override + TreeNode visitStringConcatenation(StringConcatenation expr) { + return transformTreeNode(expr, () { + final expressions = expr.expressions; + for (var i = expressions.length - 1; i >= 0; --i) { + expressions[i] = transform(expressions[i])..parent = expr; + } + }); + } + + @override + TreeNode visitListLiteral(ListLiteral expr) { + return transformTreeNode(expr, () { + final expressions = expr.expressions; + for (var i = expressions.length - 1; i >= 0; --i) { + expressions[i] = transform(expr.expressions[i])..parent = expr; + } + }); + } + + @override + TreeNode visitMapLiteral(MapLiteral expr) { + return transformTreeNode(expr, () { + for (final entry in expr.entries.reversed) { + entry.value = transform(entry.value)..parent = entry; + entry.key = transform(entry.key)..parent = entry; + } + }); + } + + @override + TreeNode visitSetLiteral(SetLiteral expr) { + return transformTreeNode(expr, () { + final expressions = expr.expressions; + for (var i = expressions.length - 1; i >= 0; --i) { + expressions[i] = transform(expr.expressions[i])..parent = expr; + } + }); + } + + @override + TreeNode visitRecordLiteral(RecordLiteral expr) { + return transformTreeNode(expr, () { + final named = expr.named; + for (var i = named.length - 1; i >= 0; --i) { + named[i] = transform(expr.named[i])..parent = expr; + } + + final positional = expr.positional; + for (var i = positional.length - 1; i >= 0; --i) { + positional[i] = transform(expr.positional[i])..parent = expr; + } + }); + } + + // Expressions with control flow + + /// Perform an action with a given list of statements so that it cannot emit + /// statements into the 'outer' list. + Expression delimit(Expression action(), List inner) { + final outer = statements; + statements = inner; + final result = action(); + statements = outer; + return result; + } + + /// Make a [Block] from a reversed list of [Statement]s by reverting the + /// statements. + Block blockOf(List reversedStatements) { + return Block(reversedStatements.reversed.toList()); + } + + @override + TreeNode visitLogicalExpression(LogicalExpression expr) { + final bool shouldName = seenAwait; + + // Right is delimited because it is conditionally evaluated. + final List rightStatements = []; + seenAwait = false; + expr.right = delimit(() => transform(expr.right), rightStatements) + ..parent = expr; + final bool rightAwait = seenAwait; + + if (rightStatements.isEmpty) { + // Easy case: right did not emit any statements. + seenAwait = shouldName; + return transformTreeNode(expr, () { + expr.left = transform(expr.left)..parent = expr; + seenAwait = seenAwait || rightAwait; + }); + } + + // If right has emitted statements we will produce a temporary t and emit + // for && (there is an analogous case for ||): + // + // t = [left] == true; + // if (t) { + // t = [right] == true; + // } + + // Recall that statements are emitted in reverse order, so first emit the + // if statement, then the assignment of [left] == true, and then translate + // left so any statements it emits occur after in the accumulated list + // (that is, so they occur before in the corresponding block). + final Block rightBody = blockOf(rightStatements); + final InterfaceType type = staticTypeContext.typeEnvironment.coreTypes + .boolRawType(staticTypeContext.nonNullable); + final VariableDeclaration result = allocateTemporary(nameIndex, type); + final objectEquals = coreTypes.objectEquals; + rightBody.addStatement(ExpressionStatement(VariableSet( + result, + EqualsCall(expr.right, BoolLiteral(true), + interfaceTarget: objectEquals, + functionType: objectEquals.getterType as FunctionType)))); + final Statement then; + final Statement? otherwise; + if (expr.operatorEnum == LogicalExpressionOperator.AND) { + then = rightBody; + otherwise = null; + } else { + then = EmptyStatement(); + otherwise = rightBody; + } + statements.add(IfStatement(castVariableGet(result, type), then, otherwise)); + + final test = EqualsCall(expr.left, BoolLiteral(true), + interfaceTarget: objectEquals, + functionType: objectEquals.getterType as FunctionType); + statements.add(ExpressionStatement(VariableSet(result, test))); + + seenAwait = false; + test.left = transform(test.left)..parent = test; + + nameIndex += 1; + seenAwait = seenAwait || rightAwait; + return castVariableGet(result, type); + } + + @override + TreeNode visitConditionalExpression(ConditionalExpression expr) { + // Then and otherwise are delimited because they are conditionally + // evaluated. + final bool shouldName = seenAwait; + + final int savedNameIndex = nameIndex; + + final thenStatements = []; + seenAwait = false; + expr.then = delimit(() => transform(expr.then), thenStatements) + ..parent = expr; + final thenAwait = seenAwait; + + final thenNameIndex = nameIndex; + nameIndex = savedNameIndex; + + final List otherwiseStatements = []; + seenAwait = false; + expr.otherwise = + delimit(() => transform(expr.otherwise), otherwiseStatements) + ..parent = expr; + final otherwiseAwait = seenAwait; + + // Only one side of this branch will get executed at a time, so just make + // sure we have enough temps for either, not both at the same time. + if (thenNameIndex > nameIndex) { + nameIndex = thenNameIndex; + } + + if (thenStatements.isEmpty && otherwiseStatements.isEmpty) { + // Easy case: neither then nor otherwise emitted any statements. + seenAwait = shouldName; + return transformTreeNode(expr, () { + expr.condition = transform(expr.condition)..parent = expr; + seenAwait = seenAwait || thenAwait || otherwiseAwait; + }); + } + + // If `then` or `otherwise` has emitted statements we will produce a + // temporary t and emit: + // + // if ([condition]) { + // t = [left]; + // } else { + // t = [right]; + // } + final result = allocateTemporary(nameIndex, expr.staticType); + final thenBody = blockOf(thenStatements); + final otherwiseBody = blockOf(otherwiseStatements); + thenBody.addStatement(ExpressionStatement(VariableSet(result, expr.then))); + otherwiseBody + .addStatement(ExpressionStatement(VariableSet(result, expr.otherwise))); + final branch = IfStatement(expr.condition, thenBody, otherwiseBody); + statements.add(branch); + + seenAwait = false; + branch.condition = transform(branch.condition)..parent = branch; + + nameIndex += 1; + seenAwait = seenAwait || thenAwait || otherwiseAwait; + return castVariableGet(result, expr.staticType); + } + + // Await expression + + @override + TreeNode visitAwaitExpression(AwaitExpression expr) { + // TODO (omersa): Only name if the await is not already in assignment RHS + return transformTreeNode(expr, () { + expr.transformChildren(this); + }, alwaysName: true); + } + + // Block expressions + + @override + TreeNode visitBlockExpression(BlockExpression expr) { + return transformTreeNode(expr, () { + expr.value = transform(expr.value)..parent = expr; + final List body = []; + for (final Statement stmt in expr.body.statements.reversed) { + final Statement? translation = _rewriteStatement(stmt); + if (translation != null) { + body.add(translation); + } + } + expr.body = Block(body.reversed.toList())..parent = expr; + }); + } + + @override + TreeNode visitLet(Let expr) { + final body = transform(expr.body); + final VariableDeclaration variable = expr.variable; + if (seenAwait) { + // There is an await in the body of `let var x = initializer in body` or + // to its right. We will produce the sequence of statements: + // + // + // var x = + // + // + // and return the body's value. + statements.add(variable); + var index = nameIndex; + seenAwait = false; + variable.initializer = transform(variable.initializer!) + ..parent = variable; + // Temporaries used in the initializer or the body are not live but the + // temporary used for the body is. + if (index + 1 > nameIndex) { + nameIndex = index + 1; + } + seenAwait = true; + return body; + } else { + // The body in `let x = initializer in body` did not contain an await. + // We can leave a let expression. + return transformTreeNode(expr, () { + // The body has already been translated. + expr.body = body..parent = expr; + variable.initializer = transform(variable.initializer!) + ..parent = variable; + }); + } + } + + @override + TreeNode visitFunctionNode(FunctionNode node) { + var nestedRewriter = _AwaitTransformer(staticTypeContext, coreTypes); + return nestedRewriter.transform(node); + } + + /// This method translates a statement nested in an expression (e.g., in a + /// block expression). It produces a translated statement, a list of + /// statements which are side effects necessary for any await, and a flag + /// indicating whether there was an await in the statement or to its right. + /// The translated statement can be null in the case where there was already + /// an await to the right. + Statement? _rewriteStatement(Statement stmt) { + // The translation is accumulating two lists of statements, an inner list + // which is a reversed list of effects needed for the current expression + // and an outer list which represents the block containing the current + // statement. We need to preserve both of those from side effects. + final List savedInner = statements; + final List savedOuter = _statementTransformer.statements; + statements = []; + _statementTransformer.statements = []; + stmt = _statementTransformer.transform(stmt); + + final List results = _statementTransformer.statements; + results.add(stmt); + + statements = savedInner; + _statementTransformer.statements = savedOuter; + if (!seenAwait && results.length == 1) { + return results.first; + } + statements.addAll(results.reversed); + return null; + } + + @override + TreeNode defaultStatement(Statement stmt) { + throw UnsupportedError( + "Use _rewriteStatement to transform statement: ${stmt}"); + } +} diff --git a/pkg/dart2wasm/lib/class_info.dart b/pkg/dart2wasm/lib/class_info.dart index 01df8110108..d52dd8d202e 100644 --- a/pkg/dart2wasm/lib/class_info.dart +++ b/pkg/dart2wasm/lib/class_info.dart @@ -17,6 +17,14 @@ import 'package:wasm_builder/wasm_builder.dart' as w; /// [ClassInfo._addField] (for manually added fields) or by a line in /// [FieldIndex.validate] (for fields declared in Dart code). class FieldIndex { + static const asyncSuspendStateResume = 2; + static const asyncSuspendStateContext = 3; + static const asyncSuspendStateTargetIndex = 4; + static const asyncSuspendStateCompleter = 5; + static const asyncSuspendStateCurrentException = 6; + static const asyncSuspendStateCurrentExceptionStackTrace = 7; + static const asyncSuspendStateCurrentReturnValue = 8; + static const classId = 0; static const boxValue = 1; static const identityHash = 1; @@ -61,6 +69,21 @@ class FieldIndex { "Unexpected field index for ${cls.name}.$name"); } + check(translator.asyncSuspendStateClass, "_resume", + FieldIndex.asyncSuspendStateResume); + check(translator.asyncSuspendStateClass, "_context", + FieldIndex.asyncSuspendStateContext); + check(translator.asyncSuspendStateClass, "_targetIndex", + FieldIndex.asyncSuspendStateTargetIndex); + check(translator.asyncSuspendStateClass, "_completer", + FieldIndex.asyncSuspendStateCompleter); + check(translator.asyncSuspendStateClass, "_currentException", + FieldIndex.asyncSuspendStateCurrentException); + check(translator.asyncSuspendStateClass, "_currentExceptionStackTrace", + FieldIndex.asyncSuspendStateCurrentExceptionStackTrace); + check(translator.asyncSuspendStateClass, "_currentReturnValue", + FieldIndex.asyncSuspendStateCurrentReturnValue); + check(translator.boxedBoolClass, "value", FieldIndex.boxValue); check(translator.boxedIntClass, "value", FieldIndex.boxValue); check(translator.boxedDoubleClass, "value", FieldIndex.boxValue); diff --git a/pkg/dart2wasm/lib/closures.dart b/pkg/dart2wasm/lib/closures.dart index c174424faa1..f9e3e77d712 100644 --- a/pkg/dart2wasm/lib/closures.dart +++ b/pkg/dart2wasm/lib/closures.dart @@ -1078,9 +1078,9 @@ class CaptureFinder extends RecursiveVisitor { final Closures closures; final Member member; final Map variableDepth = {}; - final List functionIsSyncStar = [false]; + final List functionIsSyncStarOrAsync = [false]; - int get depth => functionIsSyncStar.length - 1; + int get depth => functionIsSyncStarOrAsync.length - 1; CaptureFinder(this.closures, this.member); @@ -1091,15 +1091,23 @@ class CaptureFinder extends RecursiveVisitor { @override void visitFunctionNode(FunctionNode node) { assert(depth == 0); // Nested function nodes are skipped by [_visitLambda]. - functionIsSyncStar[0] = node.asyncMarker == AsyncMarker.SyncStar; + functionIsSyncStarOrAsync[0] = node.asyncMarker == AsyncMarker.SyncStar || + node.asyncMarker == AsyncMarker.Async; node.visitChildren(this); - functionIsSyncStar[0] = false; + functionIsSyncStarOrAsync[0] = false; } @override void visitAssertStatement(AssertStatement node) { if (translator.options.enableAsserts) { - node.visitChildren(this); + super.visitAssertStatement(node); + } + } + + @override + void visitAssertBlock(AssertBlock node) { + if (translator.options.enableAsserts) { + super.visitAssertBlock(node); } } @@ -1124,9 +1132,9 @@ class CaptureFinder extends RecursiveVisitor { void _visitVariableUse(TreeNode variable) { int declDepth = variableDepth[variable] ?? 0; assert(declDepth <= depth); - if (declDepth < depth || functionIsSyncStar[declDepth]) { + if (declDepth < depth || functionIsSyncStarOrAsync[declDepth]) { final capture = closures.captures[variable] ??= Capture(variable); - if (functionIsSyncStar[declDepth]) capture.written = true; + if (functionIsSyncStarOrAsync[declDepth]) capture.written = true; } else if (variable is VariableDeclaration && variable.parent is FunctionDeclaration) { closures.closurizedFunctions.add(variable.parent as FunctionDeclaration); @@ -1146,7 +1154,7 @@ class CaptureFinder extends RecursiveVisitor { } void _visitThis() { - if (depth > 0 || functionIsSyncStar[0]) { + if (depth > 0 || functionIsSyncStarOrAsync[0]) { closures.isThisCaptured = true; } } @@ -1200,9 +1208,10 @@ class CaptureFinder extends RecursiveVisitor { m.addFunction(type, "$member closure at ${node.location}"); closures.lambdas[node] = Lambda(node, function); - functionIsSyncStar.add(node.asyncMarker == AsyncMarker.SyncStar); + functionIsSyncStarOrAsync.add(node.asyncMarker == AsyncMarker.SyncStar || + node.asyncMarker == AsyncMarker.Async); node.visitChildren(this); - functionIsSyncStar.removeLast(); + functionIsSyncStarOrAsync.removeLast(); } @override @@ -1232,7 +1241,14 @@ class ContextCollector extends RecursiveVisitor { @override void visitAssertStatement(AssertStatement node) { if (enableAsserts) { - node.visitChildren(this); + super.visitAssertStatement(node); + } + } + + @override + void visitAssertBlock(AssertBlock node) { + if (enableAsserts) { + super.visitAssertBlock(node); } } diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart index fc3cb78b98a..6ec69c98cac 100644 --- a/pkg/dart2wasm/lib/code_generator.dart +++ b/pkg/dart2wasm/lib/code_generator.dart @@ -4,6 +4,7 @@ import 'dart:collection' show LinkedHashMap; +import 'package:dart2wasm/async.dart'; import 'package:dart2wasm/class_info.dart'; import 'package:dart2wasm/closures.dart'; import 'package:dart2wasm/dispatch_table.dart'; @@ -98,9 +99,17 @@ class CodeGenerator extends ExpressionVisitor1 Reference reference) { bool isSyncStar = functionNode?.asyncMarker == AsyncMarker.SyncStar && !reference.isTearOffReference; - return isSyncStar - ? SyncStarCodeGenerator(translator, function, reference) - : CodeGenerator(translator, function, reference); + bool isAsync = functionNode?.asyncMarker == AsyncMarker.Async && + !reference.isTearOffReference; + bool isTypeChecker = reference.isTypeCheckerReference; + + if (!isTypeChecker && isSyncStar) { + return SyncStarCodeGenerator(translator, function, reference); + } else if (!isTypeChecker && isAsync) { + return AsyncCodeGenerator(translator, function, reference); + } else { + return CodeGenerator(translator, function, reference); + } } w.Module get m => translator.m; @@ -209,19 +218,7 @@ class CodeGenerator extends ExpressionVisitor1 } assert(member.function!.asyncMarker != AsyncMarker.SyncStar); - - if (member.function!.asyncMarker == AsyncMarker.Async && - !reference.isAsyncInnerReference) { - // Generate the async wrapper function, i.e. the function that gets - // called when an async function is called. The inner function, containing - // the body of the async function, is marked as an async inner reference - // and is generated separately. - Procedure procedure = member as Procedure; - w.BaseFunction inner = - translator.functions.getFunction(procedure.asyncInnerReference); - int parameterOffset = _initializeThis(member); - return generateAsyncWrapper(procedure.function, inner, parameterOffset); - } + assert(member.function!.asyncMarker != AsyncMarker.Async); translator.membersBeingGenerated.add(member); generateBody(member); @@ -295,94 +292,6 @@ class CodeGenerator extends ExpressionVisitor1 b.end(); } - /// Generate the async wrapper for an async function and its associated - /// stub function. - /// - /// The async wrapper is the outer function that gets called when the async - /// function is called. It bundles up the arguments to the function into an - /// arguments struct along with a reference to the stub function. - /// - /// This struct is passed to the async helper, which allocates a new stack and - /// calls the stub function on that stack. - /// - /// The stub function unwraps the arguments from the struct and calls the - /// inner function, containing the implementation of the async function. - void generateAsyncWrapper( - FunctionNode functionNode, w.BaseFunction inner, int parameterOffset) { - w.DefinedFunction stub = - m.addFunction(translator.functions.asyncStubFunctionType); - w.BaseFunction asyncHelper = - translator.functions.getFunction(translator.asyncHelper.reference); - - w.Instructions stubBody = stub.body; - w.Local stubArguments = stub.locals[0]; - w.Local stubStack = stub.locals[1]; - - // Set up the parameter to local mapping, for type checks and in case a - // type parameter is used in the return type. - int paramIndex = parameterOffset; - for (TypeParameter typeParam in functionNode.typeParameters) { - typeLocals[typeParam] = paramLocals[paramIndex++]; - } - for (VariableDeclaration param in functionNode.positionalParameters) { - locals[param] = paramLocals[paramIndex++]; - } - for (VariableDeclaration param in functionNode.namedParameters) { - locals[param] = paramLocals[paramIndex++]; - } - - generateTypeChecks(functionNode.typeParameters, functionNode, - ParameterInfo.fromLocalFunction(functionNode)); - - // Push the type argument to the async helper, specifying the type argument - // of the returned `Future`. - DartType returnType = functionNode.returnType; - DartType innerType = returnType is InterfaceType && - returnType.classNode == translator.coreTypes.futureClass - ? returnType.typeArguments.single - : const DynamicType(); - types.makeType(this, innerType); - - // Create struct for stub reference and arguments - w.StructType baseStruct = translator.functions.asyncStubBaseStruct; - w.StructType argsStruct = m.addStructType("${function.functionName} (args)", - fields: baseStruct.fields, superType: baseStruct); - - // Push stub reference - w.Global stubGlobal = translator.makeFunctionRef(stub); - b.global_get(stubGlobal); - - // Transfer function arguments to inner - w.Local argsLocal = - stub.addLocal(w.RefType.def(argsStruct, nullable: false)); - stubBody.local_get(stubArguments); - translator.convertType(stub, stubArguments.type, argsLocal.type); - stubBody.local_set(argsLocal); - int arity = function.type.inputs.length; - for (int i = 0; i < arity; i++) { - int fieldIndex = argsStruct.fields.length; - w.ValueType type = function.locals[i].type; - argsStruct.fields.add(w.FieldType(type, mutable: false)); - b.local_get(function.locals[i]); - stubBody.local_get(argsLocal); - stubBody.struct_get(argsStruct, fieldIndex); - } - b.struct_new(argsStruct); - - // Call async helper - b.call(asyncHelper); - translator.convertType( - function, asyncHelper.type.outputs.single, outputs.single); - b.end(); - - // Call inner function from stub - stubBody.local_get(stubStack); - stubBody.call(inner); - translator.convertType( - stub, inner.type.outputs.single, stub.type.outputs.single); - stubBody.end(); - } - void setupParametersAndContexts(Member member) { ParameterInfo paramInfo = translator.paramInfoFor(reference); int parameterOffset = _initializeThis(member); @@ -533,15 +442,11 @@ class CodeGenerator extends ExpressionVisitor1 if (member is Constructor) { generateInitializerList(member); } - // Async function type checks are generated in the wrapper functions, in - // [generateAsyncWrapper]. - if (member.function!.asyncMarker != AsyncMarker.Async) { - final List typeParameters = member is Constructor - ? member.enclosingClass.typeParameters - : member.function!.typeParameters; - generateTypeChecks( - typeParameters, member.function!, translator.paramInfoFor(reference)); - } + final List typeParameters = member is Constructor + ? member.enclosingClass.typeParameters + : member.function!.typeParameters; + generateTypeChecks( + typeParameters, member.function!, translator.paramInfoFor(reference)); Statement? body = member.function!.body; if (body != null) { visitStatement(body); @@ -574,10 +479,7 @@ class CodeGenerator extends ExpressionVisitor1 // Initialize closure information from enclosing member. this.closures = closures; - if (lambda.functionNode.asyncMarker == AsyncMarker.Async && - lambda.function == function) { - return generateAsyncLambdaWrapper(lambda); - } + assert(lambda.functionNode.asyncMarker != AsyncMarker.Async); setupLambdaParametersAndContexts(lambda); @@ -588,15 +490,6 @@ class CodeGenerator extends ExpressionVisitor1 return function; } - w.DefinedFunction generateAsyncLambdaWrapper(Lambda lambda) { - _initializeContextLocals(lambda.functionNode); - w.DefinedFunction inner = - translator.functions.addAsyncInnerFunctionFor(function); - generateAsyncWrapper(lambda.functionNode, inner, 1); - return CodeGenerator(translator, inner, reference) - .generateLambda(lambda, closures); - } - /// Initialize locals containing `this` in constructors and instance members. /// Returns the number of parameter locals taken up by the receiver parameter, /// i.e. the parameter offset for the first type parameter (or the first @@ -937,7 +830,13 @@ class CodeGenerator extends ExpressionVisitor1 } @override - void visitAssertBlock(AssertBlock node) {} + void visitAssertBlock(AssertBlock node) { + if (!options.enableAsserts) return; + + for (Statement statement in node.statements) { + visitStatement(statement); + } + } @override void visitTryCatch(TryCatch node) { @@ -1369,81 +1268,25 @@ class CodeGenerator extends ExpressionVisitor1 return; } - final switchExprClass = - translator.classForType(dartTypeOf(node.expression)); - - bool check() => - node.cases.expand((c) => c.expressions).every((e) => - e is L || - e is NullLiteral || - (e is ConstantExpression && - (e.constant is C || e.constant is NullConstant) && - (translator.hierarchy.isSubtypeOf( - translator.classForType(dartTypeOf(e)), switchExprClass)))); - - // Identify kind of switch. One of `nullableType` or `nonNullableType` will - // be the type for Wasm local that holds the switch value. - late final w.ValueType nullableType; - late final w.ValueType nonNullableType; - late final void Function() compare; - if (node.cases.every((c) => - c.expressions.isEmpty && c.isDefault || - c.expressions.every((e) => - e is NullLiteral || - e is ConstantExpression && e.constant is NullConstant))) { - // default-only switch - nonNullableType = w.RefType.eq(nullable: false); - nullableType = w.RefType.eq(nullable: true); - compare = () => throw "Comparison in default-only switch"; - } else if (check()) { - // bool switch - nonNullableType = w.NumType.i32; - nullableType = - translator.classInfo[translator.boxedBoolClass]!.nullableType; - compare = () => b.i32_eq(); - } else if (check()) { - // int switch - nonNullableType = w.NumType.i64; - nullableType = - translator.classInfo[translator.boxedIntClass]!.nullableType; - compare = () => b.i64_eq(); - } else if (check()) { - // String switch - nonNullableType = - translator.classInfo[translator.stringBaseClass]!.nonNullableType; - nullableType = nonNullableType.withNullability(true); - compare = () => call(translator.stringEquals.reference); - } else { - // Object switch - nonNullableType = translator.topInfo.nonNullableType; - nullableType = translator.topInfo.nullableType; - compare = () => b.call(translator.functions - .getFunction(translator.coreTypes.identicalProcedure.reference)); - } + final switchInfo = SwitchInfo(this, node); bool isNullable = dartTypeOf(node.expression).isPotentiallyNullable; // 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(nonNullableType); + w.Local switchValueNonNullableLocal = addLocal(switchInfo.nonNullableType); w.Local? switchValueNullableLocal = - isNullable ? addLocal(nullableType) : null; + isNullable ? addLocal(switchInfo.nullableType) : null; // Initialize switch value local - wrap(node.expression, isNullable ? nullableType : nonNullableType); + wrap(node.expression, + isNullable ? switchInfo.nullableType : switchInfo.nonNullableType); b.local_set( isNullable ? switchValueNullableLocal! : switchValueNonNullableLocal); // Special cases - SwitchCase? defaultCase = node.cases - .cast() - .firstWhere((c) => c!.isDefault, orElse: () => null); - - SwitchCase? nullCase = node.cases.cast().firstWhere( - (c) => c!.expressions.any((e) => - e is NullLiteral || - e is ConstantExpression && e.constant is NullConstant), - orElse: () => null); + SwitchCase? defaultCase = switchInfo.defaultCase; + SwitchCase? nullCase = switchInfo.nullCase; // Create `loop` for backward jumps w.Label loopLabel = b.loop(); @@ -1472,7 +1315,9 @@ class CodeGenerator extends ExpressionVisitor1 b.local_get(switchValueNullableLocal!); b.br_on_null(nullLabel); translator.convertType( - function, nullableType.withNullability(false), nonNullableType); + function, + switchInfo.nullableType.withNullability(false), + switchInfo.nonNullableType); b.local_set(switchValueNonNullableLocal); } @@ -1483,9 +1328,9 @@ class CodeGenerator extends ExpressionVisitor1 exp is ConstantExpression && exp.constant is NullConstant) { // Null already checked, skip } else { - wrap(exp, nonNullableType); + wrap(exp, switchInfo.nonNullableType); b.local_get(switchValueNonNullableLocal); - compare(); + switchInfo.compare(); b.br_if(switchLabels[c]!); } } @@ -1560,18 +1405,7 @@ class CodeGenerator extends ExpressionVisitor1 @override w.ValueType visitAwaitExpression( AwaitExpression node, w.ValueType expectedType) { - w.BaseFunction awaitHelper = - translator.functions.getFunction(translator.awaitHelper.reference); - - // The stack for the suspension is the last parameter to the function. - w.Local stack = function.locals[function.type.inputs.length - 1]; - assert(stack.type == translator.functions.asyncStackType); - - wrap(node.operand, translator.topInfo.nullableType); - b.local_get(stack); - b.call(awaitHelper); - - return translator.topInfo.nullableType; + throw 'Await expression in code generator: $node (${node.location})'; } @override @@ -3322,6 +3156,14 @@ class CodeGenerator extends ExpressionVisitor1 // evaluator. throw new UnsupportedError("CodeGenerator.visitIfCaseStatement"); } + + void debugRuntimePrint(String s) { + final printFunction = + translator.functions.getFunction(translator.printToConsole.reference); + translator.constants.instantiateConstant( + function, b, StringConstant(s), printFunction.type.inputs[0]); + b.call(printFunction); + } } class TryBlockFinalizer { @@ -3364,6 +3206,90 @@ class SwitchBackwardJumpInfo { : defaultLoopLabel = null; } +class SwitchInfo { + /// Non-nullable Wasm type of the `switch` expression. Used when the + /// expression is not nullable, and after the null check. + late final w.ValueType nullableType; + + /// Nullable Wasm type of the `switch` expression. Only used when the + /// expression is nullable. + late final w.ValueType nonNullableType; + + /// Generates code that compares value of a `case` expression with the + /// `switch` expression's value. Expects `case` and `switch` values to be on + /// stack, in that order. + late final void Function() compare; + + /// The `default: ...` case, if exists. + late final SwitchCase? defaultCase; + + /// The `null: ...` case, if exists. + late final SwitchCase? nullCase; + + SwitchInfo(CodeGenerator codeGen, SwitchStatement node) { + final translator = codeGen.translator; + + final switchExprClass = + translator.classForType(codeGen.dartTypeOf(node.expression)); + + bool check() => + node.cases.expand((c) => c.expressions).every((e) => + e is L || + e is NullLiteral || + (e is ConstantExpression && + (e.constant is C || e.constant is NullConstant) && + (translator.hierarchy.isSubtypeOf( + translator.classForType(codeGen.dartTypeOf(e)), + switchExprClass)))); + + if (node.cases.every((c) => + c.expressions.isEmpty && c.isDefault || + c.expressions.every((e) => + e is NullLiteral || + e is ConstantExpression && e.constant is NullConstant))) { + // default-only switch + nonNullableType = w.RefType.eq(nullable: false); + nullableType = w.RefType.eq(nullable: true); + compare = () => throw "Comparison in default-only switch"; + } else if (check()) { + // bool switch + nonNullableType = w.NumType.i32; + nullableType = + translator.classInfo[translator.boxedBoolClass]!.nullableType; + compare = () => codeGen.b.i32_eq(); + } else if (check()) { + // int switch + nonNullableType = w.NumType.i64; + nullableType = + translator.classInfo[translator.boxedIntClass]!.nullableType; + compare = () => codeGen.b.i64_eq(); + } else if (check()) { + // String switch + nonNullableType = + translator.classInfo[translator.stringBaseClass]!.nonNullableType; + nullableType = nonNullableType.withNullability(true); + compare = () => codeGen.call(translator.stringEquals.reference); + } else { + // Object switch + nonNullableType = translator.topInfo.nonNullableType; + nullableType = translator.topInfo.nullableType; + compare = () => codeGen.b.call(translator.functions + .getFunction(translator.coreTypes.identicalProcedure.reference)); + } + + // Special cases + defaultCase = node.cases + .cast() + .firstWhere((c) => c!.isDefault, orElse: () => null); + + nullCase = node.cases.cast().firstWhere( + (c) => c!.expressions.any((e) => + e is NullLiteral || + e is ConstantExpression && e.constant is NullConstant), + orElse: () => null); + } +} + enum _VirtualCallKind { Get, Set, diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart index a3b1971a321..5573188c24f 100644 --- a/pkg/dart2wasm/lib/functions.dart +++ b/pkg/dart2wasm/lib/functions.dart @@ -28,17 +28,6 @@ class FunctionCollector { // allocation of that class is encountered final Map> _pendingAllocation = {}; - final w.ValueType asyncStackType = const w.RefType.extern(nullable: true); - - late final w.FunctionType asyncStubFunctionType = m.addFunctionType( - [const w.RefType.struct(nullable: false), asyncStackType], - [translator.topInfo.nullableType]); - - late final w.StructType asyncStubBaseStruct = m.addStructType("#AsyncStub", - fields: [ - w.FieldType(w.RefType.def(asyncStubFunctionType, nullable: false)) - ]); - FunctionCollector(this.translator); w.Module get m => translator.m; @@ -167,27 +156,11 @@ class FunctionCollector { "${target.asMember}"); } - if (target.isAsyncInnerReference) { - w.BaseFunction outer = getFunction(target.asProcedure.reference); - return action( - _asyncInnerFunctionTypeFor(outer), "${outer.functionName} inner"); - } - final ftype = target.asMember.accept1(_FunctionTypeGenerator(translator), target); return action(ftype, "${target.asMember}"); } - w.DefinedFunction addAsyncInnerFunctionFor(w.BaseFunction outer) { - w.FunctionType ftype = _asyncInnerFunctionTypeFor(outer); - return m.addFunction(ftype, "${outer.functionName} inner"); - } - - w.FunctionType _asyncInnerFunctionTypeFor(w.BaseFunction outer) { - return m.addFunctionType([...outer.type.inputs, asyncStackType], - [translator.topInfo.nullableType]); - } - void activateSelector(SelectorInfo selector) { selector.targets.forEach((classId, target) { if (!target.asMember.isAbstract) { diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart index bdb7c948767..122d3ee4739 100644 --- a/pkg/dart2wasm/lib/intrinsics.dart +++ b/pkg/dart2wasm/lib/intrinsics.dart @@ -1517,23 +1517,6 @@ class Intrinsifier { } } - // _asyncBridge2 - if (member.enclosingLibrary.name == "dart.async" && - name == "_asyncBridge2") { - w.Local args = paramLocals[0]; - w.Local stack = paramLocals[1]; - const int stubFieldIndex = 0; - - b.local_get(args); - b.local_get(stack); - b.local_get(args); - b.ref_cast(w.RefType.def(translator.functions.asyncStubBaseStruct, - nullable: false)); - b.struct_get(translator.functions.asyncStubBaseStruct, stubFieldIndex); - b.call_ref(translator.functions.asyncStubFunctionType); - return true; - } - // int members if (member.enclosingClass == translator.boxedIntClass && member.function.body == null) { diff --git a/pkg/dart2wasm/lib/js/runtime_blob.dart b/pkg/dart2wasm/lib/js/runtime_blob.dart index 88871775e58..b5585e817e9 100644 --- a/pkg/dart2wasm/lib/js/runtime_blob.dart +++ b/pkg/dart2wasm/lib/js/runtime_blob.dart @@ -13,7 +13,6 @@ let buildArgsList; // the module will be instantiated. // This function returns a promise to the instantiated module. export const instantiate = async (modulePromise, importObjectPromise) => { - let asyncBridge; let dartInstance; function stringFromDartString(string) { const totalLength = dartInstance.exports.$stringLength(string); @@ -101,11 +100,6 @@ const jsRuntimeBlobPart2 = r''' ...(await importObjectPromise), }); - // Initialize async bridge. - asyncBridge = new WebAssembly.Function( - {parameters: ['anyref', 'anyref'], results: ['externref']}, - dartInstance.exports.$asyncBridge, - {promising: 'first'}); return dartInstance; } diff --git a/pkg/dart2wasm/lib/kernel_nodes.dart b/pkg/dart2wasm/lib/kernel_nodes.dart index d31bc35fb30..58f59f2cc59 100644 --- a/pkg/dart2wasm/lib/kernel_nodes.dart +++ b/pkg/dart2wasm/lib/kernel_nodes.dart @@ -87,6 +87,22 @@ mixin KernelNodes { late final Class syncStarIteratorClass = index.getClass("dart:core", "_SyncStarIterator"); + // async support classes + late final Class asyncSuspendStateClass = + index.getClass("dart:async", "_AsyncSuspendState"); + late final Procedure makeAsyncCompleter = + index.getTopLevelProcedure("dart:async", "_makeAsyncCompleter"); + late final Field completerFuture = + index.getField("dart:async", "_Completer", "future"); + late final Procedure completerComplete = + index.getProcedure("dart:async", "_AsyncCompleter", "complete"); + late final Procedure completerCompleteError = + index.getProcedure("dart:async", "_Completer", "completeError"); + late final Procedure awaitHelper = + index.getTopLevelProcedure("dart:async", "_awaitHelper"); + late final Procedure newAsyncSuspendState = + index.getTopLevelProcedure("dart:async", "_newAsyncSuspendState"); + // dart:ffi classes late final Class ffiCompoundClass = index.getClass("dart:ffi", "_Compound"); late final Class ffiPointerClass = index.getClass("dart:ffi", "Pointer"); @@ -133,12 +149,6 @@ mixin KernelNodes { late final Procedure checkLibraryIsLoaded = index.getTopLevelProcedure("dart:_internal", "checkLibraryIsLoaded"); - // dart:async procedures - late final Procedure asyncHelper = - index.getTopLevelProcedure("dart:async", "_asyncHelper"); - late final Procedure awaitHelper = - index.getTopLevelProcedure("dart:async", "_awaitHelper"); - // dart:collection procedures late final Procedure mapFactory = index.getProcedure("dart:collection", "LinkedHashMap", "_default"); @@ -249,4 +259,8 @@ mixin KernelNodes { index.getProcedure("dart:_wasm", "WasmFunction", "get:call"); late final Procedure wasmTableCallIndirect = index.getProcedure("dart:_wasm", "WasmTable", "callIndirect"); + + // Debugging + late final Procedure printToConsole = + index.getTopLevelProcedure("dart:_internal", "printToConsole"); } diff --git a/pkg/dart2wasm/lib/reference_extensions.dart b/pkg/dart2wasm/lib/reference_extensions.dart index ef0a015b5f5..d8d7a7211e0 100644 --- a/pkg/dart2wasm/lib/reference_extensions.dart +++ b/pkg/dart2wasm/lib/reference_extensions.dart @@ -32,23 +32,15 @@ extension GetterSetterReference on Reference { // implementation for that procedure. This enables a Reference to refer to any // implementation relating to a member, including its tear-off, which it can't // do in plain kernel. -// Also add an asyncInnerReference that refers to the inner, suspendable -// body of an async function, which returns the value to be put into a future. -// This can be called directly from other async functions if the result is -// directly awaited. // Use Expandos to avoid keeping the procedure alive. final Expando _tearOffReference = Expando(); -final Expando _asyncInnerReference = Expando(); final Expando _typeCheckerReference = Expando(); extension CustomReference on Member { Reference get tearOffReference => _tearOffReference[this] ??= Reference()..node = this; - Reference get asyncInnerReference => - _asyncInnerReference[this] ??= Reference()..node = this; - Reference get typeCheckerReference => _typeCheckerReference[this] ??= Reference()..node = this; } @@ -56,8 +48,6 @@ extension CustomReference on Member { extension IsCustomReference on Reference { bool get isTearOffReference => _tearOffReference[asMember] == this; - bool get isAsyncInnerReference => _asyncInnerReference[asMember] == this; - bool get isTypeCheckerReference => _typeCheckerReference[asMember] == this; } diff --git a/pkg/dart2wasm/lib/sync_star.dart b/pkg/dart2wasm/lib/sync_star.dart index 075bd9c35fa..99bb541b7b8 100644 --- a/pkg/dart2wasm/lib/sync_star.dart +++ b/pkg/dart2wasm/lib/sync_star.dart @@ -21,18 +21,18 @@ import 'package:wasm_builder/wasm_builder.dart' as w; /// initial entry target for a function body. /// - [After]: After a statement, the resumption point of a [YieldStatement], /// or the final state (iterator done) of a function body. -enum _StateTargetPlacement { Inner, After } +enum StateTargetPlacement { Inner, After } /// Representation of target in the `sync*` control flow graph. -class _StateTarget { +class StateTarget { int index; TreeNode node; - _StateTargetPlacement placement; + StateTargetPlacement placement; - _StateTarget(this.index, this.node, this.placement); + StateTarget(this.index, this.node, this.placement); String toString() { - String place = placement == _StateTargetPlacement.Inner ? "in" : "after"; + String place = placement == StateTargetPlacement.Inner ? "in" : "after"; return "$index: $place $node"; } } @@ -49,15 +49,15 @@ class _YieldFinder extends StatementVisitor { _YieldFinder(this.codeGen); - List<_StateTarget> get targets => codeGen.targets; + List get targets => codeGen.targets; void find(FunctionNode function) { // Initial state - addTarget(function.body!, _StateTargetPlacement.Inner); + addTarget(function.body!, StateTargetPlacement.Inner); assert(function.body is Block || function.body is ReturnStatement); recurse(function.body!); // Final state - addTarget(function.body!, _StateTargetPlacement.After); + addTarget(function.body!, StateTargetPlacement.After); } /// Recurse into a statement and then remove any targets added by the @@ -69,8 +69,8 @@ class _YieldFinder extends StatementVisitor { if (yieldCount == yieldCountIn) targets.length = targetsIn; } - void addTarget(TreeNode node, _StateTargetPlacement placement) { - targets.add(_StateTarget(targets.length, node, placement)); + void addTarget(TreeNode node, StateTargetPlacement placement) { + targets.add(StateTarget(targets.length, node, placement)); } @override @@ -89,71 +89,71 @@ class _YieldFinder extends StatementVisitor { @override void visitDoStatement(DoStatement node) { - addTarget(node, _StateTargetPlacement.Inner); + addTarget(node, StateTargetPlacement.Inner); recurse(node.body); } @override void visitForStatement(ForStatement node) { - addTarget(node, _StateTargetPlacement.Inner); + addTarget(node, StateTargetPlacement.Inner); recurse(node.body); - addTarget(node, _StateTargetPlacement.After); + addTarget(node, StateTargetPlacement.After); } @override void visitIfStatement(IfStatement node) { recurse(node.then); if (node.otherwise != null) { - addTarget(node, _StateTargetPlacement.Inner); + addTarget(node, StateTargetPlacement.Inner); recurse(node.otherwise!); } - addTarget(node, _StateTargetPlacement.After); + addTarget(node, StateTargetPlacement.After); } @override void visitLabeledStatement(LabeledStatement node) { recurse(node.body); - addTarget(node, _StateTargetPlacement.After); + addTarget(node, StateTargetPlacement.After); } @override void visitSwitchStatement(SwitchStatement node) { for (SwitchCase c in node.cases) { - addTarget(c, _StateTargetPlacement.Inner); + addTarget(c, StateTargetPlacement.Inner); recurse(c.body); } - addTarget(node, _StateTargetPlacement.After); + addTarget(node, StateTargetPlacement.After); } @override void visitTryCatch(TryCatch node) { recurse(node.body); for (Catch c in node.catches) { - addTarget(c, _StateTargetPlacement.Inner); + addTarget(c, StateTargetPlacement.Inner); recurse(c.body); } - addTarget(node, _StateTargetPlacement.After); + addTarget(node, StateTargetPlacement.After); } @override void visitTryFinally(TryFinally node) { recurse(node.body); - addTarget(node, _StateTargetPlacement.Inner); + addTarget(node, StateTargetPlacement.Inner); recurse(node.finalizer); - addTarget(node, _StateTargetPlacement.After); + addTarget(node, StateTargetPlacement.After); } @override void visitWhileStatement(WhileStatement node) { - addTarget(node, _StateTargetPlacement.Inner); + addTarget(node, StateTargetPlacement.Inner); recurse(node.body); - addTarget(node, _StateTargetPlacement.After); + addTarget(node, StateTargetPlacement.After); } @override void visitYieldStatement(YieldStatement node) { yieldCount++; - addTarget(node, _StateTargetPlacement.After); + addTarget(node, StateTargetPlacement.After); } } @@ -176,11 +176,11 @@ class SyncStarCodeGenerator extends CodeGenerator { SyncStarCodeGenerator(super.translator, super.function, super.reference); /// Targets of the CFG, indexed by target index. - final List<_StateTarget> targets = []; + final List targets = []; // Targets categorized by placement and indexed by node. - final Map innerTargets = {}; - final Map afterTargets = {}; + final Map innerTargets = {}; + final Map afterTargets = {}; /// The loop around the switch. late final w.Label masterLoop; @@ -226,10 +226,10 @@ class SyncStarCodeGenerator extends CodeGenerator { _YieldFinder(this).find(functionNode); for (final target in targets) { switch (target.placement) { - case _StateTargetPlacement.Inner: + case StateTargetPlacement.Inner: innerTargets[target.node] = target; break; - case _StateTargetPlacement.After: + case StateTargetPlacement.After: afterTargets[target.node] = target; break; } @@ -374,7 +374,7 @@ class SyncStarCodeGenerator extends CodeGenerator { b.unreachable(); // Initial state, executed on first [moveNext] on the iterator. - _StateTarget initialTarget = targets.first; + StateTarget initialTarget = targets.first; emitTargetLabel(initialTarget); // Clone context on first execution. @@ -390,7 +390,7 @@ class SyncStarCodeGenerator extends CodeGenerator { b.end(); } - void emitTargetLabel(_StateTarget target) { + void emitTargetLabel(StateTarget target) { currentTargetIndex++; assert(target.index == currentTargetIndex); b.end(); @@ -407,7 +407,7 @@ class SyncStarCodeGenerator extends CodeGenerator { b.return_(); } - void jumpToTarget(_StateTarget target, + void jumpToTarget(StateTarget target, {Expression? condition, bool negated = false}) { if (condition == null && negated) return; if (target.index > currentTargetIndex) { @@ -457,7 +457,7 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitDoStatement(DoStatement node) { - _StateTarget? inner = innerTargets[node]; + StateTarget? inner = innerTargets[node]; if (inner == null) return super.visitDoStatement(node); emitTargetLabel(inner); @@ -468,9 +468,9 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitForStatement(ForStatement node) { - _StateTarget? inner = innerTargets[node]; + StateTarget? inner = innerTargets[node]; if (inner == null) return super.visitForStatement(node); - _StateTarget after = afterTargets[node]!; + StateTarget after = afterTargets[node]!; allocateContext(node); for (VariableDeclaration variable in node.variables) { @@ -488,9 +488,9 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitIfStatement(IfStatement node) { - _StateTarget? after = afterTargets[node]; + StateTarget? after = afterTargets[node]; if (after == null) return super.visitIfStatement(node); - _StateTarget? inner = innerTargets[node]; + StateTarget? inner = innerTargets[node]; jumpToTarget(inner ?? after, condition: node.condition, negated: true); visitStatement(node.then); @@ -504,7 +504,7 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitLabeledStatement(LabeledStatement node) { - _StateTarget? after = afterTargets[node]; + StateTarget? after = afterTargets[node]; if (after == null) return super.visitLabeledStatement(node); visitStatement(node.body); @@ -513,7 +513,7 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitBreakStatement(BreakStatement node) { - _StateTarget? target = afterTargets[node.target]; + StateTarget? target = afterTargets[node.target]; if (target == null) return super.visitBreakStatement(node); jumpToTarget(target); @@ -521,7 +521,7 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitSwitchStatement(SwitchStatement node) { - _StateTarget? after = afterTargets[node]; + StateTarget? after = afterTargets[node]; if (after == null) return super.visitSwitchStatement(node); // TODO(51342): Implement this. @@ -530,7 +530,7 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitTryCatch(TryCatch node) { - _StateTarget? after = afterTargets[node]; + StateTarget? after = afterTargets[node]; if (after == null) return super.visitTryCatch(node); // TODO(51343): implement this. @@ -539,7 +539,7 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitTryFinally(TryFinally node) { - _StateTarget? after = afterTargets[node]; + StateTarget? after = afterTargets[node]; if (after == null) return super.visitTryFinally(node); // TODO(51343): implement this. @@ -548,9 +548,9 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitWhileStatement(WhileStatement node) { - _StateTarget? inner = innerTargets[node]; + StateTarget? inner = innerTargets[node]; if (inner == null) return super.visitWhileStatement(node); - _StateTarget after = afterTargets[node]!; + StateTarget after = afterTargets[node]!; emitTargetLabel(inner); jumpToTarget(after, condition: node.condition, negated: true); @@ -562,7 +562,7 @@ class SyncStarCodeGenerator extends CodeGenerator { @override void visitYieldStatement(YieldStatement node) { - _StateTarget after = afterTargets[node]!; + StateTarget after = afterTargets[node]!; // Evaluate operand and store it to `_current` for `yield` or // `_yieldStarIterable` for `yield*`. diff --git a/pkg/dart2wasm/lib/target.dart b/pkg/dart2wasm/lib/target.dart index 06fbb2b2393..0fff96fdd8c 100644 --- a/pkg/dart2wasm/lib/target.dart +++ b/pkg/dart2wasm/lib/target.dart @@ -29,9 +29,10 @@ import 'package:front_end/src/api_prototype/constant_evaluator.dart' import 'package:front_end/src/api_prototype/const_conditional_simplifier.dart' show ConstConditionalSimplifier; +import 'package:dart2wasm/await_transformer.dart' as awaitTrans; import 'package:dart2wasm/ffi_native_transformer.dart' as wasmFfiNativeTrans; -import 'package:dart2wasm/transformers.dart' as wasmTrans; import 'package:dart2wasm/records.dart' show RecordShape; +import 'package:dart2wasm/transformers.dart' as wasmTrans; class WasmTarget extends Target { WasmTarget({this.constantBranchPruning = true}); @@ -222,6 +223,8 @@ class WasmTarget extends Target { wasmTrans.transformLibraries( libraries, coreTypes, hierarchy, diagnosticReporter); + + awaitTrans.transformLibraries(libraries, hierarchy, coreTypes); } @override diff --git a/pkg/dart2wasm/tool/run_benchmark b/pkg/dart2wasm/tool/run_benchmark index 538b0f4a32b..6b8518913ba 100755 --- a/pkg/dart2wasm/tool/run_benchmark +++ b/pkg/dart2wasm/tool/run_benchmark @@ -26,7 +26,7 @@ SDK_DIR="$(cd "${PROG_DIR}/../../.." ; pwd -P)" # Hardcoded to x64 Linux for now. D8="$SDK_DIR/third_party/d8/linux/x64/d8" -D8_OPTIONS="--experimental-wasm-gc --experimental-wasm-stack-switching --experimental-wasm-type-reflection" +D8_OPTIONS="--experimental-wasm-gc --experimental-wasm-type-reflection" RUN_WASM="$SDK_DIR/pkg/dart2wasm/bin/run_wasm.js" diff --git a/pkg/front_end/testcases/dart2wasm/for_in.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2wasm/for_in.dart.strong.transformed.expect index dcae11c2b50..883420d2365 100644 --- a/pkg/front_end/testcases/dart2wasm/for_in.dart.strong.transformed.expect +++ b/pkg/front_end/testcases/dart2wasm/for_in.dart.strong.transformed.expect @@ -15,20 +15,38 @@ static method method(core::Iterable iterable) → dynamic { } } static method asyncMethod(asy::Stream stream) → dynamic async /* futureValueType= dynamic */ { + core::bool :async_temporary_0; + dynamic :async_temporary_1; { synthesized asy::_StreamIterator #forIterator = new asy::_StreamIterator::•(stream); synthesized core::bool #jumpSentinel = #C1; - try { - for (; #jumpSentinel = await #forIterator.{asy::_StreamIterator::moveNext}(){() → asy::Future}; ) { - core::int i = #forIterator.{asy::_StreamIterator::current}{core::int}; - { - core::print(i); + { + core::int #t1 = 0; + core::Object #t2; + core::StackTrace #t3; + try { + #L1: + for (; ; ) { + :async_temporary_0 = await #forIterator.{asy::_StreamIterator::moveNext}(){() → asy::Future}; + if(#jumpSentinel = :async_temporary_0 as dynamic) { + core::int i = #forIterator.{asy::_StreamIterator::current}{core::int}; + { + core::print(i); + } + } + else + break #L1; } } - } - finally { - if(#jumpSentinel) - await #forIterator.{asy::_StreamIterator::cancel}(){() → asy::Future}; + finally { + if(#jumpSentinel) { + :async_temporary_1 = await #forIterator.{asy::_StreamIterator::cancel}(){() → asy::Future}; + :async_temporary_1; + } + #t1; + #t2; + #t3; + } } } } diff --git a/pkg/front_end/testcases/dart2wasm/yield.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2wasm/yield.dart.strong.transformed.expect index 70ebc7b7b7d..d3864507bce 100644 --- a/pkg/front_end/testcases/dart2wasm/yield.dart.strong.transformed.expect +++ b/pkg/front_end/testcases/dart2wasm/yield.dart.strong.transformed.expect @@ -11,39 +11,65 @@ static method method(core::Iterable iterable) → core::Iterable stream) → asy::Stream { synthesized asy::StreamController #controller = asy::StreamController::•(); synthesized () → asy::Future #body = () → asy::Future async /* futureValueType= void */ { + core::bool :async_temporary_0; + core::bool :async_temporary_1; + core::bool :async_temporary_2; + core::bool :async_temporary_3; + core::bool :async_temporary_4; + dynamic :async_temporary_5; synthesized asy::Completer #completer = asy::Completer::•(); #controller.{asy::StreamController::add}(#completer){(core::Object?) → void}; - await #completer.{asy::Completer::future}{asy::Future}; + :async_temporary_0 = await #completer.{asy::Completer::future}{asy::Future}; + :async_temporary_0 as dynamic; { { #controller.{asy::StreamController::add}(1){(core::Object?) → void}; #completer = asy::Completer::•(); #controller.{asy::StreamController::add}(#completer){(core::Object?) → void}; - await #completer.{asy::Completer::future}{asy::Future}; + :async_temporary_1 = await #completer.{asy::Completer::future}{asy::Future}; + :async_temporary_1 as dynamic; } { #controller.{asy::StreamController::add}(2){(core::Object?) → void}; #completer = asy::Completer::•(); #controller.{asy::StreamController::add}(#completer){(core::Object?) → void}; - await #completer.{asy::Completer::future}{asy::Future}; + :async_temporary_2 = await #completer.{asy::Completer::future}{asy::Future}; + :async_temporary_2 as dynamic; } { synthesized asy::_StreamIterator #forIterator = new asy::_StreamIterator::•(stream); synthesized core::bool #jumpSentinel = #C1; - try { - for (; #jumpSentinel = await #forIterator.{asy::_StreamIterator::moveNext}(){() → asy::Future}; ) { - synthesized core::int #awaitForVar = #forIterator.{asy::_StreamIterator::current}{core::int}; - { - #controller.{asy::StreamController::add}(#awaitForVar){(core::Object?) → void}; - #completer = asy::Completer::•(); - #controller.{asy::StreamController::add}(#completer){(core::Object?) → void}; - await #completer.{asy::Completer::future}{asy::Future}; + { + core::int #t1 = 0; + core::Object #t2; + core::StackTrace #t3; + try { + #L1: + for (; ; ) { + :async_temporary_4 = await #forIterator.{asy::_StreamIterator::moveNext}(){() → asy::Future}; + if(#jumpSentinel = :async_temporary_4 as dynamic) { + synthesized core::int #awaitForVar = #forIterator.{asy::_StreamIterator::current}{core::int}; + { + #controller.{asy::StreamController::add}(#awaitForVar){(core::Object?) → void}; + #completer = asy::Completer::•(); + #controller.{asy::StreamController::add}(#completer){(core::Object?) → void}; + :async_temporary_3 = await #completer.{asy::Completer::future}{asy::Future}; + :async_temporary_3 as dynamic; + } + } + else + break #L1; } } - } - finally { - if(#jumpSentinel) - await #forIterator.{asy::_StreamIterator::cancel}(){() → asy::Future}; + finally { + if(#jumpSentinel) { + :async_temporary_5 = await #forIterator.{asy::_StreamIterator::cancel}(){() → asy::Future}; + :async_temporary_5; + } + #t1; + #t2; + #t3; + } } } } diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart index b49ab84f4a9..ab3f34f7cc2 100644 --- a/pkg/test_runner/lib/src/compiler_configuration.dart +++ b/pkg/test_runner/lib/src/compiler_configuration.dart @@ -593,7 +593,6 @@ class Dart2WasmCompilerConfiguration extends CompilerConfiguration { final args = testFile.dartOptions; return [ '--experimental-wasm-gc', - '--experimental-wasm-stack-switching', '--experimental-wasm-type-reflection', 'pkg/dart2wasm/bin/run_wasm.js', '--', diff --git a/pkg/vm/lib/transformations/type_flow/summary_collector.dart b/pkg/vm/lib/transformations/type_flow/summary_collector.dart index a9aa541d9c0..66c05e5af84 100644 --- a/pkg/vm/lib/transformations/type_flow/summary_collector.dart +++ b/pkg/vm/lib/transformations/type_flow/summary_collector.dart @@ -259,6 +259,10 @@ class _FallthroughDetector extends ast.StatementVisitor { bool visitBlock(Block node) => node.statements.isEmpty || node.statements.last.accept(this); + @override + bool visitAssertBlock(AssertBlock node) => + node.statements.isEmpty || node.statements.last.accept(this); + @override bool visitEmptyStatement(EmptyStatement node) => true; diff --git a/sdk/bin/run_dart2wasm_d8 b/sdk/bin/run_dart2wasm_d8 index 05811abbe98..32bd6d6469d 100755 --- a/sdk/bin/run_dart2wasm_d8 +++ b/sdk/bin/run_dart2wasm_d8 @@ -36,6 +36,6 @@ if [[ $D8_OPTIONS ]]; then fi # Find the JS runtime based on the input wasm file. -exec "$D8_EXEC" --experimental-wasm-gc --experimental-wasm-stack-switching \ +exec "$D8_EXEC" --experimental-wasm-gc \ --experimental-wasm-type-reflection "${EXTRA_D8_OPTIONS[@]}" \ "$SDK_DIR/pkg/dart2wasm/bin/run_wasm.js" -- "$(realpath -- "${1%.*}.mjs")" "$@" diff --git a/sdk/lib/_internal/wasm/lib/async_patch.dart b/sdk/lib/_internal/wasm/lib/async_patch.dart index 46354354e80..5372cd1a7e0 100644 --- a/sdk/lib/_internal/wasm/lib/async_patch.dart +++ b/sdk/lib/_internal/wasm/lib/async_patch.dart @@ -1,137 +1,86 @@ -// Copyright (c) 2022, 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. - -// Machinery for async and await. -// -// The implementation is based on two mechanisms in the JS Promise integration: -// -// The export wrapper: Allocates a new stack and calls the wrapped export on the -// new stack, passing a suspender object as an extra first argument that -// represents the new stack. -// -// The import wrapper: Takes a suspender object as an extra first argument and -// calls the wrapped import. If the wrapped import returns a `Promise`, the -// current stack is suspended, and the `Promise` is forwarded to the -// corresponding call of the export wrapper, where execution resumes on the -// original stack. When the `Promise` is resolved, execution resumes on the -// suspended stack, with the call to the import wrapper returning the value the -// `Promise` was resolved with. -// -// The call sequence when calling an async function is: -// -// Caller -// -> Outer (function specific, generated by `generateAsyncWrapper`) -// -> `_asyncHelper` -// -> `_callAsyncBridge` (imported JS function) -// -> `_asyncBridge` (via the Promise integration export wrapper) -// -> `_asyncBridge2` (intrinsic function) -// -> Stub (function specific, generated by `generateAsyncWrapper`) -// -> Inner (contains implementation, generated from async inner reference) -// -// The call sequence on await is: -// -// Function containing await -// -> `_awaitHelper` -// -> `_futurePromise` (via the Promise integration import wrapper) -// -> `new Promise` -// -> `Promise` constructor callback -// -> `_awaitCallback` -// -> `Future.then` -// `futurePromise` returns the newly created `Promise`, suspending the -// current execution. -// -// When the `Future` completes: -// -// `Future.then` callback -// -> `_callResolve` (imported JS function) -// -> `Promise` resolve function -// Resolving the `Promise` causes the suspended execution to resume. - -import 'dart:_internal' show patch, scheduleCallback, unsafeCastOpaque; -import 'dart:_js_helper' show JS; +import 'dart:_internal' show scheduleCallback, patch, _AsyncCompleter; import 'dart:_wasm'; part 'timer_patch.dart'; -@pragma("wasm:entry-point") -Future _asyncHelper(WasmStructRef args) { - Completer completer = Completer(); - _callAsyncBridge(args, completer); - return completer.future; -} - -void _callAsyncBridge(WasmStructRef args, Completer completer) => - // This trampoline is needed because [asyncBridge] is a function wrapped - // by `returnPromiseOnSuspend`, and the stack-switching functionality of - // that wrapper is implemented as part of the export adapter. - JS( - "(args, completer) => asyncBridge(args, completer)", args, completer); - -@pragma("wasm:export", "\$asyncBridge") -WasmExternRef? _asyncBridge( - WasmExternRef? stack, WasmStructRef args, Completer completer) { - try { - Object? result = _asyncBridge2(args, stack); - completer.complete(result); - } catch (e, s) { - completer.completeError(e, s); - } -} - -external Object? _asyncBridge2(WasmStructRef args, WasmExternRef? stack); - -class _FutureError { - final Object exception; - final StackTrace stackTrace; - - _FutureError(this.exception, this.stackTrace); -} +typedef _AsyncResumeFun = WasmFunction< + void Function( + _AsyncSuspendState, + // Value of the last `await` + Object?, + // If the last `await` throwed an error, the error value + Object?, + // If the last `await` throwed an error, the stack trace + StackTrace?)>; @pragma("wasm:entry-point") -Object? _awaitHelper(Object? operand, WasmExternRef? stack) { - // Save the existing zone in a local, and restore('_leave') upon returning. We - // ensure that the zone will be restored in the event of an exception by - // restoring the original zone before we throw the exception. - _Zone current = Zone._current; +class _AsyncSuspendState { + // The inner function. + // + // Note: this function never throws. Any uncaught exceptions are passed to + // `_completer.completeError`. + @pragma("wasm:entry-point") + final _AsyncResumeFun _resume; + + // Context containing the local variables of the function. + @pragma("wasm:entry-point") + final WasmStructRef? _context; + + // CFG target index for the next resumption. + @pragma("wasm:entry-point") + WasmI32 _targetIndex; + + // The completer. The inner function calls `_completer.complete` or + // `_completer.onError` on completion. + @pragma("wasm:entry-point") + final _AsyncCompleter _completer; + + // When a called function throws this stores the thrown exception. Used when + // performing type tests in catch blocks. + @pragma("wasm:entry-point") + Object? _currentException; + + // When a called function throws this stores the stack trace. + @pragma("wasm:entry-point") + StackTrace? _currentExceptionStackTrace; + + // When running finalizers and the continuation is "return", the value to + // return after the last finalizer. + // + // Used in finalizer blocks. + @pragma("wasm:entry-point") + Object? _currentReturnValue; + + @pragma("wasm:entry-point") + _AsyncSuspendState(this._resume, this._context, this._completer) + : _targetIndex = WasmI32.fromInt(0), + _currentException = null, + _currentExceptionStackTrace = null, + _currentReturnValue = null; +} + +// Note: [_AsyncCompleter] is taken as an argument to be able to pass the type +// parameter to [_AsyncCompleter] without having to add a type parameter to +// [_AsyncSuspendState]. +// +// TODO (omersa): I'm not sure if the type parameter is necessary? +@pragma("wasm:entry-point") +_AsyncSuspendState _newAsyncSuspendState(_AsyncResumeFun resume, + WasmStructRef? context, _AsyncCompleter completer) => + _AsyncSuspendState(resume, context, completer); + +@pragma("wasm:entry-point") +_AsyncCompleter _makeAsyncCompleter() => _AsyncCompleter(); + +@pragma("wasm:entry-point") +void _awaitHelper(_AsyncSuspendState suspendState, Object? operand) { if (operand is! Future) { operand = Future.value(operand); } - Object? result = _futurePromise(stack, operand); - Zone._leave(current); - if (result is _FutureError) { - // TODO(joshualitt): `result.stackTrace` is not currently the complete stack - // trace. We might be able to stitch together `result.stackTrace` with - // `StackTrace.current`, but we would need to be able to handle the case - // where `result.stackTrace` is supplied by the user and must then be exact. - // Alternatively, we may be able to fix this when we actually generate stack - // traces. - Error.throwWithStackTrace(result.exception, result.stackTrace); - } - return result; -} - -Object? _futurePromise(WasmExternRef? stack, Future future) => - JS("""new WebAssembly.Function( - {parameters: ['externref', 'anyref'], results: ['anyref']}, - function(future) { - return new Promise(function (resolve, reject) { - dartInstance.exports.\$awaitCallback(future, resolve); - }); - }, - {suspending: 'first'})""", stack, future); - -@pragma("wasm:export", "\$awaitCallback") -void _awaitCallback(Future future, WasmExternRef? resolve) { - future.then((value) { - _callResolve(resolve, value); + operand.then((value) { + suspendState._resume.call(suspendState, value, null, null); }, onError: (exception, stackTrace) { - _callResolve(resolve, _FutureError(exception, stackTrace)); + suspendState._resume.call(suspendState, null, exception, stackTrace); }); } - -void _callResolve(WasmExternRef? resolve, Object? result) => - // This trampoline is needed because [resolve] is a JS function that - // can't be called directly from Wasm. - JS("(resolve, result) => resolve(result)", resolve, result); diff --git a/sdk/lib/async/future_impl.dart b/sdk/lib/async/future_impl.dart index b92c5b0fcb5..7ccc1ff1c97 100644 --- a/sdk/lib/async/future_impl.dart +++ b/sdk/lib/async/future_impl.dart @@ -5,11 +5,13 @@ part of dart.async; abstract class _Completer implements Completer { + @pragma("wasm:entry-point") final _Future future = new _Future(); // Overridden by either a synchronous or asynchronous implementation. void complete([FutureOr? value]); + @pragma("wasm:entry-point") void completeError(Object error, [StackTrace? stackTrace]) { // TODO(40614): Remove once non-nullability is sound. checkNotNullable(error, "error"); @@ -34,6 +36,7 @@ abstract class _Completer implements Completer { /// Completer which completes future asynchronously. class _AsyncCompleter extends _Completer { + @pragma("wasm:entry-point") void complete([FutureOr? value]) { if (!future._mayComplete) throw new StateError("Future already completed"); future._asyncComplete(value == null ? value as dynamic : value);