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

Use-case: statically extract the dependencies of a function #37

Open
rrousselGit opened this issue Aug 8, 2021 · 8 comments
Open

Use-case: statically extract the dependencies of a function #37

rrousselGit opened this issue Aug 8, 2021 · 8 comments

Comments

@rrousselGit
Copy link
Contributor

Currently, both Riverpod and flutter_hooks sometimes relies on users having to define a list of dependencies used by a function

An example would be:

final provider = Provider<MyClass>((ref) {
  ref.watch(dependency);
  ref.watch(anotherDependency);

  return MyClass();
},
  dependencies: { dependency, anotherDependency }, // the list of providers passed to "ref.watch"
  name: 'provider',
 );

or:

Widget build(context) {
  final cachedResult = useMemoized(
    () => MyClass(dependency),
    [dependency], // the list of variables used within the closure
  );
  final another = useMemoized(() => MyClass(foo, bar: bar), [foo, bar]);
 ...
}

The problem with this syntax is, it is error prone.

The interesting thing is, in both examples, this list of dependency is known at compile time.
This means that this secondary parameter could instead be generated by a macro directly with a 100% accuracy.

In particular, I was thinking of changing the first example to be:

@provider
MyClass $provider(ProviderReference ref) {
  ref.watch(dependency);
  ref.watch(anotherDependency);
  return MyClass();
}

which would generate:

final provider = Provider($provider, dependencies: { dependency, anotherDependency }, name: 'provider');

Sadly it seems like the current macro API does not give us enough informations about the function content to do such a thing.

@jakemac53
Copy link
Owner

I think this is a duplicate of #6 - basically some way to view the method body and extract some info from that?

@rrousselGit
Copy link
Contributor Author

Possibly. This was meant more as a "here's a problem" without knowing what the solution would be.

Feel free to close this if you want

@scheglov
Copy link

One complication with looking into function bodies is that this affects how the analyzer decides which changes affect a library API. Normally bodies of top-level functions never affect the API, which means that you can change them and reuse the summary of the library, it still have exactly the same classes, variables, and type. So, you only have to re-resolve this library (could be just the changes function body), but not any other libraries that use the changes library.

So far, while prototyping macros in the analyzer, I considered any macro-generated elements as looking only on the API portion of a file, and generating only the APIs (specifically only new class members for now), and we cache the results of macro generators into summaries. It is not out of question to look into function bodies, we just have to be cognizant of the caching considerations.

@jakemac53
Copy link
Owner

Yes the caching implications definitely matter. So far we have only been considering allowing a macro to look at the AST of the thing it directly annotates. So you would not be able to look at function bodies arbitrarily in the program.

In theory that would help with invalidation - implementations could treat macro annotated declarations specially and invalidate the macro generation (and summary) whenever their body changes. Likely these macros would be implementing some specific interface as well so you would know which ones actually reach into function bodies and need to be specialized.

But yes in general this is one of several concerns with allowing macros to introspect on function bodies 👍

@rrousselGit
Copy link
Contributor Author

Would it not be possible to track what the macro is depending on?

There are a few helpful patterns for this, and it would be a caching mechanism independent from what the macro is reading.

@scheglov
Copy link

As it is implemented in the analyzer right now, we compute "API signature" of a file based on its parsed AST, without any resolution. So, we don't know whether there are any macro annotations, where they resolved, which interfaces they implement. Then this API signature is combined with other data, and eventually turned into the cache key. If we can find the result by this key, we use it, otherwise we recompute.

One of the arcs of work that I started exploring, and was going to continue, until the macro feature became more important, was to implement fine grained dependency tracking, where we actually see which symbols are used by other symbols, and invalidate only affected portions. Then we don't use the cache key, or at least this key does not depend on the API signature, instead we get a cached result and check that it (or its parts) is still valid. With this approach we probably could be able to see that an element was macro-generated, and looked into one or another function body.

@kevmoo
Copy link
Contributor

kevmoo commented Sep 21, 2021

@rrousselGit – in your example dependency and anotherDependency are...what? Top-level fields? Instance fields?

Wondering if this info could be hoisted into annotations, etc

@rrousselGit
Copy link
Contributor Author

rrousselGit commented Sep 21, 2021

@kevmoo

@rrousselGit – in your example dependency and anotherDependency are...what? Top-level fields? Instance fields?

Wondering if this info could be hoisted into annotations, etc

They could be any variable definition, including instance fields.
And the ref.watch call can receive expressions, instead of a simple variable.

In particular, the use-cases I need to support are:

final family = Provider.family<State, Param>(...);
final dependency = Provider<Person>(...);

final provider = Provider((ref) {
  Person p = ref.watch(dependency);
  String name = ref.watch(dependency.select((p) => p.name));

  Param param = <...>
  State s = ref.watch(family(param));
  String str = ref.watch(family(param).select((s) => s.str);
});

where all ref.watch usage should extract either dependency or family from the expression.

Getters/function calls aren't supported (besides Provider.family described above). So:

Provider getProvider() => ...

final provider = Provider((ref) {
  ref.watch(getProvider())
}

is invalid

If necessary, in the examples from this comment, dependency/family/provider could be constants.

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

No branches or pull requests

4 participants