about summary refs log tree commit diff stats
path: root/412render-float-decimal.mu
diff options
context:
space:
mode:
Diffstat (limited to '412render-float-decimal.mu')
-rw-r--r--412render-float-decimal.mu597
1 files changed, 597 insertions, 0 deletions
diff --git a/412render-float-decimal.mu b/412render-float-decimal.mu
new file mode 100644
index 00000000..7aaf8b3a
--- /dev/null
+++ b/412render-float-decimal.mu
@@ -0,0 +1,597 @@
+# print out floats in decimal
+# https://research.swtch.com/ftoa
+#
+# Basic idea:
+#   Ignoring sign, floating point numbers are represented as 1.mantissa * 2^exponent
+#   Therefore, to print a float in decimal, we need to:
+#     - compute its value without decimal point
+#     - convert to an array of decimal digits
+#     - print out the array while inserting the decimal point appropriately
+#
+# Basic complication: the computation generates numbers larger than an int can
+# hold. We need a way to represent big ints.
+#
+# Key insight: use a representation for big ints that's close to what we need
+# anyway, an array of decimal digits.
+#
+# Style note: we aren't creating a big int library here. The only operations
+# we need are halving and doubling. Following the link above, it seems more
+# comprehensible to keep these operations inlined so that we can track the
+# position of the decimal point with dispatch.
+#
+# This approach turns out to be fast enough for most purposes.
+# Optimizations, however, get wildly more complex.
+
+fn test-write-float-decimal-approximate-normal {
+  var s-storage: (stream byte 0x10)
+  var s/ecx: (addr stream byte) <- address s-storage
+  # 0.5
+  var half/xmm0: float <- rational 1, 2
+  write-float-decimal-approximate s, half, 3
+  check-stream-equal s, "0.5", "F - test-write-float-decimal-approximate-normal 0.5"
+  # 0.25
+  clear-stream s
+  var quarter/xmm0: float <- rational 1, 4
+  write-float-decimal-approximate s, quarter, 3
+  check-stream-equal s, "0.25", "F - test-write-float-decimal-approximate-normal 0.25"
+  # 0.75
+  clear-stream s
+  var three-quarters/xmm0: float <- rational 3, 4
+  write-float-decimal-approximate s, three-quarters, 3
+  check-stream-equal s, "0.75", "F - test-write-float-decimal-approximate-normal 0.75"
+  # 0.125
+  clear-stream s
+  var eighth/xmm0: float <- rational 1, 8
+  write-float-decimal-approximate s, eighth, 3
+  check-stream-equal s, "0.125", "F - test-write-float-decimal-approximate-normal 0.125"
+  # 0.0625; start using scientific notation
+  clear-stream s
+  var sixteenth/xmm0: float <- rational 1, 0x10
+  write-float-decimal-approximate s, sixteenth, 3
+  check-stream-equal s, "6.25e-2", "F - test-write-float-decimal-approximate-normal 0.0625"
+  # sqrt(2); truncate floats with lots of digits after the decimal but not too many before
+  clear-stream s
+  var two-f/xmm0: float <- rational 2, 1
+  var sqrt-2/xmm0: float <- square-root two-f
+  write-float-decimal-approximate s, sqrt-2, 3
+  check-stream-equal s, "1.414", "F - test-write-float-decimal-approximate-normal √2"
+}
+
+# print whole integers without decimals
+fn test-write-float-decimal-approximate-integer {
+  var s-storage: (stream byte 0x10)
+  var s/ecx: (addr stream byte) <- address s-storage
+  # 1
+  var one-f/xmm0: float <- rational 1, 1
+  write-float-decimal-approximate s, one-f, 3
+  check-stream-equal s, "1", "F - test-write-float-decimal-approximate-integer 1"
+  # 2
+  clear-stream s
+  var two-f/xmm0: float <- rational 2, 1
+  write-float-decimal-approximate s, two-f, 3
+  check-stream-equal s, "2", "F - test-write-float-decimal-approximate-integer 2"
+  # 10
+  clear-stream s
+  var ten-f/xmm0: float <- rational 0xa, 1
+  write-float-decimal-approximate s, ten-f, 3
+  check-stream-equal s, "10", "F - test-write-float-decimal-approximate-integer 10"
+  # -10
+  clear-stream s
+  var minus-ten-f/xmm0: float <- rational -0xa, 1
+  write-float-decimal-approximate s, minus-ten-f, 3
+  check-stream-equal s, "-10", "F - test-write-float-decimal-approximate-integer -10"
+  # 999
+  clear-stream s
+  var minus-ten-f/xmm0: float <- rational 0x3e7, 1
+  write-float-decimal-approximate s, minus-ten-f, 3
+  check-stream-equal s, "999", "F - test-write-float-decimal-approximate-integer 1000"
+  # 1000 - start using scientific notation
+  clear-stream s
+  var minus-ten-f/xmm0: float <- rational 0x3e8, 1
+  write-float-decimal-approximate s, minus-ten-f, 3
+  check-stream-equal s, "1.00e3", "F - test-write-float-decimal-approximate-integer 1000"
+  # 100,000
+  clear-stream s
+  var hundred-thousand/eax: int <- copy 0x186a0
+  var hundred-thousand-f/xmm0: float <- convert hundred-thousand
+  write-float-decimal-approximate s, hundred-thousand-f, 3
+  check-stream-equal s, "1.00e5", "F - test-write-float-decimal-approximate-integer 100,000"
+}
+
+fn test-write-float-decimal-approximate-zero {
+  var s-storage: (stream byte 0x10)
+  var s/ecx: (addr stream byte) <- address s-storage
+  var zero: float
+  write-float-decimal-approximate s, zero, 3
+  check-stream-equal s, "0", "F - test-write-float-decimal-approximate-zero"
+}
+
+fn test-write-float-decimal-approximate-negative-zero {
+  var s-storage: (stream byte 0x10)
+  var s/ecx: (addr stream byte) <- address s-storage
+  var n: int
+  copy-to n, 0x80000000
+  var negative-zero/xmm0: float <- reinterpret n
+  write-float-decimal-approximate s, negative-zero, 3
+  check-stream-equal s, "-0", "F - test-write-float-decimal-approximate-negative-zero"
+}
+
+fn test-write-float-decimal-approximate-infinity {
+  var s-storage: (stream byte 0x10)
+  var s/ecx: (addr stream byte) <- address s-storage
+  var n: int
+  #          0|11111111|00000000000000000000000
+  #          0111|1111|1000|0000|0000|0000|0000|0000
+  copy-to n, 0x7f800000
+  var infinity/xmm0: float <- reinterpret n
+  write-float-decimal-approximate s, infinity, 3
+  check-stream-equal s, "Inf", "F - test-write-float-decimal-approximate-infinity"
+}
+
+fn test-write-float-decimal-approximate-negative-infinity {
+  var s-storage: (stream byte 0x10)
+  var s/ecx: (addr stream byte) <- address s-storage
+  var n: int
+  copy-to n, 0xff800000
+  var negative-infinity/xmm0: float <- reinterpret n
+  write-float-decimal-approximate s, negative-infinity, 3
+  check-stream-equal s, "-Inf", "F - test-write-float-decimal-approximate-negative-infinity"
+}
+
+fn test-write-float-decimal-approximate-not-a-number {
+  var s-storage: (stream byte 0x10)
+  var s/ecx: (addr stream byte) <- address s-storage
+  var n: int
+  copy-to n, 0xffffffff  # exponent must be all 1's, and mantissa must be non-zero
+  var nan/xmm0: float <- reinterpret n
+  write-float-decimal-approximate s, nan, 3
+  check-stream-equal s, "NaN", "F - test-write-float-decimal-approximate-not-a-number"
+}
+
+fn render-float-decimal screen: (addr screen), in: float, precision: int, x: int, y: int, color: int, background-color: int -> _/eax: int {
+  var s-storage: (stream byte 0x10)
+  var s/esi: (addr stream byte) <- address s-storage
+  write-float-decimal-approximate s, in, precision
+  var width/eax: int <- copy 0
+  var height/ecx: int <- copy 0
+  width, height <- screen-size screen
+  var result/eax: int <- draw-stream-rightward screen, s, x, width, y, color, background-color
+  return result
+}
+
+# 'precision' controls the maximum width past which we resort to scientific notation
+fn write-float-decimal-approximate out: (addr stream byte), in: float, precision: int {
+  # - special names
+  var bits/eax: int <- reinterpret in
+  compare bits, 0
+  {
+    break-if-!=
+    write out, "0"
+    return
+  }
+  compare bits, 0x80000000
+  {
+    break-if-!=
+    write out, "-0"
+    return
+  }
+  compare bits, 0x7f800000
+  {
+    break-if-!=
+    write out, "Inf"
+    return
+  }
+  compare bits, 0xff800000
+  {
+    break-if-!=
+    write out, "-Inf"
+    return
+  }
+  var exponent/ecx: int <- copy bits
+  exponent <- shift-right 0x17  # 23 bits of mantissa
+  exponent <- and 0xff
+  exponent <- subtract 0x7f
+  compare exponent, 0x80
+  {
+    break-if-!=
+    write out, "NaN"
+    return
+  }
+  # - regular numbers
+  var sign/edx: int <- copy bits
+  sign <- shift-right 0x1f
+  {
+    compare sign, 1
+    break-if-!=
+    append-byte out, 0x2d/minus
+  }
+
+  # v = 1.mantissa (in base 2) << 0x17
+  var v/ebx: int <- copy bits
+  v <- and 0x7fffff
+  v <- or 0x00800000  # insert implicit 1
+  # e = exponent - 0x17
+  var e/ecx: int <- copy exponent
+  e <- subtract 0x17  # move decimal place from before mantissa to after
+
+  # initialize buffer with decimal representation of v
+  # unlike https://research.swtch.com/ftoa, no ascii here
+  var buf-storage: (array byte 0x7f)
+  var buf/edi: (addr array byte) <- address buf-storage
+  var n/eax: int <- decimal-digits v, buf
+  # I suspect we can do without reversing, but we'll follow https://research.swtch.com/ftoa
+  # closely for now.
+  reverse-digits buf, n
+
+  # loop if e > 0
+  {
+    compare e, 0
+    break-if-<=
+    n <- double-array-of-decimal-digits buf, n
+    e <- decrement
+    loop
+  }
+
+  var dp/edx: int <- copy n
+
+  # loop if e < 0
+  {
+    compare e, 0
+    break-if->=
+    n, dp <- halve-array-of-decimal-digits buf, n, dp
+    e <- increment
+    loop
+  }
+
+  _write-float-array-of-decimal-digits out, buf, n, dp, precision
+}
+
+# store the decimal digits of 'n' into 'buf', units first
+# n must be positive
+fn decimal-digits n: int, _buf: (addr array byte) -> _/eax: int {
+  var buf/edi: (addr array byte) <- copy _buf
+  var i/ecx: int <- copy 0
+  var curr/eax: int <- copy n
+  var curr-byte/edx: int <- copy 0
+  {
+    compare curr, 0
+    break-if-=
+    curr, curr-byte <- integer-divide curr, 0xa
+    var dest/ebx: (addr byte) <- index buf, i
+    copy-byte-to *dest, curr-byte
+    i <- increment
+    loop
+  }
+  return i
+}
+
+fn reverse-digits _buf: (addr array byte), n: int {
+  var buf/esi: (addr array byte) <- copy _buf
+  var left/ecx: int <- copy 0
+  var right/edx: int <- copy n
+  right <- decrement
+  {
+    compare left, right
+    break-if->=
+    {
+      var l-a/ecx: (addr byte) <- index buf, left
+      var r-a/edx: (addr byte) <- index buf, right
+      var l/ebx: byte <- copy-byte *l-a
+      var r/eax: byte <- copy-byte *r-a
+      copy-byte-to *l-a, r
+      copy-byte-to *r-a, l
+    }
+    left <- increment
+    right <- decrement
+    loop
+  }
+}
+
+fn double-array-of-decimal-digits _buf: (addr array byte), _n: int -> _/eax: int {
+  var buf/edi: (addr array byte) <- copy _buf
+  # initialize delta
+  var delta/edx: int <- copy 0
+  {
+    var curr/ebx: (addr byte) <- index buf, 0
+    var tmp/eax: byte <- copy-byte *curr
+    compare tmp, 5
+    break-if-<
+    delta <- copy 1
+  }
+  # loop
+  var x/eax: int <- copy 0
+  var i/ecx: int <- copy _n
+  i <- decrement
+  {
+    compare i, 0
+    break-if-<=
+    # x += 2*buf[i]
+    {
+      var tmp/ecx: (addr byte) <- index buf, i
+      var tmp2/ecx: byte <- copy-byte *tmp
+      x <- add tmp2
+      x <- add tmp2
+    }
+    # x, buf[i+delta] = x/10, x%10
+    {
+      var dest-index/ecx: int <- copy i
+      dest-index <- add delta
+      var dest/edi: (addr byte) <- index buf, dest-index
+      var next-digit/edx: int <- copy 0
+      x, next-digit <- integer-divide x, 0xa
+      copy-byte-to *dest, next-digit
+    }
+    #
+    i <- decrement
+    loop
+  }
+  # final patch-up
+  var n/eax: int <- copy _n
+  compare delta, 1
+  {
+    break-if-!=
+    var curr/ebx: (addr byte) <- index buf, 0
+    var one/edx: int <- copy 1
+    copy-byte-to *curr, one
+    n <- increment
+  }
+  return n
+}
+
+fn halve-array-of-decimal-digits _buf: (addr array byte), _n: int, _dp: int -> _/eax: int, _/edx: int {
+  var buf/edi: (addr array byte) <- copy _buf
+  var n/eax: int <- copy _n
+  var dp/edx: int <- copy _dp
+  # initialize one side
+  {
+    # if buf[n-1]%2 == 0, break
+    var right-index/ecx: int <- copy n
+    right-index <- decrement
+    var right-a/ecx: (addr byte) <- index buf, right-index
+    var right/ecx: byte <- copy-byte *right-a
+    var right-int/ecx: int <- copy right
+    var remainder/edx: int <- copy 0
+    {
+      var dummy/eax: int <- copy 0
+      dummy, remainder <- integer-divide right-int, 2
+    }
+    compare remainder, 0
+    break-if-=
+    # buf[n] = 0
+    var next-a/ecx: (addr byte) <- index buf, n
+    var zero/edx: byte <- copy 0
+    copy-byte-to *next-a, zero
+    # n++
+    n <- increment
+  }
+  # initialize the other
+  var delta/ebx: int <- copy 0
+  var x/esi: int <- copy 0
+  {
+    # if buf[0] >= 2, break
+    var left/ecx: (addr byte) <- index buf, 0
+    var src/ecx: byte <- copy-byte *left
+    compare src, 2
+    break-if->=
+    # delta, x = 1, buf[0]
+    delta <- copy 1
+    x <- copy src
+    # n--
+    n <- decrement
+    # dp--
+    dp <- decrement
+  }
+  # loop
+  var i/ecx: int <- copy 0
+  {
+    compare i, n
+    break-if->=
+    # x = x*10 + buf[i+delta]
+    {
+      var ten/edx: int <- copy 0xa
+      x <- multiply ten
+      var src-index/edx: int <- copy i
+      src-index <- add delta
+      var src-a/edx: (addr byte) <- index buf, src-index
+      var src/edx: byte <- copy-byte *src-a
+      x <- add src
+    }
+    # buf[i], x = x/2, x%2
+    {
+      var quotient/eax: int <- copy 0
+      var remainder/edx: int <- copy 0
+      quotient, remainder <- integer-divide x, 2
+      x <- copy remainder
+      var dest/edx: (addr byte) <- index buf, i
+      copy-byte-to *dest, quotient
+    }
+    #
+    i <- increment
+    loop
+  }
+  return n, dp
+}
+
+fn _write-float-array-of-decimal-digits out: (addr stream byte), _buf: (addr array byte), n: int, dp: int, precision: int {
+  var buf/edi: (addr array byte) <- copy _buf
+  {
+    compare dp, 0
+    break-if->=
+    _write-float-array-of-decimal-digits-in-scientific-notation out, buf, n, dp, precision
+    return
+  }
+  {
+    var dp2/eax: int <- copy dp
+    compare dp2, precision
+    break-if-<=
+    _write-float-array-of-decimal-digits-in-scientific-notation out, buf, n, dp, precision
+    return
+  }
+  {
+    compare dp, 0
+    break-if-!=
+    append-byte out, 0x30/0
+  }
+  var i/eax: int <- copy 0
+  # bounds = min(n, dp+3)
+  var limit/edx: int <- copy dp
+  limit <- add 3
+  {
+    compare limit, n
+    break-if-<=
+    limit <- copy n
+  }
+  {
+    compare i, limit
+    break-if->=
+    # print '.' if necessary
+    compare i, dp
+    {
+      break-if-!=
+      append-byte out, 0x2e/decimal-point
+    }
+    var curr-a/ecx: (addr byte) <- index buf, i
+    var curr/ecx: byte <- copy-byte *curr-a
+    var curr-int/ecx: int <- copy curr
+    curr-int <- add 0x30/0
+    append-byte out, curr-int
+    #
+    i <- increment
+    loop
+  }
+}
+
+fn _write-float-array-of-decimal-digits-in-scientific-notation out: (addr stream byte), _buf: (addr array byte), n: int, dp: int, precision: int {
+  var buf/edi: (addr array byte) <- copy _buf
+  var i/eax: int <- copy 0
+  {
+    compare i, n
+    break-if->=
+    compare i, precision
+    break-if->=
+    compare i, 1
+    {
+      break-if-!=
+      append-byte out, 0x2e/decimal-point
+    }
+    var curr-a/ecx: (addr byte) <- index buf, i
+    var curr/ecx: byte <- copy-byte *curr-a
+    var curr-int/ecx: int <- copy curr
+    curr-int <- add 0x30/0
+    append-byte out, curr-int
+    #
+    i <- increment
+    loop
+  }
+  append-byte out, 0x65/e
+  decrement dp
+  write-int32-decimal out, dp
+}
+
+# follows the structure of write-float-decimal-approximate
+# 'precision' controls the maximum width past which we resort to scientific notation
+fn float-size in: float, precision: int -> _/eax: int {
+  # - special names
+  var bits/eax: int <- reinterpret in
+  compare bits, 0
+  {
+    break-if-!=
+    return 1  # for "0"
+  }
+  compare bits, 0x80000000
+  {
+    break-if-!=
+    return 2  # for "-0"
+  }
+  compare bits, 0x7f800000
+  {
+    break-if-!=
+    return 3  # for "Inf"
+  }
+  compare bits, 0xff800000
+  {
+    break-if-!=
+    return 4  # for "-Inf"
+  }
+  var exponent/ecx: int <- copy bits
+  exponent <- shift-right 0x17  # 23 bits of mantissa
+  exponent <- and 0xff
+  exponent <- subtract 0x7f
+  compare exponent, 0x80
+  {
+    break-if-!=
+    return 3  # for "NaN"
+  }
+  # - regular numbers
+  # v = 1.mantissa (in base 2) << 0x17
+  var v/ebx: int <- copy bits
+  v <- and 0x7fffff
+  v <- or 0x00800000  # insert implicit 1
+  # e = exponent - 0x17
+  var e/ecx: int <- copy exponent
+  e <- subtract 0x17  # move decimal place from before mantissa to after
+
+  # initialize buffer with decimal representation of v
+  var buf-storage: (array byte 0x7f)
+  var buf/edi: (addr array byte) <- address buf-storage
+  var n/eax: int <- decimal-digits v, buf
+  reverse-digits buf, n
+
+  # loop if e > 0
+  {
+    compare e, 0
+    break-if-<=
+    n <- double-array-of-decimal-digits buf, n
+    e <- decrement
+    loop
+  }
+
+  var dp/edx: int <- copy n
+
+  # loop if e < 0
+  {
+    compare e, 0
+    break-if->=
+    n, dp <- halve-array-of-decimal-digits buf, n, dp
+    e <- increment
+    loop
+  }
+
+  compare dp, 0
+  {
+    break-if->=
+    return 8  # hacky for scientific notation
+  }
+  {
+    var dp2/eax: int <- copy dp
+    compare dp2, precision
+    break-if-<=
+    return 8  # hacky for scientific notation
+  }
+
+  # result = min(n, dp+3)
+  var result/ecx: int <- copy dp
+  result <- add 3
+  {
+    compare result, n
+    break-if-<=
+    result <- copy n
+  }
+
+  # account for decimal point
+  compare dp, n
+  {
+    break-if->=
+    result <- increment
+  }
+
+  # account for sign
+  var sign/edx: int <- reinterpret in
+  sign <- shift-right 0x1f
+  {
+    compare sign, 1
+    break-if-!=
+    result <- increment
+  }
+  return result
+}