Prototype integration of shared type analysis logic with CFE.

This change replaces the InferenceVisitorImpl.visitSwitchStatement
with a call to the new shared type analyzer, and implements the
necessary callbacks to allow analysis to work end-to-end.

Change-Id: I73a9514428b4ce092762d3ea450545986d0e1ea9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/257491
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart
index acdc16e..a348e48 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
+import 'package:_fe_analyzer_shared/src/type_inference/type_analysis_result.dart';
+import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer.dart';
+import 'package:_fe_analyzer_shared/src/type_inference/type_operations.dart';
 import 'package:_fe_analyzer_shared/src/util/link.dart';
 import 'package:front_end/src/api_prototype/lowering_predicates.dart';
 import 'package:kernel/ast.dart';
@@ -42,6 +45,7 @@
 import 'inference_results.dart';
 import 'inference_visitor_base.dart';
 import 'object_access_target.dart';
+import 'shared_type_analyzer.dart';
 import 'type_constraint_gatherer.dart';
 import 'type_inference_engine.dart';
 import 'type_inferrer.dart' show TypeInferrerImpl;
@@ -74,19 +78,62 @@
 }
 
 class InferenceVisitorImpl extends InferenceVisitorBase
+    with
+        TypeAnalyzer<Node, Statement, Expression, VariableDeclaration, DartType>
     implements
         ExpressionVisitor1<ExpressionInferenceResult, DartType>,
         StatementVisitor<StatementInferenceResult>,
         InitializerVisitor<InitializerInferenceResult>,
         InferenceVisitor {
+  /// Debug-only: if `true`, manipulations of [_rewriteStack] performed by
+  /// [popRewrite] and [pushRewrite] will be printed.
+  static const bool _debugRewriteStack = false;
+
   Class? mapEntryClass;
 
+  @override
+  final TypeOperations2<DartType> typeOperations;
+
   /// Context information for the current closure, or `null` if we are not
   /// inside a closure.
   ClosureContext? _closureContext;
 
-  InferenceVisitorImpl(TypeInferrerImpl inferrer, InferenceHelper? helper)
-      : super(inferrer, helper);
+  /// If a switch statement is being visited and the type being switched on is a
+  /// (possibly nullable) enumerated type, the set of enum values for which no
+  /// case head has been seen yet; otherwise `null`.
+  ///
+  /// Enum values are represented by the [Field] object they are desugared into.
+  /// If the type being switched on is nullable, then this set also includes a
+  /// value of `null` if no case head has been seen yet that handles `null`.
+  Set<Field?>? _enumFields;
+
+  /// Stack for obtaining rewritten expressions and statements.  After
+  /// [dispatchExpression] or [dispatchStatement] visits a node for type
+  /// inference, the visited node (which may have been changed by the inference
+  /// process) is pushed onto this stack.  Later, during the processing of the
+  /// enclosing node, the visited node is popped off the stack again, and the
+  /// enclosing node is updated to point to the new, rewritten node.
+  ///
+  /// The stack sometimes contains `null`s.  These account for situations where
+  /// it's necessary to push a value onto the stack to balance a later pop, but
+  /// there is no suitable expression or statement to push.
+  final List<Node?> _rewriteStack = [];
+
+  @override
+  final TypeAnalyzerOptions options;
+
+  @override
+  late final SharedTypeAnalyzerErrors? errors = isTopLevel
+      ? null
+      : new SharedTypeAnalyzerErrors(
+          helper: helper, uriForInstrumentation: uriForInstrumentation);
+
+  InferenceVisitorImpl(
+      TypeInferrerImpl inferrer, InferenceHelper? helper, this.typeOperations)
+      : options = new TypeAnalyzerOptions(
+            nullSafetyEnabled: inferrer.libraryBuilder.isNonNullableByDefault,
+            patternsEnabled: false),
+        super(inferrer, helper);
 
   ClosureContext get closureContext => _closureContext!;
 
@@ -6580,111 +6627,32 @@
 
   @override
   StatementInferenceResult visitSwitchStatement(SwitchStatement node) {
-    ExpressionInferenceResult expressionResult = inferExpression(
-        node.expression, const UnknownType(), true,
-        isVoidAllowed: false);
-    node.expression = expressionResult.expression..parent = node;
-    DartType expressionType = expressionResult.inferredType;
-
-    Set<Field?>? enumFields;
-    if (expressionType is InterfaceType && expressionType.classNode.isEnum) {
-      enumFields = <Field?>{
-        ...expressionType.classNode.fields
-            .where((Field field) => field.isEnumElement)
-      };
-      if (expressionType.isPotentiallyNullable) {
-        enumFields.add(null);
-      }
+    // Stack: ()
+    Set<Field?>? previousEnumFields = _enumFields;
+    Expression expression = node.expression;
+    SwitchStatementTypeAnalysisResult<DartType> analysisResult =
+        analyzeSwitchStatement(node, expression, node.cases.length);
+    // Note that a switch statement with a `default` clause is always considered
+    // exhaustive, but the kernel format also keeps track of whether the switch
+    // statement is "explicitly exhaustive", meaning that it has a `case` clause
+    // for every possible enum value.  So if there's a `default` clause we need
+    // to call `isSwitchExhaustive` to figure out whether the switch is
+    // *explicitly* exhaustive.
+    node.isExplicitlyExhaustive = analysisResult.hasDefault
+        ? isSwitchExhaustive(node, analysisResult.scrutineeType)
+        : analysisResult.isExhaustive;
+    _enumFields = previousEnumFields;
+    // Stack: (Expression)
+    Node? rewrite = popRewrite();
+    if (!identical(expression, rewrite)) {
+      expression = rewrite as Expression;
+      node.expression = expression..parent = node;
     }
-
-    flowAnalysis.switchStatement_expressionEnd(node);
-
-    bool hasDefault = false;
-    bool lastCaseTerminates = true;
-    for (int caseIndex = 0; caseIndex < node.cases.length; ++caseIndex) {
-      SwitchCaseImpl switchCase = node.cases[caseIndex] as SwitchCaseImpl;
-      hasDefault = hasDefault || switchCase.isDefault;
-      flowAnalysis.switchStatement_beginCase();
-      flowAnalysis.switchStatement_beginAlternatives();
-      flowAnalysis.switchStatement_endAlternative();
-      flowAnalysis.switchStatement_endAlternatives(node,
-          hasLabels: switchCase.hasLabel);
-      for (int index = 0; index < switchCase.expressions.length; index++) {
-        ExpressionInferenceResult caseExpressionResult = inferExpression(
-            switchCase.expressions[index], expressionType, true,
-            isVoidAllowed: false);
-        Expression caseExpression = caseExpressionResult.expression;
-        switchCase.expressions[index] = caseExpression..parent = switchCase;
-        DartType caseExpressionType = caseExpressionResult.inferredType;
-        if (enumFields != null) {
-          if (caseExpression is StaticGet) {
-            enumFields.remove(caseExpression.target);
-          } else if (caseExpression is NullLiteral) {
-            enumFields.remove(null);
-          }
-        }
-
-        if (!isTopLevel) {
-          if (libraryBuilder.isNonNullableByDefault) {
-            if (!typeSchemaEnvironment.isSubtypeOf(caseExpressionType,
-                expressionType, SubtypeCheckMode.withNullabilities)) {
-              helper.addProblem(
-                  templateSwitchExpressionNotSubtype.withArguments(
-                      caseExpressionType,
-                      expressionType,
-                      isNonNullableByDefault),
-                  caseExpression.fileOffset,
-                  noLength,
-                  context: [
-                    messageSwitchExpressionNotAssignableCause.withLocation(
-                        uriForInstrumentation,
-                        node.expression.fileOffset,
-                        noLength)
-                  ]);
-            }
-          } else {
-            // Check whether the expression type is assignable to the case
-            // expression type.
-            if (!isAssignable(expressionType, caseExpressionType)) {
-              helper.addProblem(
-                  templateSwitchExpressionNotAssignable.withArguments(
-                      expressionType,
-                      caseExpressionType,
-                      isNonNullableByDefault),
-                  caseExpression.fileOffset,
-                  noLength,
-                  context: [
-                    messageSwitchExpressionNotAssignableCause.withLocation(
-                        uriForInstrumentation,
-                        node.expression.fileOffset,
-                        noLength)
-                  ]);
-            }
-          }
-        }
-      }
-      StatementInferenceResult bodyResult = inferStatement(switchCase.body);
-      if (bodyResult.hasChanged) {
-        switchCase.body = bodyResult.statement..parent = switchCase;
-      }
-
-      if (isNonNullableByDefault) {
-        lastCaseTerminates = !flowAnalysis.isReachable;
-        if (!isTopLevel) {
-          // The last case block is allowed to complete normally.
-          if (caseIndex < node.cases.length - 1 && flowAnalysis.isReachable) {
-            libraryBuilder.addProblem(messageSwitchCaseFallThrough,
-                switchCase.fileOffset, noLength, helper.uri);
-          }
-        }
-      }
-    }
-    node.isExplicitlyExhaustive = enumFields != null && enumFields.isEmpty;
-    bool isExhaustive = node.isExplicitlyExhaustive || hasDefault;
-    flowAnalysis.switchStatement_end(isExhaustive);
     Statement? replacement;
-    if (isExhaustive && !hasDefault && shouldThrowUnsoundnessException) {
-      if (!lastCaseTerminates) {
+    if (analysisResult.isExhaustive &&
+        !analysisResult.hasDefault &&
+        shouldThrowUnsoundnessException) {
+      if (!analysisResult.lastCaseTerminates) {
         LabeledStatement breakTarget;
         if (node.parent is LabeledStatement) {
           breakTarget = node.parent as LabeledStatement;
@@ -7569,6 +7537,219 @@
       }
     }
   }
+
+  /// Pops the top entry off of [_rewriteStack].
+  Node? popRewrite() {
+    Node? expression = _rewriteStack.removeLast();
+    if (_debugRewriteStack) {
+      assert(_debugPrint('POP ${expression.runtimeType} $expression'));
+    }
+    return expression;
+  }
+
+  /// Pushes an entry onto [_rewriteStack].
+  void pushRewrite(Node? node) {
+    if (_debugRewriteStack) {
+      assert(_debugPrint('PUSH ${node.runtimeType} $node'));
+    }
+    _rewriteStack.add(node);
+  }
+
+  /// Helper function used to print information to the console in debug mode.
+  /// This method returns `true` so that it can be conveniently called inside of
+  /// an `assert` statement.
+  bool _debugPrint(String s) {
+    print(s);
+    return true;
+  }
+
+  @override
+  DartType get boolType => throw new UnimplementedError('TODO(paulberry)');
+
+  @override
+  ExpressionTypeAnalysisResult<DartType> dispatchExpression(
+      Expression node, DartType context) {
+    ExpressionInferenceResult expressionResult =
+        inferExpression(node, context, true).stopShorting();
+    pushRewrite(expressionResult.expression);
+    return new SimpleTypeAnalysisResult(type: expressionResult.inferredType);
+  }
+
+  @override
+  void dispatchPattern(
+      DartType matchedType,
+      Map<VariableDeclaration, VariableTypeInfo<Node, DartType>> typeInfos,
+      MatchContext<Node, Expression> context,
+      Node node) {
+    // The front end's representation of a switch cases currently doesn't have
+    // any support for patterns; each case is represented as an expression.  So
+    // analyze it as a constant pattern.
+    return analyzeConstantPattern(
+        matchedType, typeInfos, context, node, node as Expression);
+  }
+
+  @override
+  DartType dispatchPatternSchema(Node node) {
+    // The front end's representation of a switch cases currently doesn't have
+    // any support for patterns; each case is represented as an expression.  So
+    // analyze it as a constant pattern.
+    return analyzeConstantPatternSchema();
+  }
+
+  @override
+  void dispatchStatement(Statement statement) {
+    StatementInferenceResult result = inferStatement(statement);
+    pushRewrite(result.hasChanged ? result.statement : statement);
+  }
+
+  @override
+  DartType get doubleType => throw new UnimplementedError('TODO(paulberry)');
+
+  @override
+  DartType get dynamicType => throw new UnimplementedError('TODO(paulberry)');
+
+  @override
+  void finishExpressionCase(Expression node, int caseIndex) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void handleMergedStatementCase(covariant SwitchStatement node,
+      {required int caseIndex,
+      required int executionPathIndex,
+      required int numStatements}) {
+    assert(numStatements == 1 || numStatements == 0);
+    if (numStatements == 1) {
+      // Stack: (Statement)
+      SwitchCase case_ = node.cases[executionPathIndex];
+      Statement body = case_.body;
+      Node? rewrite = popRewrite();
+      // Stack: ()
+      if (!identical(body, rewrite)) {
+        body = rewrite as Statement;
+        case_.body = body..parent = case_;
+      }
+    }
+  }
+
+  @override
+  FlowAnalysis<Node, Statement, Expression, VariableDeclaration, DartType>?
+      get flow => flowAnalysis;
+
+  @override
+  SwitchExpressionMemberInfo<Node, Expression> getSwitchExpressionMemberInfo(
+      Expression node, int index) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  SwitchStatementMemberInfo<Node, Statement, Expression>
+      getSwitchStatementMemberInfo(
+          covariant SwitchStatement node, int caseIndex) {
+    SwitchCaseImpl case_ = node.cases[caseIndex] as SwitchCaseImpl;
+    return new SwitchStatementMemberInfo([
+      for (Expression expression in case_.expressions)
+        new CaseHeadOrDefaultInfo(pattern: expression),
+      if (case_.isDefault) new CaseHeadOrDefaultInfo(pattern: null)
+    ], [
+      case_.body
+    ], labels: case_.hasLabel ? [case_] : const []);
+  }
+
+  @override
+  void handleCaseHead(covariant SwitchStatement node,
+      {required int caseIndex, required int subIndex}) {
+    // Stack: (Pattern, Expression)
+    popRewrite(); // "when" expression
+    // Stack: (Pattern)
+    SwitchCaseImpl case_ = node.cases[caseIndex] as SwitchCaseImpl;
+    Expression expression = case_.expressions[subIndex];
+    Node? rewrite = popRewrite();
+    // Stack: ()
+    if (!identical(expression, rewrite)) {
+      expression = rewrite as Expression;
+      case_.expressions[subIndex] = expression..parent = case_;
+    }
+    Set<Field?>? enumFields = _enumFields;
+    if (enumFields != null) {
+      if (expression is StaticGet) {
+        enumFields.remove(expression.target);
+      } else if (expression is NullLiteral) {
+        enumFields.remove(null);
+      }
+    }
+  }
+
+  @override
+  void handleCase_afterCaseHeads(Statement node, int caseIndex, int numHeads) {}
+
+  @override
+  void handleDefault(Node node, int caseIndex) {}
+
+  @override
+  void handleNoStatement(Statement node) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void handleNoGuard(Node node, int caseIndex) {
+    // Stack: ()
+    pushRewrite(null);
+    // Stack: (Expression)
+  }
+
+  @override
+  void handleSwitchScrutinee(DartType type) {
+    if (type is InterfaceType && type.classNode.isEnum) {
+      _enumFields = <Field?>{
+        ...type.classNode.fields.where((Field field) => field.isEnumElement),
+        if (type.isPotentiallyNullable) null
+      };
+    } else {
+      _enumFields = null;
+    }
+  }
+
+  @override
+  DartType get intType => throw new UnimplementedError('TODO(paulberry)');
+
+  @override
+  bool isSwitchExhaustive(Node node, DartType expressionType) {
+    Set<Field?>? enumFields = _enumFields;
+    return enumFields != null && enumFields.isEmpty;
+  }
+
+  @override
+  bool isVariablePattern(Node node) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  DartType listType(DartType elementType) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  DartType get objectQuestionType =>
+      throw new UnimplementedError('TODO(paulberry)');
+
+  @override
+  void setVariableType(VariableDeclaration variable, DartType type) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  DartType get unknownType => const UnknownType();
+
+  @override
+  DartType variableTypeFromInitializerType(DartType type) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void checkCleanState() {
+    assert(_rewriteStack.isEmpty);
+  }
 }
 
 /// Offset and type information collection in [InferenceVisitor.inferMapEntry].
diff --git a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart
index eab3dd4..af056bf 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart
@@ -179,6 +179,10 @@
   FlowAnalysis<TreeNode, Statement, Expression, VariableDeclaration, DartType>
       get flowAnalysis => _inferrer.flowAnalysis;
 
+  /// Provides access to the [OperationsCfe] object.  This is needed by
+  /// [isAssignable].
+  OperationsCfe get operations => _inferrer.operations;
+
   TypeSchemaEnvironment get typeSchemaEnvironment =>
       _inferrer.typeSchemaEnvironment;
 
@@ -386,18 +390,8 @@
         typeContext.classNode == coreTypes.doubleClass;
   }
 
-  bool isAssignable(DartType contextType, DartType expressionType) {
-    if (isNonNullableByDefault) {
-      if (expressionType is DynamicType) return true;
-      return typeSchemaEnvironment
-          .performNullabilityAwareSubtypeCheck(expressionType, contextType)
-          .isSubtypeWhenUsingNullabilities();
-    }
-    return typeSchemaEnvironment
-        .performNullabilityAwareSubtypeCheck(expressionType, contextType)
-        .orSubtypeCheckFor(contextType, expressionType, typeSchemaEnvironment)
-        .isSubtypeWhenIgnoringNullabilities();
-  }
+  bool isAssignable(DartType contextType, DartType expressionType) =>
+      operations.isAssignableTo(expressionType, contextType);
 
   /// Ensures that [expressionType] is assignable to [contextType].
   ///
@@ -4098,6 +4092,11 @@
         charOffset,
         length);
   }
+
+  /// The client of type inference should call this method after asking
+  /// inference to visit a node.  This performs assertions to make sure that
+  /// temporary type inference state has been properly cleaned up.
+  void checkCleanState();
 }
 
 /// Describes assignability kind of one type to another.
diff --git a/pkg/front_end/lib/src/fasta/type_inference/shared_type_analyzer.dart b/pkg/front_end/lib/src/fasta/type_inference/shared_type_analyzer.dart
new file mode 100644
index 0000000..e6ebd79
--- /dev/null
+++ b/pkg/front_end/lib/src/fasta/type_inference/shared_type_analyzer.dart
@@ -0,0 +1,95 @@
+// 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.md file.
+
+import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer.dart';
+import 'package:kernel/ast.dart';
+
+import '../fasta_codes.dart';
+import 'inference_helper.dart';
+
+/// Implementation of [TypeAnalyzerErrors] that reports errors using the
+/// front end's [InferenceHelper] class.
+class SharedTypeAnalyzerErrors
+    implements
+        TypeAnalyzerErrors<Node, Statement, Expression, VariableDeclaration,
+            DartType> {
+  final InferenceHelper helper;
+
+  final Uri uriForInstrumentation;
+
+  SharedTypeAnalyzerErrors(
+      {required this.helper, required this.uriForInstrumentation});
+
+  @override
+  void assertInErrorRecovery() {
+    // TODO(paulberry): figure out how to do this.
+  }
+
+  @override
+  void caseExpressionTypeMismatch(
+      {required Expression scrutinee,
+      required Expression caseExpression,
+      required caseExpressionType,
+      required scrutineeType,
+      required bool nullSafetyEnabled}) {
+    helper.addProblem(
+        nullSafetyEnabled
+            ? templateSwitchExpressionNotSubtype.withArguments(
+                caseExpressionType, scrutineeType, nullSafetyEnabled)
+            : templateSwitchExpressionNotAssignable.withArguments(
+                scrutineeType, caseExpressionType, nullSafetyEnabled),
+        caseExpression.fileOffset,
+        noLength,
+        context: [
+          messageSwitchExpressionNotAssignableCause.withLocation(
+              uriForInstrumentation, scrutinee.fileOffset, noLength)
+        ]);
+  }
+
+  @override
+  void inconsistentMatchVar(
+      {required Node pattern,
+      required DartType type,
+      required Node previousPattern,
+      required DartType previousType}) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void inconsistentMatchVarExplicitness(
+      {required Node pattern, required Node previousPattern}) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void nonBooleanCondition(Expression node) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void patternDoesNotAllowLate(Node pattern) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void patternTypeMismatchInIrrefutableContext(
+      {required Node pattern,
+      required Node context,
+      required DartType matchedType,
+      required DartType requiredType}) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void refutablePatternInIrrefutableContext(Node pattern, Node context) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  void switchCaseCompletesNormally(
+      covariant SwitchStatement node, int caseIndex, int numMergedCases) {
+    helper.addProblem(messageSwitchCaseFallThrough,
+        node.cases[caseIndex].fileOffset, noLength);
+  }
+}
diff --git a/pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart b/pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart
index 266c506..380608f 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart
@@ -300,11 +300,13 @@
 
 /// CFE-specific implementation of [TypeOperations].
 class OperationsCfe
