solidity
solidity copied to clipboard
Fixed point types
https://www.pivotaltracker.com/story/show/81779716
TODO:
- [x] assignment (lvalue and rvalue)
- [x] conversion (between different fixed types)
- [x] conversion (between other types)
- [x] comparison operators (< > =)
- [ ] unary operators (-, --, ++)
- [ ] binary operators (+ - / * )
- [ ] do-we-need-these? binary operators (% **)
Some notes:
IntegerType::binaryOperatorResult is too tight, this should work but seems not to: uint128(1) + ufixed(2), same with FixedPointType::binaryOperatorResult
also this should work: .5 * uint128(7)
what happens currently with uint(7) / 2? Is it identical to .5 * uint(7)?
add test about signed mod with rational constants (should behave identical to SMOD opcode)
signed mod as in a modulus operation with signed rational constants?
yes - and fixed point
alright. I'll get on that. Crafting tests. Working on some fixes. And then it's onto the actual compilation.
so couple of things. I got your first one working.
ufixed a = uint128(1) + ufixed(2);
The second two in the examples you laid out, based on how we defined implicit conversion are currently impossible.
0.5 currently converts to ufixed0x8 and that does not convert with a uint128.
uint(7) / 2 stays a uint256 after the division by 2 (it's truncating), and therefore cannot convert to ufixed128x128. And no... 0.5 * uint(7) is not the same as uint(7)/2....one is a ufixed0x8 and the other is dividing by an integer....kind of confusing....I'm thinking we may need to fix this up. Open to all suggestions....the only thing I can think up right now is to create a class atop integer type and fixed point and make it a super class of some kind...
We decided to rather denote decimal places instead of "number of bits after the comma" to reduce confusion among users. This means that
fixed64x7 is a type of 64 bits and a value x of this type is interpreted as the number x / 10**7.
The type fixed is an alias for fixed128x19. The reason is that this type simplifies multiplication and also allows conversion from int64 without loss of precision / range.
@chriseth do we still want to allow users to fully extend the decimal range so that it can be fixed0x32 (I believe that's the full amount that it could take in but may be wrong).
@VoR0220 note that the 0 is the total amount of bits in the type. The drawback of using decimals is that you cannot force the value to be between 0 and 1 anymore (because that does not fit the decimal range).
@chriseth so in other words it HAS to have an integer portion now...That's fine by me. The range and precisions seriously diminishes after 128 bits in fixed point either way.
Actually...I think we can. http://academic.evergreen.edu/projects/biophysics/technotes/program/bcd.htm#multiply
This might be relevant: https://github.com/gnosis/solidity-arithmetic/blob/master/contracts/Arithmetic.sol
^Good find.
Added a todo list above.
I think a good start could be making the below code compile:
contract C {
fixed a = 3.14;
function f(fixed b) {
a = b;
}
function g() returns (fixed) {
return a;
}
}
Is this still planned for completion?
A good question raised by @meowingtwurtle: should an explicit typecast from fixed point to integer be a reinterpretation of the bits or an actual integer conversion?
Currently typecasting is a mix between the two, but mostly it is an actual conversion and not reinterpretation. One example is function type to address (the address of the external contract) and to bytes4 (the signature of the external function).
I think we should always do an actual conversion, e.g. fixed point to integer is the integer only part. Reinterpretation can always be done via assembly if needed.
In the future it may make sense introducing a notation to separate the two.
The test case in this issue could be much improved to illustrate all features that are required to be implemented.
As long as shifts are now available in EVM, probably binary fixed point types should be considered yet again. Also it would be great to have 128-bit binary fixed point, e.g. 64.64 or 96.32 bit, because they are cheaper to multiply and divide than wider fixed point types.
I still think it might be dangerous to provide binary fixed point numbers since they are harder to understand. You are right that previously, there was no efficiency difference between the two, though...
Binary fixed point is not harder to understand (actually simpler), than binary floating point, and people usually don't have to understand internal mechanics to use them. I believe many C++ developers think, that by writing 2.99e8 they use decimal floating point, but does this misunderstanding lead to any harm?
As long as one may write area = 3.141592 * radius * radius and get correct answer, it does not matter, whether it is binary or decimal, and whether it is fixed or floating point numbers under the hood.
People are generally way more used to decimal fixed points - they use them all the time when e.g. using money. They know how rounding works and are aware about the precision limitations. Comparing binary fixed points to floating points does not gain anything, one complex beast is as complex as another one... ;)
People are using decimal fixed point for money in real life, but in computer programs best practice is to always use integer numbers for money and measure money amounts in the smallest units (Wei for Ether, Satoshi for Bitcoin, cent for USD etc). The number of decimals is only considered when converting money amount to/from string.
As long as string conversion is usually performed not in smart contract, but rather in client-side code of DApp, I would not at all consider money amounts as a use case for fixed point numbers in Solidity. What could help here is a "decimals" hint in ABI JSON, that will help Web3 API to properly convert Javascript big number with decimals into/from integer when passing them to/from smart contract.
The real use cases for fixed point numbers in Solidity are calculations of simple and compound interest rates, exchange rates, margin rates, fees, reserve amounts etc.
I still don't see the point. Interest rates are usually given in percents, not "perbins". All the other things you give are usually displayed in decimal to the user and thus also having decimal fixed points for them in the smart contract would remove confusion.
All numbers are usually displayed in decimals, including numbers such as Pi, e, and √2. This does not make these numbers more decimal that binary. Actually, decimal/binary is just an encoding, not the property of the number. For percents I didn't get your point. For me, interest rate of 2.45% is neither decimal nor binary, but just fractional. In Ethereum smart contracts it is usually convenient to store 1-second interest rate, rather than annual. This allows calculating compound interest for arbitrary time interval just by raising 1-second interest rate to the power of number of seconds in the interval (in Ethereum all time intervals always contain integer number of seconds). Such 1-second rates are usually something like 0.000000094% (corresponds to 3% annual rate). For me, both, decimal and binary representation works equally well for such rates. Mainstream Javascript implementations use binary integers and binary floating point, so at least for Javascript community, binary numbers are more familiar than decimals, in terms of range, precision, and rounding.
My point is: If you use two decimal places precision, you know that you can always store all integer percentage values without loss of precision. You need 9 decimal places to store 0.000000094% without loss of precision. How many binary places do you need? And is the number representable as a binary fraction at all?
JSON uses, and its author champions, floating decimal.
I believe 10EE-18 is popular in Solidity implementations. Here is the implementation in Compound Finance https://github.com/compound-finance/compound-money-market/blob/master/contracts/Exponential.sol
As long as source code is written with decimal literals then fixed point numbers should only be decimal based.
JSON uses, and its author champions, floating decimal.
I would not agree. For JSON, number format is implementation-dependent, but RFC 7159 explicitly says that:
This specification allows implementations to set limits on the range and precision of numbers accepted. Since software that implements IEEE 754-2008 binary64 (double precision) numbers [IEEE754] is generally available and widely used, good interoperability can be achieved by implementations that expect no more precision or range than these provide, in the sense that implementations will approximate JSON numbers within the expected precision. A JSON number such as 1E400 or 3.141592653589793238462643383279 may indicate potential interoperability problems, since it suggests that the software that created it expects receiving software to have greater capabilities for numeric magnitude and precision than is widely available.
Also, for this
As long as source code is written with decimal literals then fixed point numbers should only be decimal based.
I would not agree again. Solidity source code is usually written with both decimal and hexadecimal literals used equally often.
It seems that we are mixing three very different cases here:
- Numbers that are intrinsically integer but are rendered with certain fixed number of decimals. This includes money amounts such as Ether (stored in Wei but rendered in Ether with 18 decimals), Bitcoin (stored in Satoshi, but rendered in Bitcoin with 8 decimals), USD (stored in cents but rendered in USD with 2 decimals), etc. As long as these numbers are stored as integers, they do not need any fraction numbers support from compiler and platform. (Most common case)
- Real numbers with the more the better range and precision. In mainstream programming languages de-facto standard for such numbers is IEEE 754 double precision floating point numbers that have enough range and precision for most real-life applications. (Quite common case)
- Fractional numbers with particular decimal precision. Such numbers are often used to meet requirements of formal accounting rules, and usually accompanied with strict rounding rules such as round half to even rule. Mainstream languages usually do not support such numbers natively, but only via libraries. Though, specialized financial-oriented languages may provide core support for such numbers. (Quite rare case)
Fixed point numbers are not essential, but they are nice to have. You can always use integers and keep a fractional divisor in your head and for that divisor, it is mostly irrelevant whether it is a power of 2 or of 10. IEEE 754 does not really apply here because it specifies floating-point numbers. If you store the exponent dynamically as in floating point numbers, using powers of two makes more sense, but we do not store it dynamically.
In general, I do not think that we should use arguments like "mainstream programming languages use X", but rather "mainstream programming languages use X because of the following advantages".
My impression is that most mainstream programming languages use binary exponents because of the above argument about floating point numbers and because floating point numbers are a feature of their target machine. Both of these issues are not relevant for Solidity.
@fulldecent if I remember correctly, you have been a strong opponent of adding fixed point number types to Solidity in general. Is that still your opinion?
No, one cannot just keep divisor in head, and use integers for fixed point, unless the numbers are intrinsically integers (see my previous comment). Fixed point numbers behave differently when multiplied and divided, for example 2.00 * 3.00 = 6.00, while 200 * 300 = 60000. And, most importantly, they have different overflow behavior. For example, if one uses signed 32-bit integers to represent fixed point numbers with 3 decimals, then 1000.000 * 2000.000 will return 2000000.000 (fits into signed 32-bit), while 1000000 * 2000000 will overflow. Thus one cannot just write something like x * y / 1000 to simulate fixed point multiplication via integers.
@3sGgpQ8H of course you have to adapt some arithmetic operations, but the point I was making is that fixed point numbers do not need any drastically different ABI encoding or memory representation.
@chriseth My biggest argument is that the target machine supports only integers so we should support only integers.
My second biggest argument is that there is not widespread use of Exponent.sol or other userland approaches therefore it is wholly premature to add this feature to the language.
I'll change my mind when:
- Metamask actually uses contract ABIs when presenting transactions to the user;
- There is/are well-written userland fixed point implementation(s); and
- Projects that matter (deployed to production and having users) are using the implementation(s)