Repository: TypeScript
Issue: Design Meeting Notes, 5/31/2024
Labels: Design Notes


# Control Flow Analysis From Callbacks

https://github.com/microsoft/TypeScript/issues/11498

```ts
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?

          ```ts
          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

      ```ts
      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 of `A_pre` and `B_pre`, and that `B_post` is a possible antecedent of `A_pre` and `B_pre`.

          ```dot
          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
          }
          ```

          ```mermaid
            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.