Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[compiler] First cut at dep inference #31386

Merged
merged 10 commits into from
Nov 22, 2024
Merged

[compiler] First cut at dep inference #31386

merged 10 commits into from
Nov 22, 2024

Conversation

jbrown215
Copy link
Contributor

@jbrown215 jbrown215 commented Oct 30, 2024

This is for researching/prototyping, not a feature we are releasing imminently.

Putting up an early version of inferring effect dependencies to get feedback on the approach. We do not plan to ship this as-is, and may not start by going after direct useEffect calls. Until we make that decision, the heuristic I use to detect when to insert effect deps will suffice for testing.

The approach is simple: when we see a useEffect call with no dep array we insert the deps inferred for the lambda passed in. If the first argument is not a lambda then we do not do anything.

This diff is the easy part. I think the harder part will be ensuring that we can infer the deps even when we have to bail out of memoization. We have no other features that must run regardless of rules of react violations. Does anyone foresee any issues using the compiler passes to infer reactive deps when there may be violations?

I have a few questions:

  1. Will there ever be more than one instruction in a block containing a useEffect? if no, I can get rid of theaddedInstrs variable that I use to make sure I insert the effect deps array temp creation at the right spot.
  2. Are there any cases for resolving the first argument beyond just looking at the lvalue's identifier id that I'll need to take into account? e.g., do I need to recursively resolve certain bindings?

Copy link

vercel bot commented Oct 30, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 21, 2024 9:39pm

@josephsavona
Copy link
Contributor

Will there ever be more than one instruction in a block containing a useEffect

Ahh I think what you might be observing is that we create scopes for hook calls, but then flatten them away. And so either you’re looking at the pre-flattened state or you’re looking at a later pass after we flatten to a labeled block. We want to get rid of the labeled block and truly flatten it as if there had never been a scope, though. So I would definitely code as if there could be other instructions in the block w the effect.

@josephsavona
Copy link
Contributor

Are there any cases for resolving the first argument beyond just looking at the lvalue's identifier id

No, that should only happen if there wasn’t an inline function expression.

@jbrown215 jbrown215 changed the title [compiler] First cut at inferring effect dependencies [compiler] First cut at dep inference Oct 30, 2024
Copy link
Contributor

@josephsavona josephsavona left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exciting! A bunch of smaller feedback, we should discuss the strategy (given the need for guaranteed compilation) in our compiler design sync

@jbrown215
Copy link
Contributor Author

@mofeiZ mentioned that it would be good to promote effect lambdas to their own scope before running PropagateScopeDependencies to get the most precise deps I can

@josephsavona
Copy link
Contributor

Hmmm given that effect lambdas are immediately frozen and also transitively freeze their dependencies, they should already be in their own scope anyway. Do you have a specific example where that isn't the case now?

@jbrown215
Copy link
Contributor Author

I don't have a specific example, but we were talking about this in the context of a reduced pipeline that may not be running all of the phases. For example, we may not freeze functions in the re-run because it relies on certain Rules of React not to be violated. In that context it seems possible? In any case, it doesn't need to block this PR and I can wait until I produce that issue!

@josephsavona
Copy link
Contributor

Ahhh i see. Yeah, in the reduced pipeline you could just force-creates scopes around the lambdas passed to useEffect and run propagateScopeDeps to get the deps.

Comment on lines +11 to +20
console.log(foo);
console.log(bar);
console.log(moduleNonReactive);
console.log(localNonreactive);
console.log(globalValue);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add a ref here and a non-primitive local too?

} else {
t4 = $[9];
}
useEffect(t4, [bar, bar.qux]);
Copy link
Contributor Author

@jbrown215 jbrown215 Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a little weird to me-- the reactive deps of the function on 93 are more precise than the deps we insert here, but we filter the effect deps to just its reactive deps...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mofeiZ: I think you were working on a related problem here about capturing member expressions- does anything look strange to you here? Should I be filtering the deps differently?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted about this offline, the correct fix here was to change effect dependencies to be derived from ReactiveScope.dependencies instead of LoweredFunction.dependencies (also see #31386 (comment))

} else {
t2 = $[3];
}
useEffect(t2, [obj.a]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that obj.a (the merged dependency) is taken here as we're now using memo scope dependencies, which are merged in PropagateScopeDeps. Another benefit is that this is more "correct" as LoweredFunction.dependencies do not take into account hoistability (e.g. nullability) of accessed values.

function Component({value, cond}) {
  // here, we should take [cond, value] as a dependency, not
  // [cond, value.a.b] as evaluating value.a.b may throw
  useEffect(() => {
    if (cond) {
      read(value.a.b);
    }
  });

import { useIdentity, mutate, makeObject } from "shared-runtime";
import { useEffect } from "react";

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting case. @jbrown215 and I discussed offline and came up with an example for why these should be dependencies (see code snippet in below comment block)

} else {
t1 = $[1];
}
useEffect(t1, [obj]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment at top of fixture for why [obj] is included in the deps array even though it's a stable (and memoized) value

@mofeiZ
Copy link
Contributor

mofeiZ commented Nov 21, 2024

Hmmm given that effect lambdas are immediately frozen and also transitively freeze their dependencies, they should already be in their own scope anyway. Do you have a specific example where that isn't the case now?

In this case, should we bail out on @enableAssumeHooksFollowRulesOfReact:false for this feature?

Copy link
Contributor

@mofeiZ mofeiZ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super exciting!

@mofeiZ mofeiZ merged commit e697386 into facebook:main Nov 22, 2024
18 checks passed
@jbrown215
Copy link
Contributor Author

Thanks for all the help merging this @mofeiZ !

@yongsk0066
Copy link

I noticed this PR is about dependency inference at the compiler level, but this wouldn't be reflected in the actual source code. This seems a bit ambiguous - is this groundwork for the React IDE?

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

Successfully merging this pull request may close these issues.

6 participants