Open
Description
Control Flow Analysis From Callbacks
let x: string | number = "okay";
mystery(() => {
});
// Nope! TS thinks this is impossible.
if (x === 10) {
}
- We could assume that the callback to
mystery
might have been called. - Have a prototype that does just this.
- Basically a new control flow node inserted when a call expression contains a function expressions.
- When one is found, the body of the lambda is considered a possible branch of the control flow graph for the following code.
- Rather than adding a modifier for every call, it's arguable that the correct thing here is to be conservative and assume that the callback might have been called.
- We don't seem to have tests that are sensitive to this pattern, but it is worth running with as an experiment.
- People often think of the default mode of callbacks as occurring asynchronously - but the conservative thing is still to assume synchronous here.
- Might be worth thinking about how reads occur - for comparison versus assignability.
- So what are the risks and open questions?
- Obviously there are breaks - we don't know how big they are.
- Should we look at objects and classes with objects?
- Thinking o
- Should we make exceptions to
Promise.prototype.then
?-
What about
await
on those?async function f() { let foo1: "a" | "b" = "b"; await Promise.resolve().then(() => { foo1 = "a"; }); if (foo1 === "a") { // TypeScript assumes that this is impossible // because it doesn't understand that `foo1` // might have been assigned to in the callback. console.log("foo1 is a"); } }
-
- Implementation: creating a synthetic join point for all the function arguments to a call node.
- Do we need to consider that each function's control flow needs to be dominated by prior arguments?
-
Well, no, the arguments could be invoked in an arbitrary order.
-
Really the more conservative thing is to assume not just that the function expressions may have been called after the call, but also that
-
Example
function foo() { /*Call_pre*/ someCall(/*A_pre*/ () => {... /*A_post*/ }, /*B_pre*/ () => {... /*B_post*/}); /*Call_post*/ }
-
We need to assume that
A_post
is a possible antecedent ofA_pre
andB_pre
, and thatB_post
is a possible antecedent ofA_pre
andB_pre
.digraph { rankdir="BT"; A_pre -> Call_pre A_pre -> A_pre A_pre -> B_pre B_pre -> Call_pre B_pre -> B_pre B_pre -> A_pre A_post -> A_pre B_post -> B_pre Call_post -> A_post Call_post -> B_post Call_post -> Call_pre }
graph BT; A_pre --> Call_pre A_pre --> A_pre A_pre --> B_pre B_pre --> Call_pre B_pre --> B_pre B_pre --> A_pre A_post --> A_pre B_post --> B_pre Call_post --> A_post Call_post --> B_post Call_post --> Call_pre
-
-
- Ultimately we gotta see what this affects - but also, how does this work on our own codebase?
- Trying it out, we don't seem to have any code that has this pattern. We'll need to try it out on other real-world code.
Activity