visitCascadeExpression method
Visit a SCascadeExpression.
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;
}