-
Notifications
You must be signed in to change notification settings - Fork 1
Parse float in pure Wasm #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: scala-wasm
Are you sure you want to change the base?
Conversation
|
|
||
| def parseFloat(s: String): scala.Float = { | ||
| import Utils._ | ||
| private[this] val parseFloatImpl = linkTimeIf[ParseFloatRegExpImpl](isWebAssembly) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be needed elsewhere. Perhaps define it once and for all in an object in java.util.regex.*?
| override def toString(): String = | ||
| Conversion.toDecimalScaledString(this) | ||
| linkTimeIf(targetPureWasm) { | ||
| "" // TODO: Integer.toUnsignedString |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If all you need is Integer.toUnsignedString(i: Int), you can trivially implement that one as Long.toString(Integer.toUnsignedLong(i)). Those two methods are already implemented.
| * 5^e < 2^64 <=> e < log5(2^64) <=> e < 27.563299716697156... | ||
| */ | ||
| val smallPowerOfTens = Array[FloatingPoint]( | ||
| FloatingPoint.normalized(new BigInteger("8000000000000000", 16), -63), // 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems all the constants here are unsigned Long values. Store them as such, and convert them to BigInteger without having to parse a string?
This can be done with a small helper like
def ulongToBigInt(x: Long): BigInteger =
if (x >= 0L) BigInteger.valueOf(x)
else BigInteger.valueOf(x & ~Long.MinValue).setBit(63)| private[lang] object Constants { | ||
|
|
||
| final val ExtendedSigBits = 64 | ||
| final val ExtendedMaxSig = BigInteger.TWO.pow(ExtendedSigBits) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| final val ExtendedMaxSig = BigInteger.TWO.pow(ExtendedSigBits) | |
| final val ExtendedMaxSig = BigInteger.ONE.shiftLeft(ExtendedSigBits) |
or even
| final val ExtendedMaxSig = BigInteger.TWO.pow(ExtendedSigBits) | |
| final val ExtendedMaxSig = BigInteger.ZERO.setBit(ExtendedSigBits) |
?
| /** DIY floating point number with 64-bit significand bits */ | ||
| private[dec2flt] class FloatingPoint private (val f: BigInteger, val e: Int) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it always stay within 64 significant bits? If yes, why not use a Long (interpreted as unsigned) instead of a BigInteger?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. When multiplying these 64-bit floating-point numbers, an intermediate integer is up to 128 bits, but this could be represented by two Longs, and the result is immediately normalized back into the 64-bit range. So, it seems we can avoid using BigInteger here 👍
623ce2e to
9b443e5
Compare
5f4f0b5 to
422379d
Compare
| linkTimeIf(targetPureWasm) { | ||
| // java.lang.Long.parseLong may fail with NumberFormatException | ||
| // for a large input. | ||
| new BigInteger(s, radix).doubleValue() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When parsing truncatedMantissaStr, you should be able to use parseUnsignedLong. The largest possible string for truncatedMantissaStr is "f" * 15 + "1", which correctly (though barely) parses to -15L.
When parsing binaryExpStr, if the string has more than 11 chars (10 digits and 1 sign), you can saturate it to Int.MinValue/Int.MaxValue (depending on the sign) without parsing it. If it has at most 11 chars, you can correctly parse it with parseLong (and then convert to Int with saturation; not wrapping).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, since maxPrecisionChars = 15 it always fit in unsigned 64 bit integer 👍
| js.Dynamic.global.parseInt(s, radix).asInstanceOf[scala.Double] | ||
| } | ||
|
|
||
| val mantissa = nativeParseInt(truncatedMantissaStr, 16) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, concretely, I would leave nativeParseInt as being nativeJSParseInt and do
| val mantissa = nativeParseInt(truncatedMantissaStr, 16) | |
| val mantissa = linkTimeIf(targetPureWasm) { | |
| val mantissaLong = Long.parseUnsignedLong(truncatedMantissaStr, 16) | |
| // convert unsigned long to double | |
| (mantissaLong >>> 32).toDouble * (1L << 32) + (mantissaLong & 0xffffffffL).toDouble | |
| } { | |
| nativeJSParseInt(truncatedMantissaStr, 16) | |
| } |
and below, for the computation of binaryExp (which does not show up in the diff so I can't comment on it directly):
val binaryExp = linkTimeIf(targetPureWasm) {
if (binaryExpStr.length() > 11) {
if (binaryExpStr.charAt(0) = '-') Int.MinValue
else Int.MaxValue
} else {
val binaryExpLong = Long.parseLong(binaryExpStr)
if (binaryExpLong > Int.MaxValue.toLong) Int.MaxValue
else if (binaryExpLong < Int.MinValue.toLong) Int.MinValue
else binaryExpLong.toInt
}
} {
val binaryExpDouble = nativeParseInt(binaryExpStr, 10)
binaryExpDouble.toInt // caps to [MinValue, MaxValue]
}| package java.lang | ||
|
|
||
| import java.lang.constant.{Constable, ConstantDesc} | ||
| import java.math.BigInteger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider importing that only in the relevant methods, since it is a large dependency that we shouldn't use lightly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The input mantissa for Bellerophon still needs to be parsed as a BigInteger.
Other dependencies should be eliminated once Math.scalb implementation is merged.
| val absStr = Integer.toUnsignedString(digits(0)) | ||
| // TODO: Integer.toUnsignedString hasn't yet implemented in Wasm | ||
| val absStr = linkTimeIf(targetPureWasm) { | ||
| java.lang.Long.toString(Integer.toUnsignedLong(digits(0))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could push that one step further into Integer.toUnsignedString.
| testFull("-87654.321", -87654.321) | ||
| testFull("+.3f", 0.3) | ||
|
|
||
| // Hex notation, with exactly the output of toHexString() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lost comment here :)
|
@sjrd Thank you for comments! Once scala-js#5251 is merged, I'm planning to rebase on it. |
Implement bellerophon algorithm from "How to Read Floating Point Numbers Accurately" by William D. Clinger
TODO: