Skip to content

Design Meeting Notes, 6/7/2024 #58804

Closed
Closed

Description

Control Flow over Callback Arguments, and the Deferred Marker Type

#58729

  • Want to be able to track callbacks as possible branches of control flow.

  • Unfortunately, there are plenty of callbacks that are queued and ran in a deferred manner.

  • Need a way to signal that any possibly invocation of a function will not happen immediately.

  • The direction we took was to wrap the type of the callback in a Deferred<...> helper type.

  • This is more work, so how are we avoiding it?

    • Some optimizations around collapsing un-effect-ful parts of the control flow graph that we introduced in TS 5.5 - that helps a bit.
    • But there is still a lot of work now resolving functons that are called to figure out if any parameter is marked with a Deferred.
      • This work would have been done regardless of if a variable is actually ever referenced in the lambda.

        doStuff(() => {
            x = 123;
        });
        y;
        • In the above example, our callback is known to be effectful - so we would require a walk of it to determine the type of y even though y isn't ever referenced.
      • So instead of resolving the type of the called function immediately, we do a walk of the callback's CFG to look for assignments to y before we decide to resolve.

      • But if you have an effectful function, aren't you going to have to resolve the call anyway?

        • Well one thing is that less unnecessary up-front work triggers fewer circularities.

        • But also, that's not necessarily the case. Local effects might have nothing to do with outer variables!

          let x = 0;
          doStuff(() => {
              let y = 123;
              y = 456;
              return y;
          });
          
          x;
          • In the above example, y is mutated, but x itself is never referenced.
  • What happens when you have several overloads - one which is deferred?

    • We don't do full overload resolution - we just look for if any signature has a Deferred type at that position.
  • It's strange that this is an intrinsic type and not something tied to the parameter/binding.

    • Can you use it in places other than parameters?
    • Wait, can you not write something like f(options: { f: Deferred<() => void> })?
      • You can write it, but it will not work.
  • If Deferred is a type, you can write { [K in keyof T]: Deferred<T[K]> } for a deferred tuple.

    • And then spread that in?
    • Well... depends on when you want to resolve. And currently we don't.
    • Speaks to this being more of a parameter modifier - because you don't want to give people the impression that it can be used everywhere.
  • We could prohibit uses of Deferred in certain places.

    • Only let it get used in parameters for now and expand down the road.
  • Nice that Deferred<...> is no new syntax, similar to NoInfer.

  • Going back to the beginning - this analysis is not just for assignments, it's also for array mutations.

    let a = [];
    a.push("abc");
    
    invoke(() => {
        a.push(123);
    });
    
    a; // Array<number | string>
  • Does any of this track whether or not the function is truly deferred or not?

    • No, and it's not a simple question to really answer.

    • Complications around awaiting, aliasing, etc.

    • Though it raises some questions about Deferred as well.

      let fns: Array<() => void> = [];
      function enqueue(f: Deferred<() => void>) {
          // Is it okay to push a Deferred function in here?
          fns.push(f);
      }
      
      function runThem() {
          for (const f of fns) {
              f();
          }
      }
      
      let y: number | string = 0;
      enqueue(() => {
          y = "abc";
      })
      y; // We correctly see that the narrowed type of 'y' is 'number' because we used 'Deferred'.
      
      let x: number | string = 0;
      enqueue(() => {
          x = "abc";
      })
      runThem();
      x; // Because we wrote 'Deferred', we will *incorrectly* say that the narrowed type is 'number'.
    • Kind of depends on how strictly you view Deferred as "next turn of the event loop" or "not immediately".

Activity

  1. fatcerberus commented on Jun 7, 2024

    @fatcerberus

    Deferred helper type

    Shouldn’t it be the other way around - a NonDeferred helper type? If you treat “non-deferred” as the default then you end up causing the same breaks you were trying to avoid (code that assumes a callback is deferred and has its narrowings cancelled prematurely)

    plus I’d think that functions calling a callback immediately are probably in the minority relative to ones where it’s deferred/called asynchronously.

  2. fatcerberus commented on Jun 7, 2024

    @fatcerberus

    plus I’d think that functions calling a callback immediately are probably in the minority relative to ones where it’s deferred/called asynchronously.

    …unless someone just got finished reading a monad tutorial and went mad with power after it finally clicked for them 🚎

  3. DanielRosenwasser commented on Jun 7, 2024

    @DanielRosenwasser
    MemberAuthor

    The idea is that this would be behind a --strict flag which takes a more conservative view of narrowing - and declaration sites would be able to opt out of that.

  4. fatcerberus commented on Jun 7, 2024

    @fatcerberus

    Hmm… it seems like it’s probably pretty idiomatic today to do stuff like

    let foo: Foo | null = createFoo();
    foo.onFinished(() => {
        foo = null;
    });
    // now foo is fully initialized, pass it around

    As a matter of principle I’d feel uncomfortable passing foo around before all its callbacks were registered, and it wouldn’t be feasible for me to turn this proposed flag on until the library writer updated their types to align, which might make it too breaky even for strict

    My point stands about the potential for code bloat with Deferred vs. NonDeferred too

  5. Josh-Cena commented on Jun 11, 2024

    @Josh-Cena
    Contributor

    plus I’d think that functions calling a callback immediately are probably in the minority relative to ones where it’s deferred/called asynchronously.

    I would postulate that most callbacks are array method callbacks, FWIW.

  6. DanielRosenwasser commented on Jun 11, 2024

    @DanielRosenwasser
    MemberAuthor

    and it wouldn’t be feasible for me to turn this proposed flag on until the library writer updated their types to align, which might make it too breaky even for strict

    This is a bit of my concern too - that people would try this out, find it's too painful, and forget to try to turn it on again in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design NotesNotes from our design meetings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions