Skip to content

[BREAKING] Use proper SAR for signed right shifts and emulate on pre-constantinople.#4084

Merged
axic merged 2 commits intodevelopfrom
signedRightShift
Jun 12, 2018
Merged

[BREAKING] Use proper SAR for signed right shifts and emulate on pre-constantinople.#4084
axic merged 2 commits intodevelopfrom
signedRightShift

Conversation

@ekpyron
Copy link
Collaborator

@ekpyron ekpyron commented May 7, 2018

Closes #3847.

@ekpyron ekpyron self-assigned this May 7, 2018
@ekpyron ekpyron changed the title Use proper SAR for signed right shifts; emulate on pre-constantinopel. [BREAKING] Use proper SAR for signed right shifts; emulate on pre-constantinopel. May 7, 2018
Changelog.md Outdated
### 0.5.0 (unreleased)

Features:
* General: Signed right shift uses proper arithmetic shift, i.e. rounding towards negative infinity.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use inline assembly for this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this can be done as a pragma 050, should we do that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah OK - sorry, I missed your last comment.

Copy link
Contributor

@chriseth chriseth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change it to using inline assembly.

@ekpyron ekpyron force-pushed the signedRightShift branch 2 times, most recently from f874af2 to dd0205a Compare May 8, 2018 07:55
@ekpyron
Copy link
Collaborator Author

ekpyron commented May 8, 2018

@chriseth
Uses inline assembly now.
I had a look at the optimized EVM assembly output and at the gas estimates. The gas estimates are identical for the assembly and the non-assembly versions and the generated code is slightly different (as one would expect), but looks fine.

@ekpyron
Copy link
Collaborator Author

ekpyron commented May 8, 2018

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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a space after each comma :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some test cases that verify that signextend is not needed.

@ekpyron ekpyron force-pushed the signedRightShift branch from dd0205a to 0dac962 Compare May 8, 2018 08:04
}
)";
compileAndRun(sourceCode, 0, "C");
ABI_CHECK(callContractFunction("f(int16,int16)", u256(-4266), u256(0)), encodeArgs(u256(-4266)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use hex here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ekpyron ekpyron force-pushed the signedRightShift branch 3 times, most recently from d747897 to ea37ddc Compare May 8, 2018 09:41
@axic
Copy link
Contributor

axic commented May 8, 2018

@ekpyron do you want to pull out the test cases? Those can be merged now (with the current values).

@ekpyron
Copy link
Collaborator Author

ekpyron commented May 9, 2018

Note: Needs to be rebased once the tests in #4102 are merged into develop and develop is merged into 050.

@ekpyron
Copy link
Collaborator Author

ekpyron commented May 14, 2018

Rebased.

Changelog.md Outdated
### 0.5.0 (unreleased)

Breaking Changes:
* General: Signed right shift uses proper arithmetic shift, i.e. rounding towards negative infinity.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a comment in Types.cpp:1057 (BinaryOperatorResult of RationalNumberType).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be consistent with int(-1) >> 1 I would say. @axic what do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ekpyron ekpyron force-pushed the signedRightShift branch from f4a0a96 to 934dbdb Compare May 14, 2018 17:56
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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - I think this is undefined in boost, so I will have to change away from this.

@ekpyron ekpyron force-pushed the signedRightShift branch 2 times, most recently from 65a39d6 to fb4ec8d Compare May 14, 2018 18:42
@chriseth chriseth force-pushed the 050 branch 2 times, most recently from 2d1ffcf to f98c8fa Compare May 15, 2018 13:08
@ekpyron ekpyron force-pushed the signedRightShift branch from a86ccd1 to 00bc38c Compare May 23, 2018 17:59
char const* sourceCode = R"(
contract C {
function f1() pure returns (int) {
return -4266 >> 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change these tests to move the expectations closer to the test case, perhaps using return (-4266 >> 0 == -4266)?

@chriseth
Copy link
Contributor

@axic @leonardoalt can you please review the shifting routines?

@ekpyron ekpyron force-pushed the signedRightShift branch 2 times, most recently from af01d6b to 9080a13 Compare May 28, 2018 18:32
@axic axic changed the title [BREAKING] Use proper SAR for signed right shifts; emulate on pre-constantinopel. [BREAKING] Use proper SAR for signed right shifts; emulate on pre-constantinople. May 30, 2018
axic
axic previously approved these changes May 30, 2018
Copy link
Contributor

@axic axic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signextend uses byte offsets, not bit offsets, doesn't it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how I could get a shift that's not a multiple of 8 this way...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you are right! I forgot about that!

@ekpyron ekpyron force-pushed the signedRightShift branch from a31386d to 782f2b2 Compare June 7, 2018 14:46
@ekpyron
Copy link
Collaborator Author

ekpyron commented Jun 7, 2018

@chriseth Do you think the comment in ExpressionCompiler.cpp works now?

@axic
Copy link
Contributor

axic commented Jun 11, 2018

I am still fine merging this. The code can be improved/changed, if needed, at a later time.

axic
axic previously approved these changes Jun 11, 2018
@axic
Copy link
Contributor

axic commented Jun 11, 2018

This is in conflict now. Will rebase.

@axic
Copy link
Contributor

axic commented Jun 11, 2018

Rebased. @chriseth are we ok to merge this?

@ekpyron ekpyron force-pushed the signedRightShift branch from 69bbf2f to a9cb89c Compare June 11, 2018 23:02
axic
axic previously requested changes Jun 11, 2018
if (m_context.evmVersion().hasBitwiseShifting() && !c_valueSigned)
m_context << Instruction::SHR;
if (m_context.evmVersion().hasBitwiseShifting())
m_context << Instruction::SAR;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@axic axic force-pushed the signedRightShift branch from a2e479e to e84b55b Compare June 12, 2018 08:32
@axic axic changed the title [BREAKING] Use proper SAR for signed right shifts; emulate on pre-constantinople. [BREAKING] Use proper SAR for signed right shifts and emulate on pre-constantinople. Jun 12, 2018
@axic axic merged commit ae2b589 into develop Jun 12, 2018
@axic axic deleted the signedRightShift branch June 12, 2018 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants