dart-lang / dart-lang/language
Static "reflection" capability: general parsed data from classes, methods or vars
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
**Updated**
There is a number of issues (#251, sdk#39438, sdk#1150, sdk#39438) which are all related to the capacity of programmatically performing actions in the code according to parsed information from the code itself. If the parsed information was somehow available from within the code it would be possible to automate code patterns which would otherwise require to manually change the code in several places instead of just one and being also a convenience the static safety assurance that is provided by this parsed data instead of having to write a string manually (prone to error). Historically parsing has been used to execute code and “reading the code from within the code” has not been thought of as useful because it has no meaning from the perspective of code execution, however practice has shown referencing previously typed code is useful, especially in static typed languages.
Accessing the “code from within the code” could also provide the consistency convenience of static safety to certain code paths that have generally been accessible only through the means of what is called "reflection". The reflection capability could be separated as converting:
1. Class and methods to String data
2. String data to Class and methods
Number 1 is direct to approach as compile time constants.
Number 2 could be statically satisfied over the visible interface of a variable by creating a one line shortcut for a cumbersome code pattern.
Another way of understanding what accessing the parsed information means is: going back to the written definition of a variable. variables-classes-methods are fundamentally a reference to a pointer, being their names useless from the perspective of the machine. But practice has shown that sometimes developers do want to programmatically use the written definition of a var (or class or method, etc) as a string.
### Proposal: `parse` keyword
Access the static “written code” data of any variable in the code (class, method or var) using the keyword-method `parse`:
```dart
class A { final bar = ‘bar’; foo(int x) {} }
const ParsedClass cls = parse(A);
const ParsedField field = cls.vars.bar;
const ParsedMethod method = cls.methods.foo;
const Map methodsMap = cls.methodsMap;
const Map varMap = cls.varMap;
class B {
A a;
foo() {
const parsedB = parse(); // defaults to parse the enclosing class = parse(B)
const ParsedVar parsedVarA = parse(a)
const parsedA = parse(parsedVarA.type); // = parse(A)
return parsedB.methods.foo.name + parsedA.vars.bar.name; // translates to: return 'foo' + 'bar'
}
}
```
In general the `Parsed` type should expose all kind of data from the written code related to the variable-class-method, like names, parent class, fields, method arguments, types and so forth (even the method body). `parse` always creates compile time constant values and the declaration of any `Parsed` type vars should be enforced with the usage of an explicit `const` to make the constant nature of `parse` completely clear to the user (it can only be data inferred from the code statically).
### Invoking methods [sdk#45359](https://github.com/flutter/flutter/issues/45359)
Invoking methods in a “reflective” way could be statically enabled by using invoke method directly on a ParsedClass and passing the object as the first parameter (for static methods the first parameter should be null). Example:
```dart
class B extends A {}
const ParsedClass cls = parse(A);
A a = A()
cls.invoke(a, 'foo', []);
```
Method invoke in ParsedClass could be translated by the compiler to the following code pattern (example):
```dart
/// invoke on parse(A).invoke translates to something like this
invoke(A object, String name, [Iterable args, Map namedArgs]) {
switch (name) {
case 'foo':
parse(a.foo).checkValidArgs(args, namedArgs); // throw exception if args are not valid
return a.foo();
break;
case 'toString':
parse(a.toString).checkValidArgs(args, namedArgs);
return a.toString();
break;
case 'noSuchMethod':
parse(a.noSuchMethod).checkValidArgs(args, namedArgs);
return a.noSuchMethod(args[0]);
break;
default:
throw MethodNotInInterfaceException();
}
}
```
`ParsedMethod.checkValidArgs` resolves to code in a similar way as `ParsedClass.invoke` does. When invoking a method through "static reflection", noSuchMethod is never invoked by default, instead if the inputs are invalid, MethodNotInIterfaceException is thrown. Throwing an exception is necessary in case the underlying object did actually implement the method, in which case it would be inconsistent to invoke noSuchMethod.
### First step
As first step I would suggest starting with parse(var-class-method) that only provides the name of the var as a symbol. This means that initially it would only be useful to do `parse(myVar).name`. The Parse type leaves the door open to keep extending the feature as proposed above.
Additionally the ParsedClass.invoke method could be considered to be implemented as mirrors support have been highly requested for Flutter.
Contributor guide
Assessment
This issue has not been assessed yet.