-
Notifications
You must be signed in to change notification settings - Fork 6k
Teach frontend compiler to replace toString with super.toString for selected packages
#17068
Conversation
|
Ah. I should write some tests for this. |
jonahwilliams
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
zanderso
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fyi @goderbauer
|
|
||
| @override | ||
| void visitProcedure(Procedure node) { | ||
| if (node.name.name == 'toString' && node.enclosingLibrary != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "to" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "too" -> "to" ?
|
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to replace node.function.body. If that works that sounds better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, and updated tests.
| void visitProcedure(Procedure node) { | ||
| if ( | ||
| node.name.name == 'toString' && | ||
| node.enclosingClass != null && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can also check !node.isStatic && !node.isAbstract
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| if ( | ||
| node.name.name == 'toString' && | ||
| node.enclosingClass != null && | ||
| node.enclosingLibrary != null && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
|
||
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider testing if expression.constant is InstanceConstant, as there could be other kinds of constants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: alphabetize imports.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| return completer.future; | ||
| } | ||
|
|
||
| /// A [RecursiveVisitor] that replaces [Object.toString] overrides with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this code go in a separate file/library?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
alexmarkov
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
| if (pragmaName is! StringConstant) { | ||
| continue; | ||
| } | ||
| if ((pragmaName as StringConstant).value == 'flutter_frontend:keep') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: maybe call the pragma simply 'flutter:keep' ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jonahwilliams @zanderso wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you give an example of where this annotation would go? In what situations is it needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Talked offline. Going to add a type to dart:ui called keepToString.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Class name is _KeepToString, not keepToString.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh. I really need an integration test for this. I'll add one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed and added an integration test that would catch this in the future.
zanderso
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm w/ nits
| return completer.future; | ||
| } | ||
|
|
||
| /// A [RecursiveVisitor] that replaces [Object.toString] overrides with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If package uses --> If a package uses
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done and done.
| /// `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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
| // @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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the short description reference what happens in release vs. debug?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
alexmarkov
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still lgtm.
|
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.