dart-lang / language

Design of the Dart language
Other
2.66k stars 205 forks source link

Can Code objects be introspected? #1933

Closed munificent closed 4 months ago

munificent commented 3 years ago

We've discussed whether there is any API to let you destructure a Code object and introspect over its syntactic subcomponents. For example, given a Code object for a + b, is there a way to determine that it's a + expression and what the two operands are?

mateusfccp commented 3 years ago

IMHO I can't see much gain in doing so. Do you see a strong use-case for this? Given the nature of the approach for static meta-programming (macros), I think it could lead to confusion. If we used a AST-based approach, then it would make sense.

TimWhiting commented 3 years ago

I think it could be useful for dependency tracking. This enables use cases like automatic cache invalidation, machine learning (automatic differentiation), and automatic rebuilds of widgets.

Also I know @rrousselGit is interested in this for state management dependency tracking.

rrousselGit commented 3 years ago

I'd definitely need the ability to inspect the content of functions/expressions.

I need to track all references of a variable, and track all method call on that variable (including unresolved method call – which would be generated by a macro)

jakemac53 commented 3 years ago

As long as we don't try to resolve the code, I don't see any feasibility issues. As soon as we give you a Code object we have already essentially incurred most if not all of the potential downsides of it. So I don't see a fundamental issue with exposing it, other than designing the API itself to be as minimal/flexible as possible so it can be resilient in the face of language changes.

And possibly we could allow you to resolve parts of the code in later macro phases?

TimWhiting commented 3 years ago

@jakemac53 I think that is a good solution. On top of that minimal API macro authors could create a minimal AST parser for different language versions if they need more info, and the dart authors don't need to worry about breaking macros, instead it places breaking changes to the AST as a burden on the external AST package and macro authors. Along with that it would be useful for macros to know what language version they are running on, so that they can emit a warning if the language version of the user's code might cause errors in the macro. (such as when they depend on the AST structure). Obviously simple macros should avoid the AST altogether in order to minimize breakage, but more advanced ones can emit warnings that they do not claim to support newer language versions.

I also think being able to resolve identifiers in later macro phases would then be very useful. Eg:

DartType builder.typeOf(String identifier)

or something

munificent commented 3 years ago

As long as we don't try to resolve the code, I don't see any feasibility issues.

If we don't resolve it, is it useful to introspect on it? The use cases I've seen are around things like tracking dependencies, but that only works reliably if you know what identifiers refer to.

other than designing the API itself to be as minimal/flexible as possible so it can be resilient in the face of language changes.

I think this is the main potential downside. If we treat Code objects as opaque, then the API is basically invariant even as the language syntax evolves. If we expose any destructuring API, then we have to figure out how that API handles language changes.

We could do that by making it very minimal and essentially dynamically typed. That at least means language changes might not statically break the API. But any macro expecting certain properties to exist on some syntax node could still fail at execution time if the property isn't there. I'm not convinced that that's a net win.

I think unless we have solid use cases, we should probably consider Code objects to be essentially write-only. You can build and compose them, but not introspect or decompose them.

jakemac53 commented 3 years ago

Do we have use cases for making them introspectable outside of statement/expression level macros? Possibly we could just punt on this until we have the time to evaluate those.

munificent commented 2 years ago

Do we have use cases for making them introspectable outside of statement/expression level macros?

The only one I know offhand is the request to hunt through the body of a function to find dependencies for observability. I'm OK with kicking this down the road.

rrousselGit commented 2 years ago

What does "kicking this down the road" means? As I'll definitely need this for dependency tracking.

Considering we can do that using build_runner today, it'd be a bit unexpected IMO to no-longer be able to,

munificent commented 2 years ago

What does "kicking this down the road" means?

Basically just deciding on this later when we are further along with more fundamental design questions and implementation feasibility studies. I think we can decide whether or not the feature needs to support this after making other decisions since the overall design doesn't hinge on it.

rrousselGit commented 2 years ago

Coming back to this, another use-case I have is making some runtime errors compile errors instead

In particular, one case I have is, users may define the following:

@Macro(flag: true)
void a(Class class) {}

@Macro()
void b(Class class) {
  class.doSomething(a);
}

And I would like the line:

  class.doSomething(a);

to fail to compile because b did not pass the flag, such that folks have to either write:

@Macro()
void a(Class class) {}

@Macro()
void b(Class class) {
  class.doSomething(a);
}

or:

@Macro(flag: true)
void a(Class class) {}

@Macro(flag: true)
void b(Class class) {
  class.doSomething(a);
}

There are some workarounds to achieve this using the type system (by having a Class vs ClassWithFlag type). But this comes with an enormous amount of complexity and involves really bad error messages.

Riverpod managed to do this without macros, but it caused Riverpod to have 50+ public classes instead of ~20 (see its API reference https://pub.dev/documentation/riverpod/latest/riverpod/riverpod-library.html). That's significant pollution of the public API just for a single compilation error.

And the error message is fairly unclear too with no way of customizing it. It currently is something among the lines of:

The argument type 'ClassBase' can't be assigned to the parameter type 'ClassWithFlagBase'

It's a fairly obscure error with no way of clarifying it. No beginner seeing this compilation error will naturally guess what the fix to this error it

If Code objects could be introspected, then Riverpod's public API could be cut in half and the error message could become something much more human-friendly, such as:

`b` tried to doSomething on `a`, yet `a` has `flag: true` but `b` doesn't.

To fix, either remove `flag: true` from `a` or add `flag: true` to `b`.
davidmorgan commented 4 months ago

It looks like we will do some introspection on expressions for macro metadata

https://github.com/dart-lang/language/issues/3847

for any more than that it's a feature request / extension:

https://github.com/dart-lang/language/issues/2185