-    with TypeOperations<DartType>
+    with TypeOperations<DartType>, TypeOperations2<DartType>
     implements Operations<VariableDeclaration, DartType> {
   final TypeEnvironment typeEnvironment;
 
-  OperationsCfe(this.typeEnvironment);
+  final bool isNonNullableByDefault;
+
+  OperationsCfe(this.typeEnvironment, {required this.isNonNullableByDefault});
 
   @override
   TypeClassification classifyType(DartType? type) {
@@ -382,6 +384,44 @@
     }
     return from;
   }
+
+  @override
+  DartType glb(DartType type1, DartType type2) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  bool isAssignableTo(DartType fromType, DartType toType) {
+    if (isNonNullableByDefault) {
+      if (fromType is DynamicType) return true;
+      return typeEnvironment
+          .performNullabilityAwareSubtypeCheck(fromType, toType)
+          .isSubtypeWhenUsingNullabilities();
+    } else {
+      return typeEnvironment
+          .performNullabilityAwareSubtypeCheck(fromType, toType)
+          .orSubtypeCheckFor(toType, fromType, typeEnvironment)
+          .isSubtypeWhenIgnoringNullabilities();
+    }
+  }
+
+  @override
+  bool isDynamic(DartType type) => type is DynamicType;
+
+  @override
+  DartType lub(DartType type1, DartType type2) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  DartType makeNullable(DartType type) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  DartType? matchListType(DartType type) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
 }
 
 /// Type inference results used for testing.
diff --git a/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart b/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
index 798d641..1110cc9 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
@@ -94,16 +94,16 @@
 
   final TypeInferenceEngine engine;
 
+  final OperationsCfe operations;
+
   @override
   late final FlowAnalysis<TreeNode, Statement, Expression, VariableDeclaration,
-      DartType> flowAnalysis = libraryBuilder
-          .isNonNullableByDefault
-      ? new FlowAnalysis(
-          new OperationsCfe(engine.typeSchemaEnvironment), assignedVariables,
-          respectImplicitlyTypedVarInitializers:
-              libraryBuilder.libraryFeatures.constructorTearoffs.isEnabled)
-      : new FlowAnalysis.legacy(
-          new OperationsCfe(engine.typeSchemaEnvironment), assignedVariables);
+          DartType> flowAnalysis =
+      libraryBuilder.isNonNullableByDefault
+          ? new FlowAnalysis(operations, assignedVariables,
+              respectImplicitlyTypedVarInitializers:
+                  libraryBuilder.libraryFeatures.constructorTearoffs.isEnabled)
+          : new FlowAnalysis.legacy(operations, assignedVariables);
 
   @override
   final AssignedVariables<TreeNode, VariableDeclaration> assignedVariables;
@@ -142,12 +142,14 @@
             const [], const DynamicType(), libraryBuilder.nonNullable),
         classHierarchy = engine.classHierarchy,
         instrumentation = isTopLevel ? null : engine.instrumentation,
