-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Figure out how the "static access shorthand" feature (a.k.a. "enum shorthands") should handle selector chains and more complex expressions #4130
Comments
For reference, I took a look at the Swift spec. The enum shorthand thing in Swift is called "implicit member access" (a pretty nice name, I think). It does allow selectors ("postfix expressions") after the implicit member. The reference says:
You can even have a member chain where the various return types in the chain wander away from the context type, as long the final operation in the chain returns a subtype of the context type. They also do what Lasse proposed and allow implicit access to members on the underlying type when the context type is nullable:
Given that, trying to go ahead and support entire selector chains in Dart seems reasonable to me too. Given the way cascades and null-shorting work, I suspect that users will expect it to work. I think Paul's point in the language meeting about users expecting auto-complete to work after a shorthand was pretty compelling.
I think we should follow the same rules that we use to determine which surrounding expressions are subject to null-aware shorting. |
There are multiple possible levels here.
I personally think "selector chain" is a good trade-off. The operators makes things much more complicated because they introduce context types and treat left/right operands differently (not that we don't otherwise), and "constructor only" introduces more special-cases than it's worth. Including assignment and increment/decrement (which are supported by null-shortening, feels a little wrong here. Assignment introduces a completely new value expression with its own context type, whose value becomes the value of the assignment expression. int x = .parse(text).extensionSomething().counter = .fromEnvironment("wohoo"); I guess the (For |
One underlying issue with the second option above (getters, function and visitor invocations only) is that the semantics, static and dynamic, for It's non-trivial to do that for For the latter options, it's a matter of defining the contexts that introduce an implicit static receiver, and how it's propagated to the |
I don't understand what you mean here, and I don't think this is really correct. I mean, it's fine if you want to special case it this way, but in either case, the process you are following for
|
Following up here as well as via email. I'm open to shipping the full selector version, but... I really want to be sure folks are doing this with their eyes open. This is a very expensive feature, with a huge testing area, and with a very large step function in the ongoing complexity of the language. Think of all of the complexity that we run into with type inference contexts, implicit casts, etc. We're proposing, with this feature, to essentially double that surface area. Every single place in the language where a new typing context is produced, we now need to produce (and test for) a new static implicit access context being produced and propagated. Every place that is enclosed by a static implicit access context but should not have access to it needs to be tested to ensure that one is not accidentally propagated. Interactions between coercions need to be specified, implemented, and tested. Promotion and demotion need to be specified, implemented and tested. There is a huge amount of work implied in here. All that's fine if we really think this is worth it, but I have to admit, I'm deeply unconvinced that the selector chain examples will ever see widespread use. It's just hard for me to see myself, in the middle of coding, to be starting to write something like I'm somewhat sympathetic to the usefulness of this for constructor invocations, but again, I'd like some examples/evidence that this would actually be useful in practice. Someone suggested the example of the Here are some quick examples of the kinds of things that we're going to have to specify, implement, and test if we ship the full version of this feature (just off the top of my head). class A {
static A getA() => A();
}
// This should work
void test(bool b) {
A? a = (b) ? .getA() : ((b) ? .getA() : .getA());
} class A {
static A get getA => A();
A call(int x) => A();
}
// This should work
void test(bool b) {
A? a = .getA(3);
} class A {}
class B extends A {
static A getA() => A();
}
// This should work
void test(A a) {
if (a is B) {
a = .getA();
// a = B.getA(); demotes a to A
}
} class A {
static A getA() => A();
}
class B extends A {}
void test() {
{
A? a = A();
a ?? .getA(); // I think this should work?
}
{
A a = A();
B b = a ?? .getA(); // I think this shouldn't work, because the context for the RHS is B?
}
} |
@leafpetersen I'm not sure the complexity is as high as you're suggesting here. If we go with just selector chains, the specification will basically be: Grammar<postfixExpression> ::= ... ;; existing cases
| <implicitStasticMemberAccesss> <selector>*
<implicitStasticMemberAccesss> ::= '.' (<identifier> | 'new') plus a similar entry for constant patters. Static semanticsType inference on a For every place in the specification where we specify the meaning of There is no propagation of contexts through recursive inference, the context type used is recognized immediately at the We are duplicating something, but it's really only the tail-inferences, the final static member invocation, where we allow both Complexity aside, we could stop at <postfixExpression> ::= '.' <identifier> <argumentPart>? | '.' 'new' <arguments> That grammatically reduces what we can accept, and if we're fine with |
FWIW, I think in swift, the expression I don't understand the additional complications related to nullable types like |
@lrhn I don't think this is correct. I think you are being misled by the syntactic nature of the grammar, which doesn't reflect the structure of the AST. In any case, you and @stereotype441 seem to be disagreeing here about whether propagation is required, so perhaps you and he should hash this out offline? |
I'm happy to meet with either you or @lrhn offline if it helps. But I guess I should point out that even though the grammar in the language spec currently says FWIW, a similar situation arises within a cascade section (which, simplifying a bit, essentially has the form So yeah, I believe that somehow the context information needs to get delivered down the tree. But that doesn't necessarily mean it's going to be complex to do so. We have a lot of clever ways to move information from point A to point B, and I suspect this particular piece of information is going to be pretty simple to move around, because none of the intermediate AST nodes needs to be able to modify it. I'm going to experiment with some possible implementation approaches and see whether I can find an approach that handles selector chains without too much complex machinery. |
I did a little experimentation with an example that was listed in the other mega-thread (I think taken from the Flutter docs). The original code looks like this: Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: Color(0xFFFFFFFF)),
left: BorderSide(color: Color(0xFFFFFFFF)),
right: BorderSide(),
bottom: BorderSide(),
),
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: Color(0xFFDFDFDF)),
left: BorderSide(color: Color(0xFFDFDFDF)),
right: BorderSide(color: Color(0xFF7F7F7F)),
bottom: BorderSide(color: Color(0xFF7F7F7F)),
),
color: Color(0xFFBFBFBF),
),
child: const Text(
'OK',
textAlign: TextAlign.center,
style: TextStyle(color: Color(0xFF000000))
),
),
) If I assume the following static extension declarations: extension on Decoration {
const factory Decoration box({
Color? color,
DecorationImage? image,
BoxBorder? border,
BorderRadiusGeometry? borderRadius,
List<BoxShadow>? boxShadow,
Gradient? gradient,
BlendMode? backgroundBlendMode,
BoxShape shape = BoxShape.rectangle,
}) = BoxDecoration;
}
extension on BoxBorder {
const factory BoxBorder border(
{BorderSide top = BorderSide.none,
BorderSide right = BorderSide.none,
BorderSide bottom = BorderSide.none,
BorderSide left = BorderSide.none}) = Border;
}
extension on EdgeInsetsGeometry {
// What do we do here? We need to cover
// {EdgeInsets, EdgeInsetsDirectional}.{all, only, symmetric}
// with this extension if we want to get rid of the EdgeInsets prefix below.
} then I believe, if I've done the resolution correctly in my head, that we get the following: Container(
decoration: const .box(
border: .border(
top: .new(color: .new(0xFFFFFFFF)),
left: .new(color: .new(0xFFFFFFFF)),
right: .new(),
bottom: .new(),
),
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0),
decoration: const .box(
border: .border(
top: .new(color: .new(0xFFDFDFDF)),
left: .new(color: .new(0xFFDFDFDF)),
right: .new(color: .new(0xFF7F7F7F)),
bottom: .new(color: .new(0xFF7F7F7F)),
),
color: .new(0xFFBFBFBF),
),
child: const Text('OK',
textAlign: .center,
style: .new(color: .new(0xFF000000))),
),
);
} I didn't have any great ideas about how to name the This is definitely shorter code. Editing felt a little odd since it wasn't obvious to me when I could just type @goderbauer @mit-mit thoughts on this? Is this level of brevity worth some extra cost? Is not supporting arbitrary selector chains reasonable? |
We should maybe consider it a kind of tech-debt that our specification and implementation do not agree. Let's assume we changed the grammar to: <postfixExpression> ::= <assignableExpression> <postfixOperator>
| <selectorExpression>
<selectorExpression> ::= <primary> | <selectorExpression> <selector>
<primary> ::= ...existing rules ...
| <implicitStaticMemberAccess>
<implicitStaticMemberAccess> ::= '.' (<identifier> | 'new') which I believe would better match the AST, and still only applied the implicit static member context to selector chains, not assignments/increments. (And found a way to specify null shortening recursively, if it isn't already.) Then we would capture the context type at (I'll try to see what it would look like if we also want to include assignments and increment operators, to match the null-shortening's extent.) |
The Dart static access shorthand proposal specifies a new grammar construct allowing a selector chain to begin with a
.
followed by an identifier ornew
(the.
may be preceded byconst
). For example (from the spec draft):The currently specified semantics is that the context for the entire selector chain is used to resolve the identifier. This enables more complex examples like this (also from the spec draft):
This implies some action at a distance, which will in turn require some extra bookkeeping in the implementations. To see why, consider that a selector chain can involve an arbitrary large number of method invocations, function invocations, null checks, index operations, and property accesses. For example, this would be permitted:
The parse tree for this declaration would look something like this (making some guesses and simplifying assumptions about the AST representation):
The existing downwards inference mechanism passes the context
A
down from the VariableDeclaration to the PropertyGet, but no further. To implement the feature as currently described, we would need some sort of mechanism for plumbing the context down to the MethodInvocation at the bottom of the tree.The question at issue here is: what would be the cost of such a mechanism (both in terms of performance and implementation complexity)? Would there be enough user benefit to justify it?
Follow up question: what if we decided to expand the mechanism to cover other kinds of syntax that can appear to the right of an expression (binary operators, conditional expressions, or cascades)? For example:
The text was updated successfully, but these errors were encountered: