visitCascadeExpression method

  1. @override
Object? visitCascadeExpression(
  1. SCascadeExpression node
)
override

Implementation

@override
Object? visitCascadeExpression(SCascadeExpression node) {
  // SCE81. A section that awaits suspends the cascade, and the state machine
  // replays the enclosing expression once the future completes. Dart
  // evaluates the target ONCE and runs the sections in order for their side
  // effects, so a bare replay would re-evaluate the target and re-run every
  // earlier section: `sb..write('a')..write(await f())` would write `'a'`
  // twice.
  //
  // So the target is memoised and completed sections are recorded, both
  // keyed by node on the async state. That is what makes this option (b) —
  // the target and the completed sections memoised the way `await` results
  // already are — rather than a refusal.
  //
  // Null outside an async function: there is nothing to replay, so the
  // bookkeeping is skipped entirely and this behaves exactly as before.
  final asyncState = currentAsyncState;

  // 1. Evaluate the target expression ONCE.
  final Object? targetValue;
  if (asyncState != null && asyncState.cascadeTargets.containsKey(node)) {
    targetValue = asyncState.cascadeTargets[node];
  } else {
    final evaluated = node.target!.accept<Object?>(this);
    if (evaluated is AsyncSuspensionRequest) return evaluated;
    targetValue = evaluated;
    if (asyncState != null) asyncState.cascadeTargets[node] = targetValue;
  }

  // 2. Execute each cascade section ON THE ORIGINAL targetValue.
  for (final section in node.cascadeSections) {
    // Already run before an earlier suspension: its side effect has
    // happened, and happening again is the defect this guards.
    if (asyncState != null &&
        asyncState.completedCascadeSections.contains(section)) {
      continue;
    }

    // We need to manually handle each section type, forcing the target.
    final Object? outcome;
    if (section is SMethodInvocation) {
      outcome = _executeCascadeMethodInvocation(targetValue, section);
    } else if (section is SPropertyAccess) {
      // Evaluate property access for potential side effects (getters), but discard result.
      outcome = _executeCascadePropertyAccess(targetValue, section);
    } else if (section is SAssignmentExpression) {
      outcome = _executeCascadeAssignment(targetValue, section);
    } else if (section is SIndexExpression) {
      // Evaluate index expression for potential side effects (getters?), but discard result.
      outcome = _executeCascadeIndexAccess(targetValue, section);
    } else {
      // Should not happen with valid cascade sections
      throw UnimplementedD4rtException(
        'Cascade section type not handled: ${section.runtimeType}',
      );
    }

    // A section's VALUE is discarded by design — the cascade evaluates to
    // its target — but a suspension is not a value.
    if (outcome is AsyncSuspensionRequest) return outcome;
    asyncState?.completedCascadeSections.add(section);
  }

  // 3. The cascade expression evaluates to the original target value.
  //
  // The memo is dropped here rather than at statement level, so a cascade
  // inside a loop starts fresh on the next iteration instead of skipping
  // every section it ran last time.
  if (asyncState != null) {
    asyncState.cascadeTargets.remove(node);
    for (final section in node.cascadeSections) {
      asyncState.completedCascadeSections.remove(section);
    }
  }
  return targetValue;
}