Java: "+=" applied to String operands can provoke side effects
bugs.openjdk.java.net
bugs.openjdk.java.net
For the unfamiliar, the way string concatenation is compiled changed with invokedynamic.
Previously, it would compile to either Sting.concat calls or transform into a StringBuilder, but with invoke dynamic we can leave it to the runtime to optimize when it first runs.
I'm not surprised. Java 9 has minor adoption, and having side effects on the left hand side of an assignment operation is really really bad practice.
So you'd need a codebase that (1) was updated or written in a very new Java version, (2) contained the smelly code and (3) had a harmful double side-effect that got noticed. That's a pretty rare combination.
No it isn't. Something like this:
data[index++] += 'a'
Would easily pass any reasonable code review. It's not that smelly or unusual.
It might be unusual with strings, which makes this less common, but it's not an unusual format at all. It's a non-trivial part of the reason ++ operator exists in the first place.
I wouldn't pass that in a code review without comment!
I'd ask the author to consider breaking down the two actions into two separate lines so that it's more clear that there are two actions and what the sequential ordering of them is.
Inline side effects like that are a mess. Without looking it up, I could not even tell you if the sequence point ordering of that kind of expression in general is defined in C!
What are you calling "inline side effects" here? The ++ operator or the += operator? Because the index-of a ++ operator is extremely well defined.
In fact that code snippet works in every language with a ++ operator exactly as you'd expect, there's really no tricks in play here. Drop that code into C, C++, Java, JavaScript, etc... and it does the exact same thing in all of them via well-defined rules. It will append 'a' to data[index] then increment index.
We're not even into the wierd land of mixing bitwise & boolean operators on the same line where you operator precedence becomes non-obvious.
Of course, you can always do stupid stuff to make life difficult. In C++
int data[100];
memset(data, 0, sizeof(data));
int &index = data[0];
data[index++] = 'a';
Which cell is the ++ applied to, data[0] or data[97]? data[index++] += 'a';
All that matter is what the ++ operator evaluates to - the sequencing of the side-effect modification to 'index' is irrelevant. The ++ operator evaluates to the value of 'index' before the increment is applied, so we know that the is array location which will be modified.As it happens, in C the modifications to the location in the 'data' array and the 'index' variable are not sequenced relative to each other, but that is not observable.
If you never ever worked with Java and coded in C, then it might be different. Typical Java programmer, even weak one, is fluent in Java 101, but does not really know what might confuse C programmer. In any case, I would expect C programmer finding job as Java programmer for some reason to simply ask someone in office.
I can't recall perfectly from the top of my head the rules for the ordering of inline side effects in general in C and Java, and I work in the VM research group at Oracle. Can you? Are you sure about that?
Edit:
You’ve completely changed your comment out from underneath me after I replied. That’s rude.
To answer your new question - you're not alone in not know what I mean by sequencing of side effects! Not many people understand the rules for sequencing side effects! That's my point it's subtle. The JVMLS gives you the rules, but it also says
> It is recommended that code not rely crucially on this specification. Code is usually clearer when each expression contains at most one side effect, as its outermost operation, and when code does not depend on exactly which exception arises as a consequence of the left-to-right evaluation of expressions.
I'm just following that advice!
For C it's worse - what order are two side effects run in a C expression? Usually the rule is 'it's up to the compiler - it can do them in any order it wants.'
What you need here is just knowing what "index++" means, nothing about sequencing at all.
I could understand your objection with the original version of the code, where it referred to both "i++" and "i" in a single line. That depends on sequencing rules and is a bad idea to allow.
But this version, that only refers to "index++" once, doesn't have any subtleties.
How do you explain your rule to a junior? You're going to have to tell them that inline side effects are fine, as long as you follow another set of rules about sequencing - a set of rules you admit most people don't know at all and fewer people understand correctly!
You don't need to know the sequencing at all. It doesn't matter.
My rule would be "If you side-effect a variable, don't use it anywhere else in the same line". I think that's plenty simple.
Edit: Another way of wording my rule would be "if you have to care about sequencing, don't do it"
Also, if the junior cant comprehend reasoning, he/she will have hard time with expressions like "inline side effects". The word inline is not much used in Java world.
If there is style guide, then it is different matter and the argument is about usefulness of style guide itself (e.g. you follow the rule unless rule harms, even if the rule is overly broad). Absent style guide, imposing your personal preferences on good lines, because entirely different line would be unreadable is not something I can support. I used to follow such instructions, but don't anymore because such blind follow tactic lead to all kinds of problems for me.
When I worked with juniors, I wanted them to understand so that they can make own decisions as soon as possible. I did not wanted them to memorize hundreds of random rules.
The ++ operator is not obscure at all and used more often then for example += operator.
I shy away from inline side effects because in general I do not intuitively understand how they sequence. I'd rather just avoid in general.
15.7, Evaluation Order:
> It is recommended that code not rely crucially on this specification. Code is usually clearer when each expression contains at most one side effect, as its outermost operation, and when code does not depend on exactly which exception arises as a consequence of the left-to-right evaluation of expressions.
So when I review I advise, as per the spec, that expressions contain a single side effect.
If you think brevity means clarity, try reading some old Fortran code with comments stripped out and single-letter variable names like i, j, k, b, g, h, m, p, q, etc. (I say Fortran because in that era, such variable names were much more in vogue to save a couple of bytes here and there - but really, this can be done in any language).
Even simple algorithms (like finding the center of the circle through three given points) become puzzles. But sure, they are brief.
[1]https://signalvnoise.com/posts/3250-clarity-over-brevity-in-...
In this case, though, the OP makes a strong point that constructs like data[index++] = value; aren't idiomatic to Java - even the spec advises against this.
And in any case, side effects in assignments can be a tricky thing to understand. Sometimes they are necessary, as in
status = perform_computation_on_data(some_mutable_data);
But in the example discussed is not in this vein. I'd say it's not pretty for the same reason as cramming two statements on one line: data[i]=value; i++;
is not pretty: it's trying to do two things with one line, which kind of defeats the point of breaking up code into lines.So I don't disagree with you - but there's a happy medium between Fizz Buzz Enterprise and Code Golf - style code :)
if(haveitem) buf[i++] = item;
and if(haveitem) line[i++%nline] += sprintf(" %8i",item);
seems like an obvious way to evenly distribute column-grouped data.data[i++] += other[j++]
Yeah you could do this other ways, but it's not actually uncommon. You'll see this sort of thing in collection implementations regularly, for example, or things doing parsing. Not necessarily with the append operator as well, but certainly indexing into an array with a ++ variable.
And I have some dislike for "clever" code. I trade keystrokes for readability every day.
(Unless, as we can see, someone doesn't know what they are doing in compiler maintenance, but I mean besides that.)
data[ index ++ ] = data[ index ++ ] + “a”;
But that can’t be the right expansion.Ugh. Today’s code sample on Sesame Street, brought to you by the letters “F” & “U”, and the number 2. Why would you subject somebody to something that edgy?
int temp = index ++;
data[ temp ] = data[ temp ] + "a";
But the "FP wanna be" guy in me just hates that kind of C inspired foolishness.[0] Allegedly the fix was available in a non-public build of 8u162
You haven't heard of the bug in binary search that existed for nine years then?
https://ai.googleblog.com/2006/06/extra-extra-read-all-about...
Even if it isn't LTS, does it make sense to.stay on the 8, which will be end of life soon anyway?
At least, programming using modules makes the codebase ready for Java 11.
The only thing that I don't like is the removal of javapackager without substituting it (jlink cannot produce bundles with applications, only a light redistributed image)
"+=" is [usually explained as] shorthand for repeating the left side. Why should
> foo[i++] += 1
evaluate differently than
> foo[i++] = foo[i++] + 1
?
The way it currently works is the way I would expect it to work, but I'm evidently in the minority here. I also don't write Java and since this is a change from Java 8, it needs to be fixed.
Let me state that again: yes, of course this bug needs to be fixed.
The spec is how it should work, regardless of developer's opinions.
> Why should [a] evaluate differently than [b]?
In my opinion (and I'm guessing in the opinion of the spec authors, though I can't be sure), saying "x += y" means "add y to x", so I'd expect the language to:
1) Figure out what "x" is once
2) Add "y" to it
Your opinion seems to be that saying "x += y" means "desugar to x = x + y" and you expect the language to: 1) Figure out what "x" is as a value
2) Add "y" to the value of "x"
3) Figure out what "x" is as a location (which may differ from 1)
4) Store the result into "x"
To me step 3 is very surprising, that "x" may differ, which is why I think the spec's definition is the more intuitive one (plus, it doesn't require knowledge of "desugaring" things).Rather than a redundancy and sugar on + and = ("I do not want to type the left side twice"), it is a distinction I am making to the compiler ("I do not want you to evaluate the left side twice").
The title [Edit: since changed] is a little bit fear-monger-y. It's only broken when the left hand side is not safe to evaluate more than once.
It can be prevented with a compile-time flag.
Java 9 broke other things. Here is one: https://github.com/andy-goryachev/FxDock/issues/8 WebView is working fine in 8, totally broken in 9, partially broken in 10. Do they have a QA department?
That said, javafx is a bit of an orphan compared the rest of the JDK. And that's why it's getting pushed out.
What a dramatic change over what seems to be a solvable memory leak. I only toyed with JavaFX but at least it's webview is dead to me.
and another thing - how does one submit a defect report to Oracle? neither bugs.openjdk.java.net nor bugs.java.com have a clear link, and I can't find where one can register an account.
The ease of avoidance is unrelated to whether it's broken behavior or not.
array[i++] += i ... array[i++] += "a"
is just is broken. So is appending to array[foo()] or foo().fieldAlso: it only breaks when it's an array of Strings.
The brakes on my car aren't broken most of the time, it's only when I push down on the pedal!
Unfortunately, the `++` / `--` made it to Java, and are still used sometimes, despite JVM not being PDP-11, and definitely not trying to conserve every precious byte of RAM.
But importantly your original point about "depending on order of their evaluation" is wrong. Most of the examples don't care about order at all. They break because things happen the wrong number of times.
Funnily the chapter starts with the following caveat:
It is recommended that code not rely crucially on this specification. Code is usually clearer when each expression contains at most one side effect, as its outermost operation, and when code does not depend on exactly which exception arises as a consequence of the left-to-right evaluation of expressions.
I didn't know the Java compiler converts this automatically now. Since which version is this? I suspect there might be still edge cases where it doesn't work.
The bug happens with a function call on the LHS too.
> nor by allowing assignment operators (including augmented assignment ops) to evaluate to the value of their RHS
I'm confused. The assignment is happening last, and whatever it evaluates to is being discarded. How would changing what it evaluates to do anything?
But more importantly, the comment was about implementing Java itself in Rust, not the broken function, and that is why it wouldn't change anything.
See: substrateVM, graal
[2]> (macroexpand '(incf (aref a (incf i)) x))
(LET*
((#:G3214 A) (#:G3215 (INCF I)) (#:G3217 X)
(#:G3216 (+ (AREF #:G3214 #:G3215) #:G3217)))
(SYSTEM::STORE #:G3214 #:G3215 #:G3216)) ;
T
You can't just blindly expand the += by copying the expressions/AST node on the left. Or in general; you don't take expressions in the program and blindly duplicate them. How can someone working on compilers not know that???This bug is a little narrower than this makes it out to be, it ONLY fails when the array on the left is a String[ ]. The way String operations get compiled down to bytecode changed a lot around those releases so the bug is more like an unforeseen interaction between optimization passes than a naive MST/Macro implementation.
The "funny" thing here to me is the surprising gap in the regression testing given the quality of most of the JDK. go figure
On the other hand this explanation is stupid, as a programmer can implement `add()` in a wrong way.
A side effect would be like submitting an order for am Amazon Echo by mistake.
This is a bug, a serious bug, that leads to variables being assigned the wrong value.