-        typeSchemaEnvironment = engine.typeSchemaEnvironment {}
+        typeSchemaEnvironment = engine.typeSchemaEnvironment,
+        operations = new OperationsCfe(engine.typeSchemaEnvironment,
+            isNonNullableByDefault: libraryBuilder.isNonNullableByDefault);
 
   InferenceVisitorBase _createInferenceVisitor(InferenceHelper? helper) {
     // For full (non-top level) inference, we need access to the
     // InferenceHelper so that we can perform error reporting.
-    return new InferenceVisitorImpl(this, helper);
+    return new InferenceVisitorImpl(this, helper, operations);
   }
 
   @override
@@ -157,7 +159,9 @@
     ExpressionInferenceResult result = visitor.inferExpression(
         initializer, const UnknownType(), true,
         isVoidAllowed: true);
-    return visitor.inferDeclarationType(result.inferredType);
+    DartType type = visitor.inferDeclarationType(result.inferredType);
+    visitor.checkCleanState();
+    return type;
   }
 
   @override
@@ -170,6 +174,7 @@
     initializerResult = visitor.ensureAssignableResult(
         declaredType, initializerResult,
         isVoidAllowed: declaredType is VoidType);
+    visitor.checkCleanState();
     return initializerResult;
   }
 
@@ -191,6 +196,7 @@
     }
     result =
         closureContext.handleImplicitReturn(visitor, body, result, fileOffset);
