https://github.com/akkartik/mu/blob/main/linux/412print-float-decimal.mu

      1 # print out floats in decimal
      2 # https://research.swtch.com/ftoa
      3 #
      4 # Basic idea:
      5 #   Ignoring sign, floating point numbers are represented as 1.mantissa * 2^exponent
      6 #   Therefore, to print a float in decimal, we need to:
      7 #     - compute its value without decimal point
      8 #     - convert to an array of decimal digits
      9 #     - print out the array while inserting the decimal point appropriately
     10 #
     11 # Basic complication: the computation generates numbers larger than an int can
     12 # hold. We need a way to represent big ints.
     13 #
     14 # Key insight: use a representation for big ints that's close to what we need
     15 # anyway, an array of decimal digits.
     16 #
     17 # Style note: we aren't creating a big int library here. The only operations
     18 # we need are halving and doubling. Following the link above, it seems more
     19 # comprehensible to keep these operations inlined so that we can track the
     20 # position of the decimal point with dispatch.
     21 #
     22 # This approach turns out to be fast enough for most purposes.
     23 # Optimizations, however, get wildly more complex.
     24
     25 fn test-write-float-decimal-approximate-normal {
     26   var s-storage: (stream byte 0x10)
     27   var s/ecx: (addr stream byte) <- address s-storage
     28   # 0.5
     29   var half/xmm0: float <- rational 1, 2
     30   write-float-decimal-approximate s, half, 3
     31   check-stream-equal s, "0.5", "F - test-write-float-decimal-approximate-normal 0.5"
     32   # 0.25
     33   clear-stream s
     34   var quarter/xmm0: float <- rational 1, 4
     35   write-float-decimal-approximate s, quarter, 3
     36   check-stream-equal s, "0.25", "F - test-write-float-decimal-approximate-normal 0.25"
     37   # 0.75
     38   clear-stream s
     39   var three-quarters/xmm0: float <- rational 3, 4
     40   write-float-decimal-approximate s, three-quarters, 3
     41   check-stream-equal s, "0.75", "F - test-write-float-decimal-approximate-normal 0.75"
     42   # 0.125
     43   clear-stream s
     44   var eighth/xmm0: float <- rational 1, 8
     45   write-float-decimal-approximate s, eighth, 3
     46   check-stream-equal s, "0.125", "F - test-write-float-decimal-approximate-normal 0.125"
     47   # 0.0625; start using scientific notation
     48   clear-stream s
     49   var sixteenth/xmm0: float <- rational 1, 0x10
     50   write-float-decimal-approximate s, sixteenth, 3
     51   check-stream-equal s, "6.25e-2", "F - test-write-float-decimal-approximate-normal 0.0625"
     52   # sqrt(2); truncate floats with lots of digits after the decimal but not too many before
     53   clear-stream s
     54   var two-f/xmm0: float <- rational 2, 1
     55   var sqrt-2/xmm0: float <- square-root two-f
     56   write-float-decimal-approximate s, sqrt-2, 3
     57   check-stream-equal s, "1.414", "F - test-write-float-decimal-approximate-normal √2"
     58 }
     59
     60 # print whole integers without decimals
     61 fn test-write-float-decimal-approximate-integer {
     62   var s-storage: (stream byte 0x10)
     63   var s/ecx: (addr stream byte) <- address s-storage
     64   # 1
     65   var one-f/xmm0: float <- rational 1, 1
     66   write-float-decimal-approximate s, one-f, 3
     67   check-stream-equal s, "1", "F - test-write-float-decimal-approximate-integer 1"
     68   # 2
     69   clear-stream s
     70   var two-f/xmm0: float <- rational 2, 1
     71   write-float-decimal-approximate s, two-f, 3
     72   check-stream-equal s, "2", "F - test-write-float-decimal-approximate-integer 2"
     73   # 10
     74   clear-stream s
     75   var ten-f/xmm0: float <- rational 0xa, 1
     76   write-float-decimal-approximate s, ten-f, 3
     77   check-stream-equal s, "10", "F - test-write-float-decimal-approximate-integer 10"
     78   # -10
     79   clear-stream s
     80   var minus-ten-f/xmm0: float <- rational -0xa, 1
     81   write-float-decimal-approximate s, minus-ten-f, 3
     82   check-stream-equal s, "-10", "F - test-write-float-decimal-approximate-integer -10"
     83   # 999
     84   clear-stream s
     85   var minus-ten-f/xmm0: float <- rational 0x3e7, 1
     86   write-float-decimal-approximate s, minus-ten-f, 3
     87   check-stream-equal s, "999", "F - test-write-float-decimal-approximate-integer 1000"
     88   # 1000 - start using scientific notation
     89   clear-stream s
     90   var minus-ten-f/xmm0: float <- rational 0x3e8, 1
     91   write-float-decimal-approximate s, minus-ten-f, 3
     92   check-stream-equal s, "1.00e3", "F - test-write-float-decimal-approximate-integer 1000"
     93   # 100,000
     94   clear-stream s
     95   var hundred-thousand/eax: int <- copy 0x186a0
     96   var hundred-thousand-f/xmm0: float <- convert hundred-thousand
     97   write-float-decimal-approximate s, hundred-thousand-f, 3
     98   check-stream-equal s, "1.00e5", "F - test-write-float-decimal-approximate-integer 100,000"
     99 }
    100
    101 fn test-write-float-decimal-approximate-zero {
    102   var s-storage: (stream byte 0x10)
    103   var s/ecx: (addr stream byte) <- address s-storage
    104   var zero: float
    105   write-float-decimal-approximate s, zero, 3
    106   check-stream-equal s, "0", "F - test-write-float-decimal-approximate-zero"
    107 }
    108
    109 fn test-write-float-decimal-approximate-negative-zero {
    110   var s-storage: (stream byte 0x10)
    111   var s/ecx: (addr stream byte) <- address s-storage
    112   var n: int
    113   copy-to n, 0x80000000
    114   var negative-zero/xmm0: float <- reinterpret n
    115   write-float-decimal-approximate s, negative-zero, 3
    116   check-stream-equal s, "-0", "F - test-write-float-decimal-approximate-negative-zero"
    117 }
    118
    119 fn test-write-float-decimal-approximate-infinity {
    120   var s-storage: (stream byte 0x10)
    121   var s/ecx: (addr stream byte) <- address s-storage
    122   var n: int
    123   #          0|11111111|00000000000000000000000
    124   #          0111|1111|1000|0000|0000|0000|0000|0000
    125   copy-to n, 0x7f800000
    126   var infinity/xmm0: float <- reinterpret n
    127   write-float-decimal-approximate s, infinity, 3
    128   check-stream-equal s, "Inf", "F - test-write-float-decimal-approximate-infinity"
    129 }
    130
    131 fn test-write-float-decimal-approximate-negative-infinity {
    132   var s-storage: (stream byte 0x10)
    133   var s/ecx: (addr stream byte) <- address s-storage
    134   var n: int
    135   copy-to n, 0xff800000
    136   var negative-infinity/xmm0: float <- reinterpret n
    137   write-float-decimal-approximate s, negative-infinity, 3
    138   check-stream-equal s, "-Inf", "F - test-write-float-decimal-approximate-negative-infinity"
    139 }
    140
    141 fn test-write-float-decimal-approximate-not-a-number {
    142   var s-storage: (stream byte 0x10)
    143   var s/ecx: (addr stream byte) <- address s-storage
    144   var n: int
    145   copy-to n, 0xffffffff  # exponent must be all 1's, and mantissa must be non-zero
    146   var nan/xmm0: float <- reinterpret n
    147   write-float-decimal-approximate s, nan, 3
    148   check-stream-equal s, "NaN", "F - test-write-float-decimal-approximate-not-a-number"
    149 }
    150
    151 fn print-float-decimal-approximate screen: (addr screen), in: float, precision: int {
    152   var s-storage: (stream byte 0x10)
    153   var s/esi: (addr stream byte) <- address s-storage
    154   write-float-decimal-approximate s, in, precision
    155   print-stream screen, s
    156 }
    157
    158 # 'precision' controls the maximum width past which we resort to scientific notation
    159 fn write-float-decimal-approximate out: (addr stream byte), in: float, precision: int {
    160   # - special names
    161   var bits/eax: int <- reinterpret in
    162   compare bits, 0
    163   {
    164     break-if-!=
    165     write out, "0"
    166     return
    167   }
    168   compare bits, 0x80000000
    169   {
    170     break-if-!=
    171     write out, "-0"
    172     return
    173   }
    174   compare bits, 0x7f800000
    175   {
    176     break-if-!=
    177     write out, "Inf"
    178     return
    179   }
    180   compare bits, 0xff800000
    181   {
    182     break-if-!=
    183     write out, "-Inf"
    184     return
    185   }
    186   var exponent/ecx: int <- copy bits
    187   exponent <- shift-right 0x17  # 23 bits of mantissa
    188   exponent <- and 0xff
    189   exponent <- subtract 0x7f
    190   compare exponent, 0x80
    191   {
    192     break-if-!=
    193     write out, "NaN"
    194     return
    195   }
    196   # - regular numbers
    197   var sign/edx: int <- copy bits
    198   sign <- shift-right 0x1f
    199   {
    200     compare sign, 1
    201     break-if-!=
    202     append-byte out, 0x2d/minus
    203   }
    204
    205   # v = 1.mantissa (in base 2) << 0x17
    206   var v/ebx: int <- copy bits
    207   v <- and 0x7fffff
    208   v <- or 0x00800000  # insert implicit 1
    209   # e = exponent - 0x17
    210   var e/ecx: int <- copy exponent
    211   e <- subtract 0x17  # move decimal place from before mantissa to after
    212
    213   # initialize buffer with decimal representation of v
    214   # unlike https://research.swtch.com/ftoa, no ascii here
    215   var buf-storage: (array byte 0x7f)
    216   var buf/edi: (addr array byte) <- address buf-storage
    217   var n/eax: int <- decimal-digits v, buf
    218   # I suspect we can do without reversing, but we'll follow https://research.swtch.com/ftoa
    219   # closely for now.
    220   reverse-digits buf, n
    221
    222   # loop if e > 0
    223   {
    224     compare e, 0
    225     break-if-<=
    226     n <- double-array-of-decimal-digits buf, n
    227     e <- decrement
    228     loop
    229   }
    230
    231   var dp/edx: int <- copy n
    232
    233   # loop if e < 0
    234   {
    235     compare e, 0
    236     break-if->=
    237     n, dp <- halve-array-of-decimal-digits buf, n, dp
    238     e <- increment
    239     loop
    240   }
    241
    242   _write-float-array-of-decimal-digits out, buf, n, dp, precision
    243 }
    244
    245 # store the decimal digits of 'n' into 'buf', units first
    246 # n must be positive
    247 fn decimal-digits n: int, _buf: (addr array byte) -> _/eax: int {
    248   var buf/edi: (addr array byte) <- copy _buf
    249   var i/ecx: int <- copy 0
    250   var curr/eax: int <- copy n
    251   var curr-byte/edx: int <- copy 0
    252   {
    253     compare curr, 0
    254     break-if-=
    255     curr, curr-byte <- integer-divide curr, 0xa
    256     var dest/ebx: (addr byte) <- index buf, i
    257     copy-byte-to *dest, curr-byte
    258     i <- increment
    259     loop
    260   }
    261   return i
    262 }
    263
    264 fn reverse-digits _buf: (addr array byte), n: int {
    265   var buf/esi: (addr array byte) <- copy _buf
    266   var left/ecx: int <- copy 0
    267   var right/edx: int <- copy n
    268   right <- decrement
    269   {
    270     compare left, right
    271     break-if->=
    272     {
    273       var l-a/ecx: (addr byte) <- index buf, left
    274       var r-a/edx: (addr byte) <- index buf, right
    275       var l/ebx: byte <- copy-byte *l-a
    276       var r/eax: byte <- copy-byte *r-a
    277       copy-byte-to *l-a, r
    278       copy-byte-to *r-a, l
    279     }
    280     left <- increment
    281     right <- decrement
    282     loop
    283   }
    284 }
    285
    286 # debug helper
    287 fn dump-digits _buf: (addr array byte), count: int, msg: (addr array byte) {
    288   var buf/edi: (addr array byte) <- copy _buf
    289   var i/ecx: int <- copy 0
    290   print-string 0, msg
    291   print-string 0, ": "
    292   {
    293     compare i, count
    294     break-if->=
    295     var curr/edx: (addr byte) <- index buf, i
    296     var curr-byte/eax: byte <- copy-byte *curr
    297     var curr-int/eax: int <- copy curr-byte
    298     print-int32-decimal 0, curr-int
    299     print-string 0, " "
    300     break-if-=
    301     i <- increment
    302     loop
    303   }
    304   print-string 0, "\n"
    305 }
    306
    307 fn double-array-of-decimal-digits _buf: (addr array byte), _n: int -> _/eax: int {
    308   var buf/edi: (addr array byte) <- copy _buf
    309   # initialize delta
    310   var delta/edx: int <- copy 0
    311   {
    312     var curr/ebx: (addr byte) <- index buf, 0
    313     var tmp/eax: byte <- copy-byte *curr
    314     compare tmp, 5
    315     break-if-<
    316     delta <- copy 1
    317   }
    318   # loop
    319   var x/eax: int <- copy 0
    320   var i/ecx: int <- copy _n
    321   i <- decrement
    322   {
    323     compare i, 0
    324     break-if-<=
    325     # x += 2*buf[i]
    326     {
    327       var tmp/ecx: (addr byte) <- index buf, i
    328       var tmp2/ecx: byte <- copy-byte *tmp
    329       x <- add tmp2
    330       x <- add tmp2
    331     }
    332     # x, buf[i+delta] = x/10, x%10
    333     {
    334       var dest-index/ecx: int <- copy i
    335       dest-index <- add delta
    336       var dest/edi: (addr byte) <- index buf, dest-index
    337       var next-digit/edx: int <- copy 0
    338       x, next-digit <- integer-divide x, 0xa
    339       copy-byte-to *dest, next-digit
    340     }
    341     #
    342     i <- decrement
    343     loop
    344   }
    345   # final patch-up
    346   var n/eax: int <- copy _n
    347   compare delta, 1
    348   {
    349     break-if-!=
    350     var curr/ebx: (addr byte) <- index buf, 0
    351     var one/edx: int <- copy 1
    352     copy-byte-to *curr, one
    353     n <- increment
    354   }
    355   return n
    356 }
    357
    358 fn halve-array-of-decimal-digits _buf: (addr array byte), _n: int, _dp: int -> _/eax: int, _/edx: int {
    359   var buf/edi: (addr array byte) <- copy _buf
    360   var n/eax: int <- copy _n
    361   var dp/edx: int <- copy _dp
    362   # initialize one side
    363   {
    364     # if buf[n-1]%2 == 0, break
    365     var right-index/ecx: int <- copy n
    366     right-index <- decrement
    367     var right-a/ecx: (addr byte) <- index buf, right-index
    368     var right/ecx: byte <- copy-byte *right-a
    369     var right-int/ecx: int <- copy right
    370     var remainder/edx: int <- copy 0
    371     {
    372       var dummy/eax: int <- copy 0
    373       dummy, remainder <- integer-divide right-int, 2
    374     }
    375     compare remainder, 0
    376     break-if-=
    377     # buf[n] = 0
    378     var next-a/ecx: (addr byte) <- index buf, n
    379     var zero/edx: byte <- copy 0
    380     copy-byte-to *next-a, zero
    381     # n++
    382     n <- increment
    383   }
    384   # initialize the other
    385   var delta/ebx: int <- copy 0
    386   var x/esi: int <- copy 0
    387   {
    388     # if buf[0] >= 2, break
    389     var left/ecx: (addr byte) <- index buf, 0
    390     var src/ecx: byte <- copy-byte *left
    391     compare src, 2
    392     break-if->=
    393     # delta, x = 1, buf[0]
    394     delta <- copy 1
    395     x <- copy src
    396     # n--
    397     n <- decrement
    398     # dp--
    399     dp <- decrement
    400   }
    401   # loop
    402   var i/ecx: int <- copy 0
    403   {
    404     compare i, n
    405     break-if->=
    406     # x = x*10 + buf[i+delta]
    407     {
    408       var ten/edx: int <- copy 0xa
    409       x <- multiply ten
    410       var src-index/edx: int <- copy i
    411       src-index <- add delta
    412       var src-a/edx: (addr byte) <- index buf, src-index
    413       var src/edx: byte <- copy-byte *src-a
    414       x <- add src
    415     }
    416     # buf[i], x = x/2, x%2
    417     {
    418       var quotient/eax: int <- copy 0
    419       var remainder/edx: int <- copy 0
    420       quotient, remainder <- integer-divide x, 2
    421       x <- copy remainder
    422       var dest/edx: (addr byte) <- index buf, i
    423       copy-byte-to *dest, quotient
    424     }
    425     #
    426     i <- increment
    427     loop
    428   }
    429   return n, dp
    430 }
    431
    432 fn _write-float-array-of-decimal-digits out: (addr stream byte), _buf: (addr array byte), n: int, dp: int, precision: int {
    433   var buf/edi: (addr array byte) <- copy _buf
    434   {
    435     compare dp, 0
    436     break-if->=
    437     _write-float-array-of-decimal-digits-in-scientific-notation out, buf, n, dp, precision
    438     return
    439   }
    440   {
    441     var dp2/eax: int <- copy dp
    442     compare dp2, precision
    443     break-if-<=
    444     _write-float-array-of-decimal-digits-in-scientific-notation out, buf, n, dp, precision
    445     return
    446   }
    447   {
    448     compare dp, 0
    449     break-if-!=
    450     append-byte out, 0x30/0
    451   }
    452   var i/eax: int <- copy 0
    453   # bounds = min(n, dp+3)
    454   var limit/edx: int <- copy dp
    455   limit <- add 3
    456   {
    457     compare limit, n
    458     break-if-<=
    459     limit <- copy n
    460   }
    461   {
    462     compare i, limit
    463     break-if->=
    464     # print '.' if necessary
    465     compare i, dp
    466     {
    467       break-if-!=
    468       append-byte out, 0x2e/decimal-point
    469     }
    470     var curr-a/ecx: (addr byte) <- index buf, i
    471     var curr/ecx: byte <- copy-byte *curr-a
    472     var curr-int/ecx: int <- copy curr
    473     curr-int <- add 0x30/0
    474     append-byte out, curr-int
    475     #
    476     i <- increment
    477     loop
    478   }
    479 }
    480
    481 fn _write-float-array-of-decimal-digits-in-scientific-notation out: (addr stream byte), _buf: (addr array byte), n: int, dp: int, precision: int {
    482   var buf/edi: (addr array byte) <- copy _buf
    483   var i/eax: int <- copy 0
    484   {
    485     compare i, n
    486     break-if->=
    487     compare i, precision
    488     break-if->=
    489     compare i, 1
    490     {
    491       break-if-!=
    492       append-byte out, 0x2e/decimal-point
    493     }
    494     var curr-a/ecx: (addr byte) <- index buf, i
    495     var curr/ecx: byte <- copy-byte *curr-a
    496     var curr-int/ecx: int <- copy curr
    497     curr-int <- add 0x30/0
    498     append-byte out, curr-int
    499     #
    500     i <- increment
    501     loop
    502   }
    503   append-byte out, 0x65/e
    504   decrement dp
    505   write-int32-decimal out, dp
    506 }
    507
    508 # follows the structure of write-float-decimal-approximate
    509 # 'precision' controls the maximum width past which we resort to scientific notation
    510 fn float-size in: float, precision: int -> _/eax: int {
    511   # - special names
    512   var bits/eax: int <- reinterpret in
    513   compare bits, 0
    514   {
    515     break-if-!=
    516     return 1  # for "0"
    517   }
    518   compare bits, 0x80000000
    519   {
    520     break-if-!=
    521     return 2  # for "-0"
    522   }
    523   compare bits, 0x7f800000
    524   {
    525     break-if-!=
    526     return 3  # for "Inf"
    527   }
    528   compare bits, 0xff800000
    529   {
    530     break-if-!=
    531     return 4  # for "-Inf"
    532   }
    533   var exponent/ecx: int <- copy bits
    534   exponent <- shift-right 0x17  # 23 bits of mantissa
    535   exponent <- and 0xff
    536   exponent <- subtract 0x7f
    537   compare exponent, 0x80
    538   {
    539     break-if-!=
    540     return 3  # for "NaN"
    541   }
    542   # - regular numbers
    543   # v = 1.mantissa (in base 2) << 0x17
    544   var v/ebx: int <- copy bits
    545   v <- and 0x7fffff
    546   v <- or 0x00800000  # insert implicit 1
    547   # e = exponent - 0x17
    548   var e/ecx: int <- copy exponent
    549   e <- subtract 0x17  # move decimal place from before mantissa to after
    550
    551   # initialize buffer with decimal representation of v
    552   var buf-storage: (array byte 0x7f)
    553   var buf/edi: (addr array byte) <- address buf-storage
    554   var n/eax: int <- decimal-digits v, buf
    555   reverse-digits buf, n
    556
    557   # loop if e > 0
    558   {
    559     compare e, 0
    560     break-if-<=
    561     n <- double-array-of-decimal-digits buf, n
    562     e <- decrement
    563     loop
    564   }
    565
    566   var dp/edx: int <- copy n
    567
    568   # loop if e < 0
    569   {
    570     compare e, 0
    571     break-if->=
    572     n, dp <- halve-array-of-decimal-digits buf, n, dp
    573     e <- increment
    574     loop
    575   }
    576
    577   compare dp, 0
    578   {
    579     break-if->=
    580     return 8  # hacky for scientific notation
    581   }
    582   {
    583     var dp2/eax: int <- copy dp
    584     compare dp2, precision
    585     break-if-<=
    586     return 8  # hacky for scientific notation
    587   }
    588
    589   # result = min(n, dp+3)
    590   var result/ecx: int <- copy dp
    591   result <- add 3
    592   {
    593     compare result, n
    594     break-if-<=
    595     result <- copy n
    596   }
    597
    598   # account for decimal point
    599   compare dp, n
    600   {
    601     break-if->=
    602     result <- increment
    603   }
    604
    605   # account for sign
    606   var sign/edx: int <- reinterpret in
    607   sign <- shift-right 0x1f
    608   {
    609     compare sign, 1
    610     break-if-!=
    611     result <- increment
    612   }
    613   return result
    614 }
    615
    616 ## helper
    617
    618 # like check-strings-equal, except array sizes don't have to match
    619 fn check-buffer-contains _buf: (addr array byte), _contents: (addr array byte), msg: (addr array byte) {
    620   var buf/esi: (addr array byte) <- copy _buf
    621   var contents/edi: (addr array byte) <- copy _contents
    622   var a/eax: boolean <- string-starts-with? buf, contents
    623   check a, msg
    624   var len/ecx: int <- length contents
    625   var len2/eax: int <- length buf
    626   compare len, len2
    627   break-if-=
    628   var c/eax: (addr byte) <- index buf, len
    629   var d/eax: byte <- copy-byte *c
    630   var e/eax: int <- copy d
    631   check-ints-equal e, 0, msg
    632 }
    633
    634 fn test-check-buffer-contains {
    635   var arr: (array byte 4)
    636   var a/esi: (addr array byte) <- address arr
    637   var b/eax: (addr byte) <- index a, 0
    638   var c/ecx: byte <- copy 0x61/a
    639   copy-byte-to *b, c
    640   check-buffer-contains a, "a", "F - test-check-buffer-contains"
    641   check-buffer-contains "a", "a", "F - test-check-buffer-contains/null"  # no null check when arrays have same length
    642 }