-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
The code:
FutureOr<void> foo(void Function(void Function(Object?)) callback) {
Completer<void>? completer;
var wasSync = false;
callback((value) {
if (completer != null) {
completer.complete(value);
} else {
wasSync = true;
});
if (wasSync) return;
completer = Completer();
return completer.future;
}
analyses and compiles with no errors. The completer != null
check inside the closure promotes completer
even though it's assigned outside of the closure.
I think that's actually sound, because the assignments outside of the closure cannot occur while the closure is running, not unless they're inside another closure, and that would disable promotion.
However, this promotion is also applied to asynchronous functions, across asynchronous gaps, and that is not sound.
Example:
import "dart:async";
void main() {
void Function(Object) f = (_) {};
foo((void Function(Object) c) => f = c, () => f(42));
}
void foo(
void Function(void Function(Object o)) callback,
void trigger(),
) {
Completer<Object>? completer;
print("unset");
callback((Object value) async {
if (completer != null) {
print("checked");
await 0;
print("uses");
completer.complete(value);
print("used");
}
});
completer = Completer();
print("set");
completer.future.then(print); // Sanity check.
trigger();
completer = null;
print("unset");
}
This code runs and prints:
unset
set
checked
unset
uses
and then fails when it tries to call complete
on null
.
Assuming the synchronous promotion is deliberate, and we believe it's sound, then that promotion needs to be disabled if there is any asynchronous gap between the check and the use.
The only thing that makes the promotion sound is that the code in the surrounding function cannot run while the inner function does, and that's only true if the inner function is synchronous from check to use.