From forax at univ-mlv.fr Wed Jun 1 14:22:51 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Wed, 1 Jun 2022 16:22:51 +0200 (CEST) Subject: Named record pattern In-Reply-To: References: <8bc9cbde-83d6-e400-21f6-f6faf75d6034@oracle.com> <7fe8ca64-c9af-8075-d029-bbfebfffe46e@oracle.com> Message-ID: <262948885.134806.1654093371840.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Tagir Valeev" > Cc: "amber-spec-experts" > Sent: Tuesday, May 31, 2022 6:12:06 PM > Subject: Re: Named record pattern > Gavin reminded me that we are not finalizing patterns in switch in 19 (hard to > keep track, sometimes), so we have a little bit of time to figure out what we > want here. > One thing that is potentially confusing is that patterns work indirectly in a > number of ways. For example, if we have a declared deconstruction pattern for > Point, you can't *invoke* it as you can a method; the language runtime invokes > it on your behalf under the right situations. (In this way, a deconstructor is > a little like a static initializer; it is a body of code that you declare, but > you can't invoke it directly, the runtime invokes it for you at the right time, > and that's fine.) > I had always imagined the relationship with locals being similar; a pattern > causes a local to be injected into certain scopes, but the pattern itself is > not a local variable declaration. Obviously there is more than one way to > interpret this, so we should make a more deliberate decision. > As a confounding example that suggests that pattern variables are not "just > locals", in the past we talked about various forms of "merging": > if (t instanceof Box(String s) || t instanceof Bag(String s)) { ... } > or > case Box(String s): > case Bag(String s): > common-code; > If pattern variables could be annotated, then the language would be in the > position of deciding what happens with > case Box(@Foo(1) String s): > case Bag(@Foo(2) String s): > (This is the "annotation merging" problem, which is why annotations are not > inherited in the first place.) > I don't have an answer here, but I'm going to think about the various issues and > try to capture them in more detail before proposing an answer. For me, there is a difference between a binding and a local variable, is that bindings are the one declared inside a pattern and a local variables is how a binding is transformed to be usable inside the boby if the pattern match. So we can merge bindings and as a result have one local variable to be used in the body. About the annotations, if we follow the data orientation principle (data is more important than code), we can have a record record Person(@NonNull String name) { } and a record pattern case Person(@NonNull String s) -> ... We want to be able to declare @NonNull in the record pattern so if the data is changed, if the component name of the record becomes nullable by example, the pattern will fail to compile. So i think we should allow annotations because it's a way to enforce that data are more important that code. As you said, annotation merging is an issue because the correct merging requires to know the semantics of the annotations and the compiler has no way to know that. But there is a simple solution, annotation are trees, so they can be compared structurally. Thus we can do bindings merging if the annotations are the same structurally. regards, R?mi From brian.goetz at oracle.com Wed Jun 1 15:09:30 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Wed, 1 Jun 2022 11:09:30 -0400 Subject: Named record pattern In-Reply-To: <262948885.134806.1654093371840.JavaMail.zimbra@u-pem.fr> References: <8bc9cbde-83d6-e400-21f6-f6faf75d6034@oracle.com> <7fe8ca64-c9af-8075-d029-bbfebfffe46e@oracle.com> <262948885.134806.1654093371840.JavaMail.zimbra@u-pem.fr> Message-ID: > For me, there is a difference between a binding and a local variable, > is that bindings are the one declared inside a pattern and a local > variables is how a binding is transformed to be usable inside the boby > if the pattern match. Our first (wrong) inclination was to treat pattern variables as a whole separate thing from locals.? This, in turn, led to further gratuitous divergence such as the treatment of finality. The big difference between a pattern variable and a local is their scope.? We have carefully defined the scope of a pattern variable to be exactly those places where it would be DA. > So we can merge bindings and as a result have one local variable to be > used in the body. This only works if we are cagy about *where* the declaration is.? If we say that in ??? case Foo(int x), Bar(int x): that both `int x` are separate declarations, then we've foreclosed on this avenue.? If instead we let patterns "summon locals into existence", then if they sing in proper harmony, then two patterns can summon the same variable.? While we're not 100% dedicated to this, ruling it out also seems questionable. > About the annotations, if we follow the data orientation principle > (data is more important than code), we can have a record > ? record Person(@NonNull String name) { } This is an annotation *on the record component*.? There are rules for how annotations flow from components to other API facets (fields, ctor arguments, accessor methods, etc.) > and a record pattern > ? case Person(@NonNull String s) -> ... The question is: what is being annotated here?? The pattern itself, the binding variable, or the type-use of String?? Annotations on patterns would be a little like annotations on record components; they may flow through to other things, but the pattern and the binding are separate. > We want to be able to declare @NonNull in the record pattern so if the > data is changed, if the component? name of the record becomes nullable > by example, the pattern will fail to compile. > So i think we should allow annotations because it's a way to enforce > that data are more important that code. This is awfully handwavy; I'd prefer to make these decisions on some other basis than "it seems to arrive at the answer I want in this case."? In fact, everything about this argument is making me think annotations on pattern variables is a serious mistake. > But there is a simple solution, annotation are trees, so they can be > compared structurally. Thus we can do bindings merging if the > annotations are the same structurally. I don't think we want to touch this with a ten foot pole. From daniel.smith at oracle.com Wed Jun 1 21:51:18 2022 From: daniel.smith at oracle.com (Dan Smith) Date: Wed, 1 Jun 2022 21:51:18 +0000 Subject: Simplifying switch labels Message-ID: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> I recently reviewed the spec changes for patterns in switch, and found the treatment of switch labels pretty hard to work out. Since this is really about the design more than the specification, I thought I'd share some thoughts here. Consider this early feedback on the Java 19 preview. The status quo: syntactically, a switch label is a set of colon- and comma-separated elements, where an *element* is one of { constant, enum, pattern, null, default }. (I'm oversimplifying delimiters a little: 'case' or a plain 'default' must follow ':', but not ','.) Layered on top of that syntax are a number of restrictions: colon delimiters aren't allowed when using switch *rules*; the entire set of elements must not have repeat elements; constants and enums are only allowed for certain switch types; certain combinations of elements are disallowed, while others are okay. Then there are inter-label restrictions, mainly expressed by the dominance relation. I find all these non-syntactic rules to be pretty hard to keep in my head, especially when it comes to which combinations of elements are allowed. A couple of simplifying moves I'd suggest: (1) Don't try to merge sets of switch block labels (e.g., 'case foo: case bar:'). These consecutive labels have the *effect* of handling multiple cases with a single block of code, but I think we can formally treat them as two unique blocks, the first of which falls through to the second. And I think that framing is more in line with how programmers would typically read the syntax. In this framing, the restrictions about sets of elements in a single label don't apply, because we're talking about two different labels. But we have rules to prevent various abuses. Examples: case 23: case Pattern: // illegal before and now, due to fallthrough Pattern rule case Pattern: case Pattern: // ditto case null: case Pattern: // allowed before, illegal now: use a comma for null case Pattern: default: // illegal before, legal now: you fell through to the default case case Pattern: noop(); default: // legal before and now case Pattern: case null: // binds the pattern before, pattern is out of scope now case Pattern: noop(); case null: // legal with pattern out of scope, before and now case Pattern: case 23: // illegal before, legal now with fallthrough case 23: case 23: // illegal before and now, due to dominance case default: default: // illegal before and now, due to "only one default" rule Another way to argue this is that I think 'case Pattern: case somethingelse' has a lot more in common, syntactically and conceptually, with 'case Pattern: noop(); case somethingelse:' than it does with 'case Pattern, somethingelse'. A possible limitation is if there are already special rules in the language that treat colon-delimited labels differently than separate blocks with fallthrough. I can't think of any right now, but I may be forgetting something... (2) Reduce the syntactic surface of comma-separated switch labels. There are a lot of combinations of elements?maybe a majority??that don't make sense and we prohibit. There are some others that are a bit odd, but we allow them anyway. I'd prefer to cut back on having multiple ways to do things, and syntactically enumerate the few cases that are actually meaningful. Something like: SwitchLabel: case CaseValue { , CaseValue } : case Pattern { Guard }: case null, Pattern: default: case null, default: CaseValue: null ConstantExpression EnumConstantName (There's some ambiguity in CaseValue, and I think we should do better with 'ConstantExpression', but okay, set that aside, this is the concept at least.) Note that the second kind of Pattern SwitchLabel is especially weird?it binds 'null' to a pattern variable, and requires the pattern to be a (possibly parenthesized) type pattern. So it's nice to break it out as its own syntactic case. I'd also suggest rethinking whether "case null," is the right way to express the two kinds of nullable SwitchLabels, but anyway now it's really clear that they are special. Rules like "no duplicates" and "only for certain switch types" only need to be expressed as constraints on the second kind of SwitchLabel. Dominance also seems like it would be more manageable to specify/understand. Some things this would newly disallow: - 'case default:'?just say 'default:' - case 23, default:'?just say 'default:' (mention '23' in a comment if it's important to call out) - 'case default, null:'?could add this case, I guess, or just say that 'default' always goes second - 'case Pattern, null:'?ditto - 'case null, Pattern Guard:'?confusing whether the guard is checked when 'null' This cuts back especially on the degrees of freedom for 'default': the only useful thing you can add to it, and should want to add to it, is making it match null. From brian.goetz at oracle.com Thu Jun 2 18:08:14 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 2 Jun 2022 14:08:14 -0400 Subject: Simplifying switch labels In-Reply-To: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> References: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> Message-ID: <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> > In this framing, the restrictions about sets of elements in a single label don't apply, because we're talking about two different labels. But we have rules to prevent various abuses. Examples: > > case 23: case Pattern: // illegal before and now, due to fallthrough Pattern rule Ideally, the fallthrough rule should be about _bindings_, not _patterns_.? If P an Q are patterns with no binding variables, then it should be OK to say: ??? case P: ??? case Q: The rule about fallthrough is to prevent falling into code where the bindings are not DA. > Note that the second kind of Pattern SwitchLabel is especially weird?it binds 'null' to a pattern variable, and requires the pattern to be a (possibly parenthesized) type pattern. So it's nice to break it out as its own syntactic case. I'd also suggest rethinking whether "case null," is the right way to express the two kinds of nullable SwitchLabels, but anyway now it's really clear that they are special. This is a painful compromise.? While this may be a transitional phenomena, the rule "switch matches null only when `null` appears in a case" is a helpful way to transition people away from "switch always throws on null." From forax at univ-mlv.fr Thu Jun 2 18:16:47 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Thu, 2 Jun 2022 20:16:47 +0200 (CEST) Subject: Simplifying switch labels In-Reply-To: <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> References: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> Message-ID: <1823695654.927518.1654193807132.JavaMail.zimbra@u-pem.fr> ----- Original Message ----- > From: "Brian Goetz" > To: "daniel smith" , "amber-spec-experts" > Sent: Thursday, June 2, 2022 8:08:14 PM > Subject: Re: Simplifying switch labels >> In this framing, the restrictions about sets of elements in a single label don't >> apply, because we're talking about two different labels. But we have rules to >> prevent various abuses. Examples: >> >> case 23: case Pattern: // illegal before and now, due to fallthrough Pattern >> rule > > Ideally, the fallthrough rule should be about _bindings_, not > _patterns_.? If P an Q are patterns with no binding variables, then it > should be OK to say: > > ??? case P: > ??? case Q: > > The rule about fallthrough is to prevent falling into code where the > bindings are not DA. > >> Note that the second kind of Pattern SwitchLabel is especially weird?it binds >> 'null' to a pattern variable, and requires the pattern to be a (possibly >> parenthesized) type pattern. So it's nice to break it out as its own syntactic >> case. I'd also suggest rethinking whether "case null," is the right way to >> express the two kinds of nullable SwitchLabels, but anyway now it's really >> clear that they are special. > > This is a painful compromise.? While this may be a transitional > phenomena, the rule "switch matches null only when `null` appears in a > case" is a helpful way to transition people away from "switch always > throws on null." It also allows to express that a switch statement on an enum should be exhaustive by adding case null -> throw null; R?mi From daniel.smith at oracle.com Thu Jun 2 20:22:03 2022 From: daniel.smith at oracle.com (Dan Smith) Date: Thu, 2 Jun 2022 20:22:03 +0000 Subject: Simplifying switch labels In-Reply-To: <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> References: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> Message-ID: <2C92D62C-1BE0-404A-9B55-DE396446DCCC@oracle.com> > On Jun 2, 2022, at 12:08 PM, Brian Goetz wrote: > >> In this framing, the restrictions about sets of elements in a single label don't apply, because we're talking about two different labels. But we have rules to prevent various abuses. Examples: >> >> case 23: case Pattern: // illegal before and now, due to fallthrough Pattern rule > > Ideally, the fallthrough rule should be about _bindings_, not _patterns_. If P an Q are patterns with no binding variables, then it should be OK to say: > > case P: > case Q: > > The rule about fallthrough is to prevent falling into code where the bindings are not DA. Yes, understood. The current explanation doesn't actually treat this as fallthrough, though: it treats it as if you said 'case 23, Pattern:', which is disallowed by a different rule. I'm suggesting not doing that implicit rewrite, and instead leaving this case to the fallthrough rule. >> Note that the second kind of Pattern SwitchLabel is especially weird?it binds 'null' to a pattern variable, and requires the pattern to be a (possibly parenthesized) type pattern. So it's nice to break it out as its own syntactic case. I'd also suggest rethinking whether "case null," is the right way to express the two kinds of nullable SwitchLabels, but anyway now it's really clear that they are special. > > This is a painful compromise. While this may be a transitional phenomena, the rule "switch matches null only when `null` appears in a case" is a helpful way to transition people away from "switch always throws on null." Right. So 'case null, String s' is good in that it's easy to see the null. What's troubling about it is that, at runtime, instead of testing "is this a null, or is this a String named s?", we do "is this a null that we can treat as a String named s, or is this a String named s?" (Of course these two questions are equivalent in terms of the true/false test, but they mean something different for variable bindings.) Anyway, I don't have a good alternative syntax proposal, but I at least want to draw attention to the weirdness of this case (the comma doesn't mean what you think it means), and appeal to that weirdness to push back on a framing that switch labels are just an open-ended mixture of things drawn from the "element" bucket. From daniel.smith at oracle.com Thu Jun 2 20:34:49 2022 From: daniel.smith at oracle.com (Dan Smith) Date: Thu, 2 Jun 2022 20:34:49 +0000 Subject: Simplifying switch labels In-Reply-To: <2C92D62C-1BE0-404A-9B55-DE396446DCCC@oracle.com> References: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> <2C92D62C-1BE0-404A-9B55-DE396446DCCC@oracle.com> Message-ID: <66C93157-D44B-4320-92EB-4BD5CB8BA055@oracle.com> On Jun 2, 2022, at 2:22 PM, Dan Smith > wrote: Ideally, the fallthrough rule should be about _bindings_, not _patterns_. If P an Q are patterns with no binding variables, then it should be OK to say: case P: case Q: The rule about fallthrough is to prevent falling into code where the bindings are not DA. Yes, understood. The current explanation doesn't actually treat this as fallthrough, though: it treats it as if you said 'case 23, Pattern:', which is disallowed by a different rule. Oh, I guess I missed your point here, thinking that P and Q were constants. Your comment implies that the two rules that restrict usage of patterns?can't fall through past one, and can't combine one (via ',') with most other labels?could be relaxed slightly in the case of patterns that have no bindings. I suppose that's formally true, though I'm not sure it's practically all that useful. (The only non-binding pattern we have right now is a zero-component record, right? And any non-binding patterns in the future could be equivalently expressed with 'when' clauses.) This is an orthogonal point to my suggestion that sets of switch block labels not be merged; it is relevant, however, to my suggestion that Patterns be syntactically impossible to combine (via ',') with most other labels. From brian.goetz at oracle.com Thu Jun 2 20:59:38 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Thu, 2 Jun 2022 16:59:38 -0400 Subject: Simplifying switch labels In-Reply-To: <66C93157-D44B-4320-92EB-4BD5CB8BA055@oracle.com> References: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> <2C92D62C-1BE0-404A-9B55-DE396446DCCC@oracle.com> <66C93157-D44B-4320-92EB-4BD5CB8BA055@oracle.com> Message-ID: <805f52d0-c7d6-6e7b-a8e9-917886c63f0c@oracle.com> > Oh, I guess I missed your point here, thinking that P and Q were > constants. > > Your comment implies that the two rules that restrict usage of > patterns?can't fall through past one, and can't combine one (via ',') > with most other labels?could be relaxed slightly in the case of > patterns that have no bindings. I suppose that's formally true, though > I'm not sure it's practically all that useful. (The only non-binding > pattern we have right now is a zero-component record, right? And any > non-binding patterns in the future could be equivalently expressed > with 'when' clauses.) Here's an example that's not so contrived: ??? String kind = switch (o) { ??????? case Integer _, Long _, Short _, Character _, Byte _ -> "integral"; ??????? case Double _, Float _ -> "floating point"; ??????? case Boolean _ -> "boolean"; ??????? default -> "something else"; ??? }; Once we have a "don't care" pattern, any pattern can become binding-less. Looking two steps ahead, we might decide it is not so much that there can be _no_ bindings, as much as are the bindings unifiable: ??? case Bag(String x), Box(String x) -> "container of string"; From daniel.smith at oracle.com Thu Jun 2 23:20:30 2022 From: daniel.smith at oracle.com (Dan Smith) Date: Thu, 2 Jun 2022 23:20:30 +0000 Subject: Simplifying switch labels In-Reply-To: <805f52d0-c7d6-6e7b-a8e9-917886c63f0c@oracle.com> References: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> <2C92D62C-1BE0-404A-9B55-DE396446DCCC@oracle.com> <66C93157-D44B-4320-92EB-4BD5CB8BA055@oracle.com> <805f52d0-c7d6-6e7b-a8e9-917886c63f0c@oracle.com> Message-ID: <71FD3CE7-ADE8-4875-AEEC-FFC020C5BE32@oracle.com> > On Jun 2, 2022, at 2:59 PM, Brian Goetz wrote: > > >> Oh, I guess I missed your point here, thinking that P and Q were constants. >> >> Your comment implies that the two rules that restrict usage of patterns?can't fall through past one, and can't combine one (via ',') with most other labels?could be relaxed slightly in the case of patterns that have no bindings. I suppose that's formally true, though I'm not sure it's practically all that useful. (The only non-binding pattern we have right now is a zero-component record, right? And any non-binding patterns in the future could be equivalently expressed with 'when' clauses.) > > Here's an example that's not so contrived: > > String kind = switch (o) { > case Integer _, Long _, Short _, Character _, Byte _ -> "integral"; > case Double _, Float _ -> "floating point"; > case Boolean _ -> "boolean"; > default -> "something else"; > }; > > Once we have a "don't care" pattern, any pattern can become binding-less. > > Looking two steps ahead, we might decide it is not so much that there can be _no_ bindings, as much as are the bindings unifiable: > > case Bag(String x), Box(String x) -> "container of string"; Okay, those are helpful illustrations, thanks. So an argument against a syntactic prohibition on things like 'Pattern, Pattern:' is that we may support those, subject to certain semantic restrictions, in the future. I wonder how we would try to explain 'case Integer x, null, Double x'... (Does 'x' get bound to 'null'? How?) From brian.goetz at oracle.com Fri Jun 3 13:04:25 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 3 Jun 2022 09:04:25 -0400 Subject: Simplifying switch labels In-Reply-To: <71FD3CE7-ADE8-4875-AEEC-FFC020C5BE32@oracle.com> References: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> <4ed3efea-072b-a969-0390-41cbdb20f67c@oracle.com> <2C92D62C-1BE0-404A-9B55-DE396446DCCC@oracle.com> <66C93157-D44B-4320-92EB-4BD5CB8BA055@oracle.com> <805f52d0-c7d6-6e7b-a8e9-917886c63f0c@oracle.com> <71FD3CE7-ADE8-4875-AEEC-FFC020C5BE32@oracle.com> Message-ID: <30399fb9-1aa2-a802-26a2-81193fa757cd@oracle.com> > I wonder how we would try to explain 'case Integer x, null, Double x'... (Does 'x' get bound to 'null'? How?) Your suggestion to always put the null first probably helps. From jonathan.gibbons at oracle.com Fri Jun 3 21:01:09 2022 From: jonathan.gibbons at oracle.com (Jonathan Gibbons) Date: Fri, 3 Jun 2022 14:01:09 -0700 Subject: Simplifying switch labels In-Reply-To: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> References: <35FF2AA0-219D-4A92-8CAD-2D3A60691297@oracle.com> Message-ID: Dan, > These consecutive labels have the *effect* of handling multiple cases with a single block of code, but I think we can formally treat them as two unique blocks, the first of which falls through to the second. Don't forget the handling in javac, and the notion of fallthrough, and `@SuppressWarnings("fallthrough")` -- Jon On 6/1/22 2:51 PM, Dan Smith wrote: > I recently reviewed the spec changes for patterns in switch, and found the treatment of switch labels pretty hard to work out. Since this is really about the design more than the specification, I thought I'd share some thoughts here. Consider this early feedback on the Java 19 preview. > > The status quo: syntactically, a switch label is a set of colon- and comma-separated elements, where an *element* is one of { constant, enum, pattern, null, default }. (I'm oversimplifying delimiters a little: 'case' or a plain 'default' must follow ':', but not ','.) Layered on top of that syntax are a number of restrictions: colon delimiters aren't allowed when using switch *rules*; the entire set of elements must not have repeat elements; constants and enums are only allowed for certain switch types; certain combinations of elements are disallowed, while others are okay. Then there are inter-label restrictions, mainly expressed by the dominance relation. > > I find all these non-syntactic rules to be pretty hard to keep in my head, especially when it comes to which combinations of elements are allowed. > > A couple of simplifying moves I'd suggest: > > (1) Don't try to merge sets of switch block labels (e.g., 'case foo: case bar:'). > > These consecutive labels have the *effect* of handling multiple cases with a single block of code, but I think we can formally treat them as two unique blocks, the first of which falls through to the second. And I think that framing is more in line with how programmers would typically read the syntax. > > In this framing, the restrictions about sets of elements in a single label don't apply, because we're talking about two different labels. But we have rules to prevent various abuses. Examples: > > case 23: case Pattern: // illegal before and now, due to fallthrough Pattern rule > case Pattern: case Pattern: // ditto > case null: case Pattern: // allowed before, illegal now: use a comma for null > case Pattern: default: // illegal before, legal now: you fell through to the default case > case Pattern: noop(); default: // legal before and now > case Pattern: case null: // binds the pattern before, pattern is out of scope now > case Pattern: noop(); case null: // legal with pattern out of scope, before and now > case Pattern: case 23: // illegal before, legal now with fallthrough > case 23: case 23: // illegal before and now, due to dominance > case default: default: // illegal before and now, due to "only one default" rule > > Another way to argue this is that I think 'case Pattern: case somethingelse' has a lot more in common, syntactically and conceptually, with 'case Pattern: noop(); case somethingelse:' than it does with 'case Pattern, somethingelse'. > > A possible limitation is if there are already special rules in the language that treat colon-delimited labels differently than separate blocks with fallthrough. I can't think of any right now, but I may be forgetting something... > > (2) Reduce the syntactic surface of comma-separated switch labels. > > There are a lot of combinations of elements?maybe a majority??that don't make sense and we prohibit. There are some others that are a bit odd, but we allow them anyway. I'd prefer to cut back on having multiple ways to do things, and syntactically enumerate the few cases that are actually meaningful. > > Something like: > > SwitchLabel: > case CaseValue { , CaseValue } : > case Pattern { Guard }: > case null, Pattern: > default: > case null, default: > > CaseValue: > null > ConstantExpression > EnumConstantName > > (There's some ambiguity in CaseValue, and I think we should do better with 'ConstantExpression', but okay, set that aside, this is the concept at least.) > > Note that the second kind of Pattern SwitchLabel is especially weird?it binds 'null' to a pattern variable, and requires the pattern to be a (possibly parenthesized) type pattern. So it's nice to break it out as its own syntactic case. I'd also suggest rethinking whether "case null," is the right way to express the two kinds of nullable SwitchLabels, but anyway now it's really clear that they are special. > > Rules like "no duplicates" and "only for certain switch types" only need to be expressed as constraints on the second kind of SwitchLabel. Dominance also seems like it would be more manageable to specify/understand. > > Some things this would newly disallow: > - 'case default:'?just say 'default:' > - case 23, default:'?just say 'default:' (mention '23' in a comment if it's important to call out) > - 'case default, null:'?could add this case, I guess, or just say that 'default' always goes second > - 'case Pattern, null:'?ditto > - 'case null, Pattern Guard:'?confusing whether the guard is checked when 'null' > > This cuts back especially on the degrees of freedom for 'default': the only useful thing you can add to it, and should want to add to it, is making it match null. From brian.goetz at oracle.com Fri Jun 10 12:44:51 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 10 Jun 2022 08:44:51 -0400 Subject: "With" for records Message-ID: In https://github.com/openjdk/amber-docs/blob/master/eg-drafts/reconstruction-records-and-classes.md we explore a generalized mechanism for `with` expressions, such as: ??? Point shadowPos = shape.position() with { x = 0 } The document evaluates a general mechanism involving matched pairs of constructors (or factories) and deconstruction patterns, which is still several steps out, but we can take this step now with records, because records have all the characteristics (significant names, canonical ctor and dtor) that are needed.? The main reason we might wait is if there are uncertainties in the broader target. Our C# friends have already gone here, in a way that fits into C#, using properties (which makes sense, as their language is built on that): ??? object with { property-assignments } The C# interpretation is that the RHS of this expression is a sort of "DSL", which permits property assignment but nothing else.? This is cute, but I think we can do better. In our version, the RHS is an arbitrary block of Java code; you can use loops, assignments, exceptions, etc.? The only thing that makes it "special" is that that the components of the operand are lifted into mutable locals on the RHS.? So inside the RHS when the operand is a Point, there are fresh mutable locals `x` and `y` which are initialized with the X and Y values of the operand.? Their values are committed at the end of the block using the canonical constructor. This should remind people of the *compact constructor* in a record; the body is allowed to freely mutate the special variables (who also don't have obvious declarations), and their terminal values determine the state of the record. Just as we were able to do record patterns without having full-blown deconstructors, we can do with expressions on records as well, because (a) we still have a canonical ctor, (b) we have accessors, and (c) we know the names of the components. Obviously when we get value types, we'll want classes to be able to expose (or not) such a mechanism (both for internal or external use). #### Digression: builders As a bonus, I think `with` offers us a better path to getting rid of builders than the (problematic) one everyone asks for (default values on constructor parameters.)? Consider the case of a record with many components, all of which are optional: ??? record Config(int a, ????????????????? int b, ????????????????? int c, ????????????????? ... ????????????????? int z) { ??? } Obviously, no one wants to call the canonical constructor with 26 values.? The standard workaround is a builder, but that's a lot of ceremony.? The `with` mechanism gives us a way out: ??? record Config(int a, ????????????????? int b, ????????????????? int c, ????????????????? ... ????????????????? int z) { ??????? private Config() { ??????????? this(0, 0, 0, ... 0); ??????? } ??????? public static Config BUILDER = new Config(); ??? } Now we can just say ??? Config c = Config.BUILDER with { c = 3; q = 45; } The constant isn't even necessary; we can just open up the constructor.? And if there are some required args, the constructor can expose them too.? Suppose a and b are required, but c..z are optional.? Then: ??? record Config(int a, ????????????????? int b, ????????????????? int c, ????????????????? ... ????????????????? int z) { ??????? public Config(int a, int b) { ??????????? this(a, b, 0, ... 0); ??????? } ??? } ??? Config c = new Config(1, 2) with { c = 3; q = 45; } In this way, the record acts as its own builder. (As an added bonus, the default values do not suffer from the "brittle constant" problem that a default value would likely suffer from, because they are an implementation detail of the constructor, not an exposed part of the API.) I think it is reasonable at this point to take this idea off the shelf and work towards delivering this for records, while we're building out the machinery needed to deliver this for general classes.? It has no remaining dependencies and is immediately useful for records. (As usual, please hold comments on small details until everyone has had a chance to comment on the general direction.) From forax at univ-mlv.fr Fri Jun 10 14:28:44 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Fri, 10 Jun 2022 16:28:44 +0200 (CEST) Subject: "With" for records In-Reply-To: References: Message-ID: <1564376667.5711009.1654871324376.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Friday, June 10, 2022 2:44:51 PM > Subject: "With" for records > In > [ > https://github.com/openjdk/amber-docs/blob/master/eg-drafts/reconstruction-records-and-classes.md > | > https://github.com/openjdk/amber-docs/blob/master/eg-drafts/reconstruction-records-and-classes.md > ] > we explore a generalized mechanism for `with` expressions, such as: > Point shadowPos = shape.position() with { x = 0 } > The document evaluates a general mechanism involving matched pairs of > constructors (or factories) and deconstruction patterns, which is still several > steps out, but we can take this step now with records, because records have all > the characteristics (significant names, canonical ctor and dtor) that are > needed. The main reason we might wait is if there are uncertainties in the > broader target. > Our C# friends have already gone here, in a way that fits into C#, using > properties (which makes sense, as their language is built on that): > object with { property-assignments } > The C# interpretation is that the RHS of this expression is a sort of "DSL", > which permits property assignment but nothing else. This is cute, but I think > we can do better. > In our version, the RHS is an arbitrary block of Java code; you can use loops, > assignments, exceptions, etc. The only thing that makes it "special" is that > that the components of the operand are lifted into mutable locals on the RHS. > So inside the RHS when the operand is a Point, there are fresh mutable locals > `x` and `y` which are initialized with the X and Y values of the operand. Their > values are committed at the end of the block using the canonical constructor. GREAT ! I've several questions, that we will have to answer later. The block is also special because there is an implicit return at the end ? so i believe "return" should be disallowed inside the block, right ? Does the lifting that appears at the beginning of the block call all accessors ? or the compiler try to be clever and not call an accessor when its value is not needed. For example, in point with { y = 3; } calling point.y() is useless, or is it something the JIT will take care of ? Do we allow "with" followed by an empty block ? As a way to clone a record ? About the declaration of local variables, in Java, there is no hiding/shadowing between local variables, so a code like this is rejected ? Or do we introduce a special rule for the hiding of implicit variables ? int x = Integer.parseInt(); ... foo(point with { y = 3; }); // x is declared twice because there is an implicit int x = point.x(); > This should remind people of the *compact constructor* in a record; the body is > allowed to freely mutate the special variables (who also don't have obvious > declarations), and their terminal values determine the state of the record. > Just as we were able to do record patterns without having full-blown > deconstructors, we can do with expressions on records as well, because (a) we > still have a canonical ctor, (b) we have accessors, and (c) we know the names > of the components. > Obviously when we get value types, we'll want classes to be able to expose (or > not) such a mechanism (both for internal or external use). yes, Complex.default with { re = 3; im = 4; } seems a great fit for value classes. R?mi From brian.goetz at oracle.com Fri Jun 10 15:25:38 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 10 Jun 2022 11:25:38 -0400 Subject: "With" for records In-Reply-To: <1564376667.5711009.1654871324376.JavaMail.zimbra@u-pem.fr> References: <1564376667.5711009.1654871324376.JavaMail.zimbra@u-pem.fr> Message-ID: <98a292f4-b029-858f-1cfb-559ad9436802@oracle.com> > > The block is also special because there is an implicit return at the > end ? so i believe "return" should be disallowed inside the block, right ? That's a good question; we can go in multiple directions with this. One would be to simply interpret "return" as "return from the enclosing method".? We confronted this with the blocks on the RHS of -> in switch expressions, and decided to disallow return there; that's probably a good default choice here too. > > Does the lifting that appears at the beginning of the block call all > accessors ? or the compiler try to be clever and not call an accessor > when its value is not needed. There's several reasons to just call them all, the biggest of which is that the accessors are incidental; really, there's a synthetic deconstruction pattern in the record, whose implementation just may delegate to accessors.? There is little semantic or performance benefit to not calling them (there shouldn't be side effects in accessors anyway, and the JIT will likely inline the calls away and see that unneeded fetches are dead anyway.) > For example, in > ? point with { y = 3; } > calling point.y() is useless, or is it something the JIT will take > care of ? For an ordinary record, the accessor is a field access, it will get inlined, and the fetch will be dead.? So, yes. > Do we allow "with" followed by an empty block ? As a way to clone a > record ? Yes.? The RHS is just a block of Java.? If it does nothing, it does nothing.? Free cloning. > About the declaration of local variables, in Java, there is no > hiding/shadowing between local variables, so a code like this is > rejected ? Or do we introduce a special rule for the hiding of > implicit variables ? Yes, we probably do need a special rule for this.? The component names are fixed, and collisions are likely, so these synthetic variables will probably have to be allowed to shadow other locals. > yes, Complex.default with { re = 3; im = 4; } seems a great fit for > value classes. Or even better: ??? value class Complex { ??????? ... ??????? Complex conj() -> this with { im = -im; } ??? } From steve at ethx.net Fri Jun 10 16:46:48 2022 From: steve at ethx.net (Steve Barham) Date: Fri, 10 Jun 2022 17:46:48 +0100 Subject: "With" for records Message-ID: <696FABE7-FEE6-4498-8298-1FC59D3CC044@ethx.net> > This should remind people of the *compact constructor* in a record; the > body is allowed to freely mutate the special variables (who also don't > have obvious declarations), and their terminal values determine the > state of the record. > > Just as we were able to do record patterns without having full-blown > deconstructors, we can do with expressions on records as well, because > (a) we still have a canonical ctor, (b) we have accessors, and (c) we > know the names of the components. ? > with the X and Y values of the operand. Their values are committed at > the end of the block using the canonical constructor. Is it correct to say the canonical constructor here? Would the compact constructor not be a better choice, as it gives implementors access to validate the new values to be committed? Cheers, Steve (apologies for the not replying to the thread; I subscribed to the list just recently, and don?t know that I can poke Mailman into sending me past messages so that I can reply to them) From brian.goetz at oracle.com Sat Jun 11 14:25:20 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Sat, 11 Jun 2022 10:25:20 -0400 Subject: "With" for records In-Reply-To: References: Message-ID: I got a private mail asking, basically: why not "just" generate withers for each component, so you could say `point.x(newX).y(newY)`. This would be a much weaker feature than is being proposed, in several dimensions. 1.? It doesn't scale to arbitrary classes; it's a record-specific hack.? Which means value classes are left out of the cold, as are immutable classes that can't be records or values for whatever reason.? The link I cited suggests how we're going to get to arbitrary classes; I wouldn't support this feature if we couldn't get there. 2.? It is strictly less powerful.? Say you have a record with an invariant that constraints multiple fields, such as: ??? record OddOrEvenPair(int a, int b) { ??????? OddOrEvenPair { ??????????? if (a % 2 != b % 2) ??????????????? throw new IllegalArgumentException(); ??????? } ??? } This requires that a and b both be even, or both be odd.? Note that there's no path from (2, 2) to (3, 3); any attempt to do `new OOEP(2, 2).a(3).b(3)` will fail when we try to reconstruct the intermediate state.? You need a wither that does both a and b at once.? (And we're not going to generate the 2^n combinations.) On 6/10/2022 8:44 AM, Brian Goetz wrote: > In > > https://github.com/openjdk/amber-docs/blob/master/eg-drafts/reconstruction-records-and-classes.md > > we explore a generalized mechanism for `with` expressions, such as: > > ??? Point shadowPos = shape.position() with { x = 0 } > > The document evaluates a general mechanism involving matched pairs of > constructors (or factories) and deconstruction patterns, which is > still several steps out, but we can take this step now with records, > because records have all the characteristics (significant names, > canonical ctor and dtor) that are needed.? The main reason we might > wait is if there are uncertainties in the broader target. > > Our C# friends have already gone here, in a way that fits into C#, > using properties (which makes sense, as their language is built on that): > > ??? object with { property-assignments } > > The C# interpretation is that the RHS of this expression is a sort of > "DSL", which permits property assignment but nothing else.? This is > cute, but I think we can do better. > > In our version, the RHS is an arbitrary block of Java code; you can > use loops, assignments, exceptions, etc.? The only thing that makes it > "special" is that that the components of the operand are lifted into > mutable locals on the RHS.? So inside the RHS when the operand is a > Point, there are fresh mutable locals `x` and `y` which are > initialized with the X and Y values of the operand.? Their values are > committed at the end of the block using the canonical constructor. > > This should remind people of the *compact constructor* in a record; > the body is allowed to freely mutate the special variables (who also > don't have obvious declarations), and their terminal values determine > the state of the record. > > Just as we were able to do record patterns without having full-blown > deconstructors, we can do with expressions on records as well, because > (a) we still have a canonical ctor, (b) we have accessors, and (c) we > know the names of the components. > > Obviously when we get value types, we'll want classes to be able to > expose (or not) such a mechanism (both for internal or external use). > > #### Digression: builders > > As a bonus, I think `with` offers us a better path to getting rid of > builders than the (problematic) one everyone asks for (default values > on constructor parameters.)? Consider the case of a record with many > components, all of which are optional: > > ??? record Config(int a, > ????????????????? int b, > ????????????????? int c, > ????????????????? ... > ????????????????? int z) { > ??? } > > Obviously, no one wants to call the canonical constructor with 26 > values.? The standard workaround is a builder, but that's a lot of > ceremony.? The `with` mechanism gives us a way out: > > ??? record Config(int a, > ????????????????? int b, > ????????????????? int c, > ????????????????? ... > ????????????????? int z) { > > ??????? private Config() { > ??????????? this(0, 0, 0, ... 0); > ??????? } > > ??????? public static Config BUILDER = new Config(); > ??? } > > Now we can just say > > ??? Config c = Config.BUILDER with { c = 3; q = 45; } > > The constant isn't even necessary; we can just open up the > constructor.? And if there are some required args, the constructor can > expose them too.? Suppose a and b are required, but c..z are > optional.? Then: > > ??? record Config(int a, > ????????????????? int b, > ????????????????? int c, > ????????????????? ... > ????????????????? int z) { > > ??????? public Config(int a, int b) { > ??????????? this(a, b, 0, ... 0); > ??????? } > ??? } > > ??? Config c = new Config(1, 2) with { c = 3; q = 45; } > > In this way, the record acts as its own builder. > > (As an added bonus, the default values do not suffer from the "brittle > constant" problem that a default value would likely suffer from, > because they are an implementation detail of the constructor, not an > exposed part of the API.) > > > I think it is reasonable at this point to take this idea off the shelf > and work towards delivering this for records, while we're building out > the machinery needed to deliver this for general classes.? It has no > remaining dependencies and is immediately useful for records. > > (As usual, please hold comments on small details until everyone has > had a chance to comment on the general direction.) > > > From brian.goetz at oracle.com Sat Jun 11 18:16:26 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Sat, 11 Jun 2022 14:16:26 -0400 Subject: "With" for records In-Reply-To: <98a292f4-b029-858f-1cfb-559ad9436802@oracle.com> References: <1564376667.5711009.1654871324376.JavaMail.zimbra@u-pem.fr> <98a292f4-b029-858f-1cfb-559ad9436802@oracle.com> Message-ID: <2bbd8953-6f71-a7a6-1d27-f14b3baf8cdf@oracle.com> We also probably want a rule to _prevent_ assignment to any locals *other than* the synthetic component locals.? Assigning to uplevel locals from within a `with` block seems like asking for trouble; the with block is like a transform on the component locals. However, we may need a story (hope not) for _accessing_ uplevel shadowed locals.? For example: ??? record R(A contents) { } ??? record A(B contents) { } ??? record B(int contents) { } ??? R r = ... ??? R rr = r with { contents = contents with { contents = 3 }} it is possible that the inner block might want additional information from one of the enclosing `contents` variables. On 6/10/2022 11:25 AM, Brian Goetz wrote: >> About the declaration of local variables, in Java, there is no >> hiding/shadowing between local variables, so a code like this is >> rejected ? Or do we introduce a special rule for the hiding of >> implicit variables ? > > Yes, we probably do need a special rule for this.? The component names > are fixed, and collisions are likely, so these synthetic variables > will probably have to be allowed to shadow other locals. From tanksherman27 at gmail.com Sun Jun 12 13:21:20 2022 From: tanksherman27 at gmail.com (Julian Waters) Date: Sun, 12 Jun 2022 21:21:20 +0800 Subject: "With" for records Message-ID: Since we're on the topic, I've been meaning to ask this for a while, but is there existing syntax we could re-use instead of introducing a new keyword? best regards, Julian From forax at univ-mlv.fr Sun Jun 12 16:21:06 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Sun, 12 Jun 2022 18:21:06 +0200 (CEST) Subject: "With" for records In-Reply-To: <2bbd8953-6f71-a7a6-1d27-f14b3baf8cdf@oracle.com> References: <1564376667.5711009.1654871324376.JavaMail.zimbra@u-pem.fr> <98a292f4-b029-858f-1cfb-559ad9436802@oracle.com> <2bbd8953-6f71-a7a6-1d27-f14b3baf8cdf@oracle.com> Message-ID: <334075228.6141785.1655050866800.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Saturday, June 11, 2022 8:16:26 PM > Subject: Re: "With" for records > We also probably want a rule to _prevent_ assignment to any locals *other than* > the synthetic component locals. Assigning to uplevel locals from within a > `with` block seems like asking for trouble; the with block is like a transform > on the component locals. perhaps any locals other that the synthetic ones and the ones declared inside the block. For example: record Couple(int first, int second) { Couple withMax() { return this with { var max = Math.max(first, second); // new local variable, it's ok first = max; second = max; }; } } > However, we may need a story (hope not) for _accessing_ uplevel shadowed locals. > For example: > record R(A contents) { } > record A(B contents) { } > record B(int contents) { } > R r = ... > R rr = r with { contents = contents with { contents = 3 }} > it is possible that the inner block might want additional information from one > of the enclosing `contents` variables. or inside the block we may want to have access to the parameters, like in: record Complex(double re, double im) { Complex withRe(double re) { return this with { re = re_from_outer_scope; } // find a syntax here ! } } we can introduce an intermediary local variable but i wonder if there is a better solution ? record Complex(double re, double im) { Complex withRe(double re) { var re_from_outer_scope = re; return this with { re = re_from_outer_scope; } } } and two other related questions about the syntax - do we allow to not use curly braces if there is only one assignment complex with re = 3 - or do we allow to avoid the last semicolon if there is only one assignment like in your example complex with { re = 3 } R?mi > On 6/10/2022 11:25 AM, Brian Goetz wrote: >>> About the declaration of local variables, in Java, there is no hiding/shadowing >>> between local variables, so a code like this is rejected ? Or do we introduce a >>> special rule for the hiding of implicit variables ? >> Yes, we probably do need a special rule for this. The component names are fixed, >> and collisions are likely, so these synthetic variables will probably have to >> be allowed to shadow other locals. From brian.goetz at oracle.com Sun Jun 12 16:43:33 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Sun, 12 Jun 2022 12:43:33 -0400 Subject: "With" for records In-Reply-To: <334075228.6141785.1655050866800.JavaMail.zimbra@u-pem.fr> References: <1564376667.5711009.1654871324376.JavaMail.zimbra@u-pem.fr> <98a292f4-b029-858f-1cfb-559ad9436802@oracle.com> <2bbd8953-6f71-a7a6-1d27-f14b3baf8cdf@oracle.com> <334075228.6141785.1655050866800.JavaMail.zimbra@u-pem.fr> Message-ID: <69674e17-1fcb-5232-aa09-eb9012221a64@oracle.com> On 6/12/2022 12:21 PM, forax at univ-mlv.fr wrote: > > > ------------------------------------------------------------------------ > > *From: *"Brian Goetz" > *To: *"Remi Forax" > *Cc: *"amber-spec-experts" > *Sent: *Saturday, June 11, 2022 8:16:26 PM > *Subject: *Re: "With" for records > > We also probably want a rule to _prevent_ assignment to any locals > *other than* the synthetic component locals.? Assigning to uplevel > locals from within a `with` block seems like asking for trouble; > the with block is like a transform on the component locals. > > > perhaps any locals other that the synthetic ones and the ones declared > inside the block. Yes, sorry that's exactly what I meant.? The reason for this rule is perhaps not obvious, but when we get to arbitrary ctor/dtor pairs, we will need to do _overload selection_ based on the names used in the block.? And it needs to be unambiguous which assignments in the block are intended to be to components.? For locals declared in the block, we can conclude that assignments to these are not component assignments. > > it is possible that the inner block might want additional > information from one of the enclosing `contents` variables. > > > or inside the block we may want to have access to the parameters, like in: > ? record Complex(double re, double im) { > ??? Complex withRe(double re) { > ????? return this with { re = re_from_outer_scope; }? // find a syntax > here ! > ??? } > ? } My hope is we do not have to find a syntax.? As you say, we can introduced an intermediate local: ??? int outerRe = re; ??? return this with { re = outerRe } This also is a good candidate for further refinement with our friend "let expressions": ??? return let int outeRe = re ??????? in this with { re = outerRe } > > and two other related questions about the syntax > - do we allow to not use curly braces if there is only one assignment > ??? complex with re = 3 > - or do we allow to avoid the last semicolon if there is only one > assignment like in your example > ??? complex with { re = 3 } Yes, these are reasonable special cases to consider (as we do with single-argument lambdas, or throw on the RHS of an arrow case.) From forax at univ-mlv.fr Tue Jun 14 12:22:40 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Tue, 14 Jun 2022 14:22:40 +0200 (CEST) Subject: with and binary backward compatibility Message-ID: <692878089.7365316.1655209360723.JavaMail.zimbra@u-pem.fr> Hi all, Let say we have a Point with 2 components record Point(int x, int y) { } Then we change the record to add a 3rd components in a more or less backward compatible way record Point(int x, int y, int z) { Point(int x, int y) { this(x, y, 0); // creation of the new value 0 } } Now, let say there is a 'with' somewhere in another code var newPoint = point with { x = 3; }; If this code is compiled when the record Point had only two components, so this is equivalent to Point(int x, int y) = point; // i.e. int x = point.x(); int y = point.y(); x = 3; var newPoint = new Point(x, y); The problem is that if we run that code with the new version of Point (the one with 3 components), newPoint.z is not equals to point.z but to 0, so once there is a 'with' somewhere, there is no backward compatibility anymore. We can try to restore the backward compatibility by compiling to a slightly different code using invokedynamic and a desugared method corresponding to the body of the 'with' var newPoint = invokedynamic (point) [desugared$method]; // equivalent to a call using with static Point desugared$method(int x, int y, MethodHandle mh) { // content of the body x = 3; return mh.invokeExact(x, y); } an at runtime, we generate a tree of method handles that does more or less stub(Point point) { return desugared$method(point.x(), point.y(), (a, b) -> new Point(a, b, point.z()) } because this code is generated at runtime, it will be always compatible with the latest version of Point. If we want to support this encoding, it means that the local variables of the enclosing method need to be effectively final so the body of with can be lifted to a private static method (exactly like a lambda). If we generalize this a bit, we can also use the same trick for the record pattern, in that case the pattern Point(int a, int b) is equivalent at runtime to Point(int a, int b, _) once the runtime found that the canonical deconstructor emits the values of 3 components. I'm not sure it's a path i want to follow because i would prefer the record pattern to match the shape excatly, but i find it more attractive than the idea to have overloaded deconstructors. regards, R?mi From heidinga at redhat.com Tue Jun 14 13:10:46 2022 From: heidinga at redhat.com (Dan Heidinga) Date: Tue, 14 Jun 2022 09:10:46 -0400 Subject: with and binary backward compatibility In-Reply-To: <692878089.7365316.1655209360723.JavaMail.zimbra@u-pem.fr> References: <692878089.7365316.1655209360723.JavaMail.zimbra@u-pem.fr> Message-ID: Remi, to restate your concern slightly - it sounds like there may be binary (and source?) compatibility concerns with the implementation of with'ers. You've pitched a possible implementation using invokedynamic (every compiler writer's favourite swiss army knife) but prefer something that is more explicit in the bytecode. I'm still working my way through the full reconstruction document but I assume compatibility and implementation have been given some thought even if they don't show up in that document. Let's aim for the high order bits first - figuring out if the feature is desirable and if the general direction works before deep diving into class file representation and binary compatibility. So I'd suggest putting a pin in this topic and circling back to it after further discussion of the core concept. Just my two cents =) --Dan On Tue, Jun 14, 2022 at 8:23 AM Remi Forax wrote: > > Hi all, > Let say we have a Point with 2 components > record Point(int x, int y) { } > > Then we change the record to add a 3rd components in a more or less backward compatible way > record Point(int x, int y, int z) { > Point(int x, int y) { > this(x, y, 0); // creation of the new value 0 > } > } > > Now, let say there is a 'with' somewhere in another code > > var newPoint = point with { x = 3; }; > > If this code is compiled when the record Point had only two components, so this is equivalent to > > Point(int x, int y) = point; // i.e. int x = point.x(); int y = point.y(); > x = 3; > var newPoint = new Point(x, y); > > The problem is that if we run that code with the new version of Point (the one with 3 components), > newPoint.z is not equals to point.z but to 0, so once there is a 'with' somewhere, there is no backward compatibility anymore. > > We can try to restore the backward compatibility by compiling to a slightly different code using invokedynamic and a desugared method corresponding to the body of the 'with' > > var newPoint = invokedynamic (point) [desugared$method]; // equivalent to a call using with > > static Point desugared$method(int x, int y, MethodHandle mh) { // content of the body > x = 3; > return mh.invokeExact(x, y); > } > > an at runtime, we generate a tree of method handles that does more or less > stub(Point point) { > return desugared$method(point.x(), point.y(), (a, b) -> new Point(a, b, point.z()) > } > > because this code is generated at runtime, it will be always compatible with the latest version of Point. > > If we want to support this encoding, it means that the local variables of the enclosing method need to be effectively final so the body of with can be lifted to a private static method (exactly like a lambda). > > > If we generalize this a bit, we can also use the same trick for the record pattern, in that case the pattern Point(int a, int b) is equivalent at runtime to Point(int a, int b, _) once the runtime found that the canonical deconstructor emits the values of 3 components. > I'm not sure it's a path i want to follow because i would prefer the record pattern to match the shape excatly, but i find it more attractive than the idea to have overloaded deconstructors. > > regards, > R?mi > From forax at univ-mlv.fr Tue Jun 14 14:57:50 2022 From: forax at univ-mlv.fr (forax at univ-mlv.fr) Date: Tue, 14 Jun 2022 16:57:50 +0200 (CEST) Subject: with and binary backward compatibility In-Reply-To: References: <692878089.7365316.1655209360723.JavaMail.zimbra@u-pem.fr> Message-ID: <103517625.7499841.1655218670329.JavaMail.zimbra@u-pem.fr> ----- Original Message ----- > From: "Dan Heidinga" > To: "Remi Forax" > Cc: "amber-spec-experts" > Sent: Tuesday, June 14, 2022 3:10:46 PM > Subject: Re: with and binary backward compatibility > Remi, to restate your concern slightly - it sounds like there may be > binary (and source?) compatibility concerns with the implementation of > with'ers. You've pitched a possible implementation using > invokedynamic (every compiler writer's favourite swiss army knife) but > prefer something that is more explicit in the bytecode. Yes, BTW there is a simple way to avoid those binary compatibility issues, restrict "with" to the nest or the package/module (like sealed). > I'm still > working my way through the full reconstruction document but I assume > compatibility and implementation have been given some thought even if > they don't show up in that document. The problem is that Brian believes that all constructors and deconstructors are dual, i feel this is the wrong model, it does not work that way, a constructor if not canonical creates values before calling the canonical constructor while all deconstructors all match the same way but some provide fewer bindings than the others. The binary compatibility story of "with" shows that you can not do a circle point -> bindings -> point using any constructors but the canonical constructor. > > Let's aim for the high order bits first - figuring out if the feature > is desirable and if the general direction works before deep diving > into class file representation and binary compatibility. agree > > So I'd suggest putting a pin in this topic and circling back to it > after further discussion of the core concept. > > Just my two cents =) > > --Dan R?mi > > On Tue, Jun 14, 2022 at 8:23 AM Remi Forax wrote: >> >> Hi all, >> Let say we have a Point with 2 components >> record Point(int x, int y) { } >> >> Then we change the record to add a 3rd components in a more or less backward >> compatible way >> record Point(int x, int y, int z) { >> Point(int x, int y) { >> this(x, y, 0); // creation of the new value 0 >> } >> } >> >> Now, let say there is a 'with' somewhere in another code >> >> var newPoint = point with { x = 3; }; >> >> If this code is compiled when the record Point had only two components, so this >> is equivalent to >> >> Point(int x, int y) = point; // i.e. int x = point.x(); int y = point.y(); >> x = 3; >> var newPoint = new Point(x, y); >> >> The problem is that if we run that code with the new version of Point (the one >> with 3 components), >> newPoint.z is not equals to point.z but to 0, so once there is a 'with' >> somewhere, there is no backward compatibility anymore. >> >> We can try to restore the backward compatibility by compiling to a slightly >> different code using invokedynamic and a desugared method corresponding to the >> body of the 'with' >> >> var newPoint = invokedynamic (point) [desugared$method]; // equivalent to a >> call using with >> >> static Point desugared$method(int x, int y, MethodHandle mh) { // content of >> the body >> x = 3; >> return mh.invokeExact(x, y); >> } >> >> an at runtime, we generate a tree of method handles that does more or less >> stub(Point point) { >> return desugared$method(point.x(), point.y(), (a, b) -> new Point(a, b, >> point.z()) >> } >> >> because this code is generated at runtime, it will be always compatible with the >> latest version of Point. >> >> If we want to support this encoding, it means that the local variables of the >> enclosing method need to be effectively final so the body of with can be lifted >> to a private static method (exactly like a lambda). >> >> >> If we generalize this a bit, we can also use the same trick for the record >> pattern, in that case the pattern Point(int a, int b) is equivalent at runtime >> to Point(int a, int b, _) once the runtime found that the canonical >> deconstructor emits the values of 3 components. >> I'm not sure it's a path i want to follow because i would prefer the record >> pattern to match the shape excatly, but i find it more attractive than the idea >> to have overloaded deconstructors. >> >> regards, >> R?mi From brian.goetz at oracle.com Tue Jun 14 17:32:38 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 14 Jun 2022 13:32:38 -0400 Subject: with and binary backward compatibility In-Reply-To: <692878089.7365316.1655209360723.JavaMail.zimbra@u-pem.fr> References: <692878089.7365316.1655209360723.JavaMail.zimbra@u-pem.fr> Message-ID: <0ff0f52c-bda4-8b58-2dbe-6f8ff66c4801@oracle.com> > The problem is that if we run that code with the new version of Point (the one with 3 components), > newPoint.z is not equals to point.z but to 0, so once there is a 'with' somewhere, there is no backward compatibility anymore. Yes, in that case, we experience something like "decapitation" of the z value.?? But note the same thing is true without the `with` mechanism! ??? // Old code ??? record Point(int x, int? y) { } ??? class Foo { ? ?? ?? Point increment(Point p) { ??? ? ?? ?? return new Point(p.x() + 1, p.y() + 1); ? ?? ?? } ??? } ??? // modify Point and recompile Point but not Foo ??? record Point(int x, int y, int z) { ??????? Point(int x, int y) { this(x,y,0); } ??? } ??? // new code ??? Point p = new Point(1,2,3); ??? System.out.println(foo.inc(p));? // Point[2,3,0] This has nothing to do with "with"; without with, we have the same "brittle" constructor/deconstructor selection.? With "with", we have the same, it's just not as obvious.?? Actually, it's better, because then its only a separate compilation artifact; if you recompile the with-using client, things are righted, whereas with the explicit ctor-and-accessor version, it is never righted.? So `with` is actually a compatibility improvement here, in that the brittle selection goes away after recompilation. From brian.goetz at oracle.com Fri Jun 24 19:56:30 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Fri, 24 Jun 2022 15:56:30 -0400 Subject: Fwd: Feedback on the current state of switch pattern matching and record patterns In-Reply-To: References: Message-ID: <4b5499d8-5594-ead9-26c2-6299420e2e87@oracle.com> Received on the -comments list. Summary: ?- Question regarding the asymmetry of true vs false boolean constants. ?- Questions regarding matching of nulls in switch (fundamental confusion: whether `case null, String s` is one thing or two) ?- DA treatment of assignments inside guards ?- questionable boxing in record patterns -------- Forwarded Message -------- Subject: Feedback on the current state of switch pattern matching and record patterns Date: Sun, 19 Jun 2022 03:30:24 +0000 From: Robbe Pincket To: amber-spec-comments at openjdk.org Hello amber project experts. I?ve been quite interested in recent java enhancement projects, and while checking out some of the details of both switch pattern matching and record patterns, I?ve noticed some things I want to address. Looking around, this seemed to be the latest version of the spec, so I?ll be basing my questions off of this: https://cr.openjdk.java.net/~gbierman/jep427%2B405/jep427%2B405-20220601/specs/patterns-switch-record-patterns-jls.html Switch pattern matching Switch syntax The syntax section shows that a |CaseElement|?can be |Pattern {Guard}|. This would imply that there can be multiple |when|?guards on a single pattern Constant boolean expressions in the guard expression The specifications mention that unlike with most guards, if the ?expression has the value true?, the dominance checking treats it just as if it were an unguarded pattern case. This raised two concerns: * What does ?has the value true? mean? In my eyes, that can mean quite a few things: o The boolean literal ?true? o Any constant expression whose value is true o Any boolean expression that always evaluates to true * This treatment has no equivalent with guards that are false. Considering guards that ?have the value true? as a branch that is always taken feels akin to how constant expressions are handled in while loops, whilst |while (false)|?always fails to compile as it results in the body to be unreachable. However, when the guard is false, the resultant behavior mirrors a traditional if, where it considers the body to still be reachable, even though they can never get executed. Null labels I wasn?t sure whether this was an issue at first, but after looking at the spec again and realizing I misinterpreted part of the spec, I?m very unsure about whether the compiler is technically following the spec or not; nonetheless, it?s very counterintuitive to me. (This section is a bit more ?ranty? than I?d like, but it?s getting late, and I keep discovering new weird unexpected cases while typing) Let?s look at some examples. // example 1|| switch(|o|)||{|| ||case||null,||String|s when s|.length()||==||0||->||{|| ||System.|out|.println("o: "||+|o|);|| ||}|| ||...|| }|| I was very surprised when this gave me a npe when getting the length, as I assumed that the guard would only be evaluated when the pattern matched, and didn?t expect the null to fall through to the guard. So I tested the following: // example 2|| switch(|o|)||{|| ||case||String|s when s|.length()||==||0,||null||->||{|| ||System.|out|.println("o: "||+|o|);|| ||}|| ||...|| }|| This still throws an npe. This where I decided to check the spec in depth again. If the resulting transformed case label has both a null case element and a pattern case element p where p is a pattern declaring a pattern variable x of type U, then p is replaced with an any pattern that declares x of type U (14.30.1). This explains the second case, even though the |null|?label appears after. Let?s use |String? s|?as a notation for an any pattern. The way I see the translation of the second example is like this: switch(|o|)||{|| ||case||String?|s when s|.length()||==||0:|| ||case||null:||{|| ||System.|out|.println("o: "||+|o|);|| ||break;|| ||}|| ||...|| }|| I?m keeping the |case null|?here cause the spec doesn?t mention this getting removed, which seems like a mistake to me cause it implies that the first example gets translated as this: switch(|o|)||{|| ||case||null:|| ||case||String?|s when s|.length()||==||0:|| ||{|| ||System.|out|.println("o: "||+|o|);|| ||break;|| ||}|| ||...|| }|| where |case null|?would match null first and therefore |String? s|?wouldn?t match the null. These 2 translations bring up the question: what if there are 2 case labels, one of which is null? // Example 3|| switch(|o|)||{|| ||case||String|s when s|.length()||==||0:|| ||case||null:||{|| ||System.|out|.println("o: "||+|o|);|| ||break;|| ||}|| ||...|| }|| Example 3 still throws an NPE when given null. This, just blew my mind, and I think this behavior is even wrong according to the spec. The spec mentions that if a *case label*?contains both a null case label element and a pattern case element, the pattern gets converted to a any pattern. However in this example there are 2 separate *case labels*?(but only one switch label). Note that adding a single |;| after the first |:|?results in a variant that does work as expected, most likely cause they are now 2 separate *switch labels*. One last example: static||void||example4(Object|a|,||Object|b|)||{|| ||switch(|a|)||{|| ||case||null,||String|s when |Objects.equal(|s|,|b|)||->||{|| ||System.|out|.println("a & b: "||+|s|);|| ||}|| ||case||null||->||{|| ||System.|out|.println("a is NULL, but b isn't");|| ||}|| ||default||->||{|| ||System.|out|.println("Objects: "||+|a |+||", "||+|b|);|| ||}|| ||}|| }|| This fails to compile, citing a ?duplicate case label?, however I can?t find anything in the spec that disallows 2 null cases in the same switch, even without the guard. The closest thing I see is: A switch label that supports a case constant dominates another switch label supporting the same case constant. |null|, however, is not considered a case constant - as the guard is acting on the null case, it means the second null case shouldn?t be dominated by the first one. Perhpas should be a rule that doesn?t allow this, but nothing in the current spec should disallow this, even though javac does, in fact, disallow it. Definite assignment It seems the part on definite assignment doesn?t take into account the possible assignments in the guard of switch cases. I thought this was gonna be a small thing to fix as javac seemed to behave as expected despite the specification, but then I noticed something else and ended up writing a some code where I was able to reassign a final var. Issue 1: * V is [un]assigned before the switch rule expression, switch rule block, or switch rule throw statement introduced by a switch rule in the switch block iff V is [un]assigned after the selector expression of the switch statement. * V is [un]assigned before the first block statement of a switch labeled statement group in the switch block iff both of the following are true: o V is [un]assigned after the selector expression of the switch statement. o ? If the case is guarded, this shouldn?t be checking the assignment status after the selector expression, but after the guard expression when true. It seems javac already does this. Issue 2: The naive assumption, which javac seems to be using, is that a variable V is [un]assigned before a guard expression iff it is [un]assigned after the selector expression. This however is not correct. See the following snippet: public||class|test |{|| ||static||void||test(Object|o|)||{|| ||final||String|res|;|| ||switch(|o|)||{|| ||case||String|s when |(|res |=|s|)||!=||null||&&||log(|res|)||&&|s|.length()|| >||3||->||{|| ||System.|out|.println("res: "||+|res|);|| ||}|| ||case||String|i when |(|res |=||"???")||!=||null||&&||log(|res|)||->||{|| ||System.|out|.println("res: "||+|res|);|| ||}|| ||default||->||{|| ||System.|out|.println("Object: "||+|o|);|| ||}|| ||}|| ||}|| || ||static||boolean||log(String|res|)||{|| ||System.|out|.println("res: "||+|res|);|| ||return||true;|| ||}|| || ||public||static||void||main(String[]|args|){|| ||System.|out|.println("Trial 1: ");|| ||test("Hello");|| ||System.|out|.println("Trial 2: ");|| ||test("Huh");|| ||}|| }|| Which outputs: |Trial 1:| |res: Hello| |res: Hello| |Trial 2:| |res: Huh| |res: ???| |res: ???| This code compiles, even though it is reassigning a final var. A correct definition for definite (un)assignment in this case would have to take all the pattern cases that could have matched before it into account, and the maybe also the fact that (some of) those cases could cover all possible pathways to this label, like: case||A|a when |(|res |=||"Hi")||==||null||->||{}|| case||B|b when |(|res |=||"Hello")||==||null||->||{}|| case||SealedAOrB||->||{|| ||// res is now definitely assigned?|| }|| Record Patterns Integer components. It seems that when accessing |int|?fields, it boxes them, to then immediately unbox them again. Example switch(|o|)||{|| ||case||Point(var|x|,||var|y|)||->||System.|out|.println(|x |+|y|);|| ||...|| compiles into something like: |var1 |=|o| switch(|indy typeswitch|)||{|| ||case||0||->||{|| ||int|var5 |=|$proxy$|x((Point)|var1|);|| ||int|var3 |=||Integer.valueOf(|var5|).intValue();|| |?? ?????var5 |=|$proxy$|y((Point)|var1|);|| ||int|var4 |=||Integer.valueOf(|var5|).intValue();|| || ||System.|out|.println(|var3 |+|var4|)|| ||}|| I gave a quick look to the code in |TransPattern|, but couldn?t actually find anything about it. Seems like something that gets triggered by accident? Hope all of this helps with improving the language! Greetings Robbe Pincket -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Jun 27 08:54:14 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Mon, 27 Jun 2022 10:54:14 +0200 (CEST) Subject: Bug in the record pattern translation / record pattern spec Message-ID: <54047224.5937423.1656320054130.JavaMail.zimbra@u-pem.fr> Hi all, i've already reported that issue but it was theoretical because the support of the record pattern was not integrated into the jdk 19 at that time and i was not able to explain int clearly. Now that the support is available, this code should throw a StackOverflow error but it creates a linked list of MatchException which makes it hard to debug. Wrapping all exceptions into a runtime exception is not a good idea when you have recursive calls which is a kind of natural when you have pattern matching, for me it's a spec issue but i want to be sure. BTW, the bug here lies in the fact that deconstructing Cons(int value, int size, RecChain next) calls the accessor size() which itself calls RecChain::size which is a nice puzzler by itself. public sealed interface RecChain { default int size() { return switch (this) { case Nil __ -> 0; case Cons(int value, int size, RecChain next) -> 1 + next.size(); }; } record Nil() implements RecChain { } record Cons(int value, int size, RecChain next) implements RecChain { @Override public int size() { return size != -1? size: RecChain.super.size(); } } public static void main(String[] args){ RecChain chain = new Nil(); for (var i = 0; i < 100; i++) { chain = new Cons(i, -1, chain); } System.out.println(chain.size()); } } regards, R?mi From brian.goetz at oracle.com Mon Jun 27 19:38:00 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Mon, 27 Jun 2022 15:38:00 -0400 Subject: Bug in the record pattern translation / record pattern spec In-Reply-To: <54047224.5937423.1656320054130.JavaMail.zimbra@u-pem.fr> References: <54047224.5937423.1656320054130.JavaMail.zimbra@u-pem.fr> Message-ID: [ dropping amber-dev ] Thanks for this concrete example, its helpful. So far, we can put this in the category of "there exists at least one case where this handling of exceptions makes it harder to understand what is going on."? This isn't necessarily dispositive, in that in order to make a fair determination, we'd have to consider both the frequency (this is kind of a corner case) and also the complement: the cases where it helps. Let me explain (again) why we went this way.? We're doing record patterns *now*, but it is entirely clear where this is going: record patterns are not special, records will eventually acquire an implicit canonical deconstruction pattern, and users will be able to explicitly declare deconstruction patterns as well.? And (barring separate compilation anomalies), the two should not really be distinguishable; whether `javac` generates a synthetic member for a dtor pattern, or whether it simulates the behavior of that synthetic member on the client side, should be an implementation detail.? So accessors are not "special" here. Having a deconstruction pattern throw is a logic error, of the same sort that causes NPE or AIOOBE.? Pattern matching may, as part of doing its thing, invoke the imperative logic associated with a deconstruction pattern; it expects these not to fail. Telling users "pattern matching might fail with an arbitrary error" only encourages users to try and catch these errors, which is misguided and which might well catch up other unrelated errors in the same net.? "There was a violated invariant in a pattern-driven construct" is a reasonable error model to present to users when we are invoking unspecific behavior at unspecific times on the user's behalf. Obviously, the code in this example is broken; what you're hoping for is a clean SOE (if it were wrapped once with ME, that would be fine), so users can say "oops, I'm a dummy" and fix it. On 6/27/2022 4:54 AM, Remi Forax wrote: > Hi all, > i've already reported that issue but it was theoretical because the support of the record pattern was not integrated into the jdk 19 at that time and i was not able to explain int clearly. Now that the support is available, this code should throw a StackOverflow error but it creates a linked list of MatchException which makes it hard to debug. > > Wrapping all exceptions into a runtime exception is not a good idea when you have recursive calls which is a kind of natural when you have pattern matching, for me it's a spec issue but i want to be sure. > > BTW, the bug here lies in the fact that deconstructing Cons(int value, int size, RecChain next) calls the accessor size() which itself calls RecChain::size which is a nice puzzler by itself. > > public sealed interface RecChain { > default int size() { > return switch (this) { > case Nil __ -> 0; > case Cons(int value, int size, RecChain next) -> 1 + next.size(); > }; > } > > record Nil() implements RecChain { } > > record Cons(int value, int size, RecChain next) implements RecChain { > @Override > public int size() { > return size != -1? size: RecChain.super.size(); > } > } > > public static void main(String[] args){ > RecChain chain = new Nil(); > for (var i = 0; i < 100; i++) { > chain = new Cons(i, -1, chain); > } > System.out.println(chain.size()); > } > } > > regards, > R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From forax at univ-mlv.fr Mon Jun 27 20:23:58 2022 From: forax at univ-mlv.fr (Remi Forax) Date: Mon, 27 Jun 2022 22:23:58 +0200 (CEST) Subject: Bug in the record pattern translation / record pattern spec In-Reply-To: References: <54047224.5937423.1656320054130.JavaMail.zimbra@u-pem.fr> Message-ID: <661414746.6425006.1656361438392.JavaMail.zimbra@u-pem.fr> > From: "Brian Goetz" > To: "amber-spec-experts" > Sent: Monday, June 27, 2022 9:38:00 PM > Subject: Re: Bug in the record pattern translation / record pattern spec > [ dropping amber-dev ] > Thanks for this concrete example, its helpful. > So far, we can put this in the category of "there exists at least one case where > this handling of exceptions makes it harder to understand what is going on." > This isn't necessarily dispositive, in that in order to make a fair > determination, we'd have to consider both the frequency (this is kind of a > corner case) and also the complement: the cases where it helps. > Let me explain (again) why we went this way. We're doing record patterns *now*, > but it is entirely clear where this is going: record patterns are not special, > records will eventually acquire an implicit canonical deconstruction pattern, > and users will be able to explicitly declare deconstruction patterns as well. > And (barring separate compilation anomalies), the two should not really be > distinguishable; whether `javac` generates a synthetic member for a dtor > pattern, or whether it simulates the behavior of that synthetic member on the > client side, should be an implementation detail. So accessors are not "special" > here. > Having a deconstruction pattern throw is a logic error, of the same sort that > causes NPE or AIOOBE. Pattern matching may, as part of doing its thing, invoke > the imperative logic associated with a deconstruction pattern; it expects these > not to fail. Telling users "pattern matching might fail with an arbitrary > error" only encourages users to try and catch these errors, which is misguided > and which might well catch up other unrelated errors in the same net. "There > was a violated invariant in a pattern-driven construct" is a reasonable error > model to present to users when we are invoking unspecific behavior at > unspecific times on the user's behalf. yes, that's why i said that for me the issue is closer to an implementation problem, the idea is fine but the current implementation is not. We can notice that the static block has exactly the same issue, in both case you do not want users to try to recover if an exception occurs. Compared to the semantics of a static block, the key difference is that a static block throws an Error not a runtime exception. There are two problems with using a runtime exception, users may still think it's Ok to catch it and if a getter/deconstructor throw an Error like OutOfMemoryError/StackOverflowError, wrapping it into a runtime exception change the behavior of the others try/catchs that are present below in the stack. For the deconstruction pattern, i propose to throw a specific error and to not wrap the subclasses of java.lang.Error thrown by the deconstruction pattern. > Obviously, the code in this example is broken; what you're hoping for is a clean > SOE (if it were wrapped once with ME, that would be fine), so users can say > "oops, I'm a dummy" and fix it. yes ! R?mi > On 6/27/2022 4:54 AM, Remi Forax wrote: >> Hi all, >> i've already reported that issue but it was theoretical because the support of >> the record pattern was not integrated into the jdk 19 at that time and i was >> not able to explain int clearly. Now that the support is available, this code >> should throw a StackOverflow error but it creates a linked list of >> MatchException which makes it hard to debug. >> Wrapping all exceptions into a runtime exception is not a good idea when you >> have recursive calls which is a kind of natural when you have pattern matching, >> for me it's a spec issue but i want to be sure. >> BTW, the bug here lies in the fact that deconstructing Cons(int value, int size, >> RecChain next) calls the accessor size() which itself calls RecChain::size >> which is a nice puzzler by itself. >> public sealed interface RecChain { >> default int size() { >> return switch (this) { >> case Nil __ -> 0; >> case Cons(int value, int size, RecChain next) -> 1 + next.size(); >> }; >> } >> record Nil() implements RecChain { } >> record Cons(int value, int size, RecChain next) implements RecChain { >> @Override >> public int size() { >> return size != -1? size: RecChain.super.size(); >> } >> } >> public static void main(String[] args){ >> RecChain chain = new Nil(); >> for (var i = 0; i < 100; i++) { >> chain = new Cons(i, -1, chain); >> } >> System.out.println(chain.size()); >> } >> } >> regards, >> R?mi -------------- next part -------------- An HTML attachment was scrubbed... URL: From brian.goetz at oracle.com Tue Jun 28 17:38:30 2022 From: brian.goetz at oracle.com (Brian Goetz) Date: Tue, 28 Jun 2022 13:38:30 -0400 Subject: Bug in the record pattern translation / record pattern spec In-Reply-To: <661414746.6425006.1656361438392.JavaMail.zimbra@u-pem.fr> References: <54047224.5937423.1656320054130.JavaMail.zimbra@u-pem.fr> <661414746.6425006.1656361438392.JavaMail.zimbra@u-pem.fr> Message-ID: <609ed077-e26a-0508-55c1-15ed4028a163@oracle.com> > > For the deconstruction pattern, i propose to throw a specific error > and to not wrap the subclasses of java.lang.Error thrown by the > deconstruction pattern. The wrapper should not be an Error, but I see what you are getting at -- let Error through, wrap everything else.? This would allow SOE and OOME to manifest as expected -- and rely on the "well, know its a bad idea to try and catch Error, but they are not as good about runtime exceptions."? Worth considering. -------------- next part -------------- An HTML attachment was scrubbed... URL: From alex.buckley at oracle.com Tue Jun 28 17:54:26 2022 From: alex.buckley at oracle.com (Alex Buckley) Date: Tue, 28 Jun 2022 10:54:26 -0700 Subject: Bug in the record pattern translation / record pattern spec In-Reply-To: <609ed077-e26a-0508-55c1-15ed4028a163@oracle.com> References: <54047224.5937423.1656320054130.JavaMail.zimbra@u-pem.fr> <661414746.6425006.1656361438392.JavaMail.zimbra@u-pem.fr> <609ed077-e26a-0508-55c1-15ed4028a163@oracle.com> Message-ID: <4fd7d55c-c7ab-4549-071d-cf1210443254@oracle.com> On 6/28/2022 10:38 AM, Brian Goetz wrote: >> For the deconstruction pattern, i propose to throw a specific error >> and to not wrap the subclasses of java.lang.Error thrown by the >> deconstruction pattern. > > The wrapper should not be an Error, but I see what you are getting at -- > let Error through, wrap everything else.? This would allow SOE and OOME > to manifest as expected -- and rely on the "well, know its a bad idea to > try and catch Error, but they are not as good about runtime > exceptions."? Worth considering. And of course this is already the policy for errors during bootstrap method invocation: -----JVMS 5.4.3.6----- If the invocation fails by throwing an instance of Error or a subclass of Error, resolution fails with that exception. If the invocation fails by throwing an exception that is not an instance of Error or a subclass of Error, resolution fails with a BootstrapMethodError whose cause is the thrown exception. ---------------------- Alex