Teach frontend compiler to replace toString with super.toString for selected packages#17068
Teach frontend compiler to replace toString with super.toString for selected packages#17068dnfield merged 23 commits intoflutter:masterfrom
toString with super.toString for selected packages#17068Conversation
|
Ah. I should write some tests for this. |
jonahwilliams
left a comment
There was a problem hiding this comment.
There is a script in here somewhere that populates a .packages, maybe that is working?
|
Yeah, it's the python script in the folder I believe, which is getting invoked by the build process, which creates the .packages file we need. |
|
It seems a bit overly complex TBH, it might be good to just replace it with a hard coded .packages file. |
|
|
||
| @override | ||
| void visitProcedure(Procedure node) { | ||
| if (node.name.name == 'toString' && node.enclosingLibrary != null) { |
There was a problem hiding this comment.
nit: You can flip the sense of the ifs to lower the nesting depth.
if (node.name.name != 'toString' || node.enclosingLibrary == null) {
super.visitProcedure(node);
return;
}
assert(node.enclosingClass != null);
// Turn 'dart:ui' into 'dart:ui', or
// 'package:flutter/src/semantics_event.dart' into 'package:flutter'.
final String packageUri =
'${node.enclosingLibrary.importUri.scheme}:'
'${node.enclosingLibrary.importUri.pathSegments.first}';
if (!_packageUris.contains(packageUri)) {
super.visitProcedure(node);
return;
}
node.function.replaceWith(
FunctionNode(
ReturnStatement(
SuperMethodInvocation(
node.name,
Arguments(<Expression>[]),
),
),
),
);
super.visitProcedure(node);There was a problem hiding this comment.
I did this slightly differently so that I don't have to do multiple returns but still dropped some nesting.
This also made me realize it's a mistake to assert enclosingClass is null - I should be checking for it instead, in case someone put a toString method at the top of their library. Added a test for that.
|
/cc @alexmarkov |
|
|
||
| final frontend.ProgramTransformer _child; | ||
|
|
||
| /// A set of package URIs to apply this transformer too, e.g. 'dart:ui' and |
There was a problem hiding this comment.
Yes, thanks, fixing this and the others
| } | ||
| } | ||
|
|
||
| /// Replaces [Object.toString] overides with calls to super for the specified |
There was a problem hiding this comment.
typo: "overides" -> "overrides"
| /// The [packageUris] must not be null. | ||
| ToStringVisitor(this._packageUris) : assert(_packageUris != null); | ||
|
|
||
| /// A set of package URIs to apply this transformer too, e.g. 'dart:ui' and |
|
I'm starting to think that this could be improved so that it reads a specific annotation, as suggested in the original issue. I think we'd still want to avoid completely removing the method, as that gets really messy (we have to update any callsites, which might be hard to find, and it gets a lot harder to figure out what's going on if anything goes wrong). |
| _packageUris.contains(_importUriToPackage(node.enclosingLibrary.importUri)) | ||
| ) { | ||
| node.function.replaceWith( | ||
| FunctionNode( |
There was a problem hiding this comment.
We might need to verify that toString() doesn't have any extra optional parameters. Otherwise AST would be incorrect if there are calls to such toString which pass those optional parameters, but this transformation removed them.
Or maybe we can replace a node.function.body instead of replacing the whole FunctionNode.
There was a problem hiding this comment.
Trying to replace node.function.body. If that works that sounds better.
There was a problem hiding this comment.
Done, and updated tests.
| void visitProcedure(Procedure node) { | ||
| if ( | ||
| node.name.name == 'toString' && | ||
| node.enclosingClass != null && |
There was a problem hiding this comment.
We can also check !node.isStatic && !node.isAbstract
| if ( | ||
| node.name.name == 'toString' && | ||
| node.enclosingClass != null && | ||
| node.enclosingLibrary != null && |
There was a problem hiding this comment.
This check looks unnecessary: if there is an enclosing class, then there should be an enclosing library as well.
There was a problem hiding this comment.
I guess I'd be more concerned about somehow getting an NPE below this (where I access enclosingLibrary.importUri) than saving the time on the extra null check.
| ), | ||
| ); | ||
| } | ||
| super.visitProcedure(node); |
There was a problem hiding this comment.
Do we really need to go deeper? Not going beyond members might save a little bit of compilation time.
Also consider adding
@OverRide
defaultMember(Member node) {}
There was a problem hiding this comment.
Ah, I missed something here. I'll ping this again when I push up the change that should fix it.
There was a problem hiding this comment.
Ok, I removed the super call this time. I've also added the ability to add an @pragma annotation to a toString method so we let it through.
|
I'm starting to suspect we could more safely and clearly attain these results with something like this: We could make such a mixin in dart:ui and either export it to use in the framework or just make This would let someone pretty easily "opt out" if they really needed to. |
|
It would also let packages outside of the framework safely adopt this pattern. |
|
The following annotation would now make this transformer ignore a specific toString method: |
| for (ConstantExpression expression in node.annotations.whereType<ConstantExpression>()) { | ||
| final InstanceConstant constant = expression.constant as InstanceConstant; | ||
| if (constant.classNode.name == 'pragma' && constant.classNode.enclosingLibrary.importUri.toString() == 'dart:core') { | ||
| final StringConstant pragmaName = constant.fieldValues.values.first as StringConstant; |
There was a problem hiding this comment.
constant.fieldValues.values.first doesn't look reliable as there are multiple fields which may go in an arbitrary order in constant.fieldValues map.
There was a problem hiding this comment.
Aww phooey. I'll try to fix that up. My hope was that since this should be a stable order map (the contract for Map is that order remains the same), and we know the type has its first parameter as the name, this could be ok. But I'll try to do something better here.
|
|
||
| bool _hasKeepAnnotation(Procedure node) { | ||
| for (ConstantExpression expression in node.annotations.whereType<ConstantExpression>()) { | ||
| final InstanceConstant constant = expression.constant as InstanceConstant; |
There was a problem hiding this comment.
Consider testing if expression.constant is InstanceConstant, as there could be other kinds of constants.
There was a problem hiding this comment.
Done, and did it for the cast below as well.
| import 'package:kernel/ast.dart'; | ||
| import 'package:path/path.dart' as path; | ||
|
|
||
| import 'package:vm/incremental_compiler.dart'; |
| return completer.future; | ||
| } | ||
|
|
||
| /// A [RecursiveVisitor] that replaces [Object.toString] overrides with |
There was a problem hiding this comment.
Should this code go in a separate file/library?
There was a problem hiding this comment.
I started doing that but it seemed to be overly complicating things to me - these classes are pretty short right now and the style in the engine and flutter repos tend to be to group related things together rather than splitting files.
That said I don't have a strong opinion about this one either way.
There was a problem hiding this comment.
It probably makes more sense to move the transformations to (a) separate library(ies) when the second one gets added. Let's keep an eye out for that, or leave a comment/TODO.
| if (pragmaName is! StringConstant) { | ||
| continue; | ||
| } | ||
| if ((pragmaName as StringConstant).value == 'flutter_frontend:keep') { |
There was a problem hiding this comment.
Nit: maybe call the pragma simply 'flutter:keep' ?
There was a problem hiding this comment.
Could you give an example of where this annotation would go? In what situations is it needed?
There was a problem hiding this comment.
class MyStringBuffer {
StringBuffer _stringBuffer = StringBuffer();
// ...
@override
@pragma('flutter_frontend:keep')
String toString() {
return _stringBuffer.toString();
}
}Would prevent the transformer from overwriting the method.
There was a problem hiding this comment.
Are there cases where a program would be incorrect, or the transformation would do something wrong if the annotation is not present?
There was a problem hiding this comment.
If someone tried to apply this transfomer to dart:core, it would break their program - all the primitive type classes override toString in meaningful ways, as does StringBuffer.
You shouldn't do that, and we shouldn't make it easy for people to do that. The plan right now is to use this for dart:ui and package:flutter to save binary space, and to test this in a controlled internal environment with programs that have good test coverage in the modes we apply it to.
There was a problem hiding this comment.
Talked offline. Going to add a type to dart:ui called keepToString.
There was a problem hiding this comment.
Added annotation to dart:ui for this called keepToString, plus documentation there about this feature and how to use the annotation.
| continue; | ||
| } | ||
| final InstanceConstant constant = expression.constant as InstanceConstant; | ||
| if (constant.classNode.name == 'keepToString' && constant.classNode.enclosingLibrary.importUri.toString() == 'dart:ui') { |
There was a problem hiding this comment.
Class name is _KeepToString, not keepToString.
There was a problem hiding this comment.
Ahh. I really need an integration test for this. I'll add one.
There was a problem hiding this comment.
Fixed and added an integration test that would catch this in the future.
| return completer.future; | ||
| } | ||
|
|
||
| /// A [RecursiveVisitor] that replaces [Object.toString] overrides with |
There was a problem hiding this comment.
It probably makes more sense to move the transformations to (a) separate library(ies) when the second one gets added. Let's keep an eye out for that, or leave a comment/TODO.
lib/ui/annotations.dart
Outdated
| /// `toString` bodies with `return super.toString()` during compilation. This | ||
| /// significantly reduces release code size, and would make it impossible to | ||
| /// implement a meaningful override of `toString` for release mode without | ||
| /// disabling the feature and losing the size savings. If package uses this |
There was a problem hiding this comment.
If package uses --> If a package uses
| /// `toString` bodies with `return super.toString()` during compilation. This | ||
| /// significantly reduces release code size, and would make it impossible to | ||
| /// implement a meaningful override of `toString` for release mode without | ||
| /// disabling the feature and losing the size savings. If package uses this |
| // @dart = 2.6 | ||
| part of dart.ui; | ||
|
|
||
| /// Annotation used by Flutter's Dart compiler to indicate that an |
There was a problem hiding this comment.
Should the short description reference what happens in release vs. debug?
There was a problem hiding this comment.
The way the flag is implemented, it's not really specific to release or debug.
When we get to a point where we'd turn this on by default in release we should probably mention something. I'll add a TODO to update the docs for this when that happens.
|
Filed flutter/flutter#52770 to track the infra flake, landing this on red to kick flaky infra failures. |
…tring` for selected packages (flutter/engine#17068)
The idea is to eventually apply this to
dart:uiandpackage:flutter/*. This saves about 200kb on the gallery snapshot. An audit of the codebase (and the flutter style guide) point to these methods always being used for debug only info.