[BREAKING] Use proper SAR for signed right shifts and emulate on pre-constantinople.#4084
[BREAKING] Use proper SAR for signed right shifts and emulate on pre-constantinople.#4084
Conversation
Changelog.md
Outdated
| ### 0.5.0 (unreleased) | ||
|
|
||
| Features: | ||
| * General: Signed right shift uses proper arithmetic shift, i.e. rounding towards negative infinity. |
There was a problem hiding this comment.
For 0.4.0 we had a separate section with breaking changes, I think we should do that again.
| if (c_valueSigned) | ||
| { | ||
| // stack: value_to_shift shift_amount | ||
| m_context << u256(0) << Instruction::DUP3 << Instruction::SLT; |
There was a problem hiding this comment.
Can you use inline assembly for this?
There was a problem hiding this comment.
Actually, this can be done as a pragma 050, should we do that?
There was a problem hiding this comment.
Ok - I didn't use inline assembly so far, because this way it's clearer to me that the amount of overhead introduced is not too excessive. But I can change it now.
Regarding a pragma 050, I'm not sure - this change does not only change some warnings to errors and it isn't merely a breaking syntactic change, but it will actually semantically change existing code. Did we do that before with a pragma 050? If so I can do it here as well - otherwise it may make sense to treat this one differently.
There was a problem hiding this comment.
The overhead in generated code should not be too large and the optimizer should reduce it to exactly the same code, but please test that. The main reason why I want to push inline assembly is because it makes the transition to webassembly easier.
We can discuss on wednesday whether we want 050 for that.
There was a problem hiding this comment.
Not sure whether people are more likely to read Changelogs for 0.5.0 than for earlier releases - the problem with this change is that it changes semantics, but is absolutely silent. And I could actually imagine people using signed right shifts, while expecting to end up at zero at some point... An option would be to instead introduce a temporary warning about all signed right shifts before 0.5.0 (and maybe even for pragma 050 and only remove it with actual 0.5.0) - but a warning might be too strong for that... What do you think?
There was a problem hiding this comment.
Ah OK - sorry, I missed your last comment.
chriseth
left a comment
There was a problem hiding this comment.
Please change it to using inline assembly.
f874af2 to
dd0205a
Compare
|
@chriseth |
|
Note: still to be discussed: should this be a pragma 050 change or not? |
| { | ||
| if (c_valueSigned) | ||
| m_context.appendInlineAssembly(R"({ | ||
| let xor_mask := sub(0,slt(value_to_shift,0)) |
There was a problem hiding this comment.
Please add a space after each comma :)
There was a problem hiding this comment.
Can we assume the value to be clean, or do we have to call signextend before the check? Since we previously only used SHR, it should be fine, I think.
There was a problem hiding this comment.
I think it should be fine, but I can try to find some test cases for that and/or read through the code to see whether the assumption is valid. I think I'll also add a few more test cases for more odd and even, signed and unsigned values of different bit widths in general.
There was a problem hiding this comment.
Added some test cases that verify that signextend is not needed.
| } | ||
| )"; | ||
| compileAndRun(sourceCode, 0, "C"); | ||
| ABI_CHECK(callContractFunction("f(int16,int16)", u256(-4266), u256(0)), encodeArgs(u256(-4266))); |
There was a problem hiding this comment.
Would it be better to use hex here?
There was a problem hiding this comment.
boost::multiprecision's doc states signed to unsigned conversions extract the last MaxBits bits of the 2's complement representation of the input value, so negative decimals are fine. To me hex values with a negative sign look weird and using the 2's complement might be harder to understand (especially with rounding towards negative infinity).
The existing tests also already used a mixture of hex and dec values, so if we were to agree that hex is better, then we should probably change it everywhere.
d747897 to
ea37ddc
Compare
|
@ekpyron do you want to pull out the test cases? Those can be merged now (with the current values). |
|
Note: Needs to be rebased once the tests in #4102 are merged into develop and develop is merged into 050. |
|
Rebased. |
Changelog.md
Outdated
| ### 0.5.0 (unreleased) | ||
|
|
||
| Breaking Changes: | ||
| * General: Signed right shift uses proper arithmetic shift, i.e. rounding towards negative infinity. |
There was a problem hiding this comment.
Can you please better highlight that this is a semantics change that might go unnoticed?
| sign extends. Shifting by a negative amount throws a runtime exception. | ||
| expression ``x << y`` is equivalent to ``x * 2**y``, and, for positive integers, | ||
| ``x >> y`` is equivalent to ``x / 2**y``. For negative ``x``, ``x >> y`` | ||
| is equivalent to dividing by a power of ``2`` while rounding down (towards negative infinity). |
There was a problem hiding this comment.
We should probably say something about literals. -1>>2 will result in -1/2 and thus int(-1>>1) will be different from int(-1) >> 1, right?
There was a problem hiding this comment.
There is also a comment in Types.cpp:1057 (BinaryOperatorResult of RationalNumberType).
There was a problem hiding this comment.
I hadn't thought about literals actually - I think they need to be reworked as well.
Looking at Types.cpp:1060-1077 it looks like -1>>1 will actually perform logical shift and not arithmetic shift and result in 0 right now. Similarly -1>>2, etc.
The question is: should -1>>1 be -1/2 or should it be -1?
There was a problem hiding this comment.
It should be consistent with int(-1) >> 1 I would say. @axic what do you think?
There was a problem hiding this comment.
I would agree that -1>>1 should be consistent with int(-1) >> 1 and therefore result in -1. Fractional operands are disallowed anyways and one can always use -1/(2^1) (resp. -1/(2^c) instead of -1>>c, where c is a positive literal) instead, if one wants fractional results.
In any case I will add test cases for right shifting negative literals - they were apparently missing or haven't been updated.
libsolidity/ast/Types.cpp
Outdated
| value = m_value.numerator() < 0 ? -1 : 0; | ||
| else | ||
| value = rational(m_value.numerator() / mp::pow(bigint(2), exponent), 1); | ||
| value = rational(m_value.numerator() >> exponent, 1); |
There was a problem hiding this comment.
It looks like boost can actually perform arithmetic right shift on bigint, but I'm still looking for a mention of that in the boost docs and haven't found anything so far...
There was a problem hiding this comment.
Ok - I think this is undefined in boost, so I will have to change away from this.
65a39d6 to
fb4ec8d
Compare
2d1ffcf to
f98c8fa
Compare
| char const* sourceCode = R"( | ||
| contract C { | ||
| function f1() pure returns (int) { | ||
| return -4266 >> 0; |
There was a problem hiding this comment.
Can you change these tests to move the expectations closer to the test case, perhaps using return (-4266 >> 0 == -4266)?
|
@axic @leonardoalt can you please review the shifting routines? |
af01d6b to
9080a13
Compare
axic
left a comment
There was a problem hiding this comment.
This in general looks good to me.
| // For positive values xor_mask is zero and xor(value_to_shift, xor_mask) is again value_to_shift. | ||
| m_context.appendInlineAssembly(R"({ | ||
| let xor_mask := sub(0, slt(value_to_shift, 0)) | ||
| value_to_shift := xor(div(xor(value_to_shift, xor_mask), exp(2, shift_amount)), xor_mask) |
There was a problem hiding this comment.
By the way, something that might also work is (we could check if it is cheaper):
mstore(0x20, value)
signextend(mload(sub(0x20, shift_amount)), shift_amount)
There was a problem hiding this comment.
signextend uses byte offsets, not bit offsets, doesn't it?
There was a problem hiding this comment.
Not sure how I could get a shift that's not a multiple of 8 this way...
There was a problem hiding this comment.
Oh you are right! I forgot about that!
|
@chriseth Do you think the comment in ExpressionCompiler.cpp works now? |
|
I am still fine merging this. The code can be improved/changed, if needed, at a later time. |
|
This is in conflict now. Will rebase. |
|
Rebased. @chriseth are we ok to merge this? |
69bbf2f to
a9cb89c
Compare
| if (m_context.evmVersion().hasBitwiseShifting() && !c_valueSigned) | ||
| m_context << Instruction::SHR; | ||
| if (m_context.evmVersion().hasBitwiseShifting()) | ||
| m_context << Instruction::SAR; |
There was a problem hiding this comment.
Hm, I think this is broken. This should check for c_valueSigned, otherwise right shifting an unsigned value with the top bit set will be wrong.
There was a problem hiding this comment.
Oh, wow - you're right! I was so focused on the emulation part that I missed this entirely! I fixed it and added another test case that would have caught this mistake.
Closes #3847.