macro_prototype icon indicating copy to clipboard operation
macro_prototype copied to clipboard

Use-case: statically extract the dependencies of a function

Open rrousselGit opened this issue 4 years ago • 8 comments

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.

rrousselGit avatar Aug 08 '21 20:08 rrousselGit

I think this is a duplicate of https://github.com/jakemac53/macro_prototype/issues/6 - basically some way to view the method body and extract some info from that?

jakemac53 avatar Aug 09 '21 17:08 jakemac53

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

rrousselGit avatar Aug 09 '21 21:08 rrousselGit

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.

scheglov avatar Aug 18 '21 16:08 scheglov

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 👍

jakemac53 avatar Aug 18 '21 16:08 jakemac53

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.

rrousselGit avatar Aug 18 '21 16:08 rrousselGit

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.

scheglov avatar Aug 18 '21 17:08 scheglov

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

Wondering if this info could be hoisted into annotations, etc

kevmoo avatar Sep 21 '21 17:09 kevmoo

@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.

rrousselGit avatar Sep 21 '21 18:09 rrousselGit