+    visitor.checkCleanState();
     DartType? futureValueType = closureContext.futureValueType;
     assert(!(asyncMarker == AsyncMarker.Async && futureValueType == null),
         "No future value type computed.");
@@ -231,6 +237,7 @@
     InvocationInferenceResult result = visitor.inferInvocation(
         visitor, typeContext, fileOffset, targetType, targetInvocationArguments,
         staticTarget: target);
+    visitor.checkCleanState();
     DartType resultType = result.inferredType;
     if (resultType is InterfaceType) {
       return resultType.typeArguments;
@@ -247,8 +254,10 @@
     // TODO(paulberry): experiment to see if dynamic dispatch would be better,
     // so that the type hierarchy will be simpler (which may speed up "is"
     // checks).
-    InferenceVisitor visitor = _createInferenceVisitor(helper);
-    return visitor.inferInitializer(initializer);
+    InferenceVisitorBase visitor = _createInferenceVisitor(helper);
+    InitializerInferenceResult result = visitor.inferInitializer(initializer);
+    visitor.checkCleanState();
+    return result;
   }
 
   @override
@@ -260,6 +269,7 @@
       // because inference on metadata requires the helper.
       InferenceVisitorBase visitor = _createInferenceVisitor(helper);
       visitor.inferMetadata(visitor, parent, annotations);
+      visitor.checkCleanState();
     }
   }
 
@@ -278,6 +288,7 @@
       initializer =
           visitor.ensureAssignableResult(declaredType, result).expression;
     }
+    visitor.checkCleanState();
     return initializer;
   }
 }
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index efe056a..476e815 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -71,6 +71,7 @@
 artificial
 asgerf
 askesc
+asking
 aspect
 aspx
 asserting
@@ -101,6 +102,7 @@
 backstop
 badly
 bail
+balance
 bang
 bar
 basically
@@ -274,6 +276,7 @@
 continuations
 contra
 contribute
+conveniently
 convention
 conversely
 coordinated
@@ -437,6 +440,7 @@
 en
 encapsulation
 end'ed
+end's
 enforce
 enforced
 enforces
@@ -634,6 +638,7 @@
 identifies
 identifying
 ideographic
+idle
 idn
 ids
 idx
@@ -826,6 +831,7 @@
 manage
 mangled
 manipulation
+manipulations
 manner
 markdown
 masking
@@ -918,6 +924,7 @@
 observatory
 observe
 obstruct
+obtaining
 occasionally
 occupied
 offending
@@ -1402,6 +1409,7 @@
 sw
 swapped
 sweep
+switched
 symbolic
 synchronously
 syncs
diff --git a/pkg/front_end/testcases/general/switch_error_recovery.dart b/pkg/front_end/testcases/general/switch_error_recovery.dart
new file mode 100644
index 0000000..5888e37
--- /dev/null
+++ b/pkg/front_end/testcases/general/switch_error_recovery.dart
@@ -0,0 +1,11 @@
+// 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.
+
+class C<X, Y> {}
+
+void main() {
+  switch (1) {
+    C<int, int> case 1: break;
+  }
+}
diff --git a/pkg/front_end/testcases/general/switch_error_recovery.dart.textual_outline.expect b/pkg/front_end/testcases/general/switch_error_recovery.dart.textual_outline.expect
new file mode 100644
index 0000000..0f2e897
--- /dev/null
+++ b/pkg/front_end/testcases/general/switch_error_recovery.dart.textual_outline.expect
@@ -0,0 +1,3 @@
+class C<X, Y> {}
+
+void main() {}
diff --git a/pkg/front_end/testcases/general/switch_error_recovery.dart.textual_outline_modelled.expect b/pkg/front_end/testcases/general/switch_error_recovery.dart.textual_outline_modelled.expect
new file mode 100644
index 0000000..0f2e897
--- /dev/null
+++ b/pkg/front_end/testcases/general/switch_error_recovery.dart.textual_outline_modelled.expect
@@ -0,0 +1,3 @@
+class C<X, Y> {}
+
+void main() {}
diff --git a/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.expect b/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.expect
new file mode 100644
index 0000000..688fb59
--- /dev/null
+++ b/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.expect
@@ -0,0 +1,22 @@
+library /*isNonNullableByDefault*/;
+//
+// Problems in library:
+//
+// pkg/front_end/testcases/general/switch_error_recovery.dart:9:5: Error: Expected to find 'case'.
+//     C<int, int> case 1: break;
+//     ^
+//
+import self as self;
+import "dart:core" as core;
+
+class C<X extends core::Object? = dynamic, Y extends core::Object? = dynamic> extends core::Object {
+  synthetic constructor •() → self::C<self::C::X%, self::C::Y%>
+    : super core::Object::•()
+    ;
+}
+static method main() → void {
+  switch(1) {
+    #L1:
+      {}
+  }
+}
diff --git a/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.modular.expect b/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.modular.expect
new file mode 100644
index 0000000..688fb59
--- /dev/null
+++ b/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.modular.expect
@@ -0,0 +1,22 @@
+library /*isNonNullableByDefault*/;
+//
+// Problems in library:
+//
+// pkg/front_end/testcases/general/switch_error_recovery.dart:9:5: Error: Expected to find 'case'.
+//     C<int, int> case 1: break;
+//     ^
+//
+import self as self;
+import "dart:core" as core;
+
+class C<X extends core::Object? = dynamic, Y extends core::Object? = dynamic> extends core::Object {
+  synthetic constructor •() → self::C<self::C::X%, self::C::Y%>
+    : super core::Object::•()
+    ;
+}
+static method main() → void {
+  switch(1) {
+    #L1:
+      {}
+  }
+}
diff --git a/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.outline.expect b/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.outline.expect
new file mode 100644
index 0000000..6fd9e64
--- /dev/null
+++ b/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.outline.expect
@@ -0,0 +1,10 @@
+library /*isNonNullableByDefault*/;
+import self as self;
+import "dart:core" as core;
+
+class C<X extends core::Object? = dynamic, Y extends core::Object? = dynamic> extends core::Object {
+  synthetic constructor •() → self::C<self::C::X%, self::C::Y%>
+    ;
+}
+static method main() → void
+  ;
diff --git a/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.transformed.expect b/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.transformed.expect
new file mode 100644
index 0000000..688fb59
--- /dev/null
+++ b/pkg/front_end/testcases/general/switch_error_recovery.dart.weak.transformed.expect
@@ -0,0 +1,22 @@
+library /*isNonNullableByDefault*/;
+//
+// Problems in library:
+//
+// pkg/front_end/testcases/general/switch_error_recovery.dart:9:5: Error: Expected to find 'case'.
+//     C<int, int> case 1: break;
+//     ^
+//
+import self as self;
+import "dart:core" as core;
+
+class C<X extends core::Object? = dynamic, Y extends core::Object? = dynamic> extends core::Object {
+  synthetic constructor •() → self::C<self::C::X%, self::C::Y%>
+    : super core::Object::•()
+    ;
+}
+static method main() → void {
+  switch(1) {
+    #L1:
+      {}
+  }
+}