https://github.com/akkartik/mu/blob/main/baremetal/412render-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-render-float-decimal-normal {
 26   var screen-on-stack: screen
 27   var screen/esi: (addr screen) <- address screen-on-stack
 28   initialize-screen screen, 0x20, 5  # 32 columns should be more than enough
 29   # 0.5
 30   var half/xmm0: float <- rational 1, 2
 31   var dummy/eax: int <- render-float-decimal screen, half, 3/precision, 0, 0, 3/fg, 0/bg
 32   check-screen-row screen, 0/y, "0.5 ", "F - test-render-float-decimal-normal 0.5"
 33   # 0.25
 34   clear-screen screen
 35   var quarter/xmm0: float <- rational 1, 4
 36   var dummy/eax: int <- render-float-decimal screen, quarter, 3/precision, 0, 0, 3/fg, 0/bg
 37   check-screen-row screen, 0/y, "0.25 ", "F - test-render-float-decimal-normal 0.25"
 38   # 0.75
 39   clear-screen screen
 40   var three-quarters/xmm0: float <- rational 3, 4
 41   var dummy/eax: int <- render-float-decimal screen, three-quarters, 3/precision, 0, 0, 3/fg, 0/bg
 42   check-screen-row screen, 0/y, "0.75 ", "F - test-render-float-decimal-normal 0.75"
 43   # 0.125
 44   clear-screen screen
 45   var eighth/xmm0: float <- rational 1, 8
 46   var dummy/eax: int <- render-float-decimal screen, eighth, 3/precision, 0, 0, 3/fg, 0/bg
 47   check-screen-row screen, 0/y, "0.125 ", "F - test-render-float-decimal-normal 0.125"
 48   # 0.0625; start using scientific notation
 49   clear-screen screen
 50   var sixteenth/xmm0: float <- rational 1, 0x10
 51   var dummy/eax: int <- render-float-decimal screen, sixteenth, 3/precision, 0, 0, 3/fg, 0/bg
 52   check-screen-row screen, 0/y, "6.25e-2 ", "F - test-render-float-decimal-normal 0.0625"
 53   # sqrt(2); truncate floats with lots of digits after the decimal but not too many before
 54   clear-screen screen
 55   var two-f/xmm0: float <- rational 2, 1
 56   var sqrt-2/xmm0: float <- square-root two-f
 57   var dummy/eax: int <- render-float-decimal screen, sqrt-2, 3/precision, 0, 0, 3/fg, 0/bg
 58   check-screen-row screen, 0/y, "1.414 ", "F - test-render-float-decimal-normal √2"
 59 }
 60 
 61 # print whole integers without decimals
 62 fn test-render-float-decimal-integer {
 63   var screen-on-stack: screen
 64   var screen/esi: (addr screen) <- address screen-on-stack
 65   initialize-screen screen, 0x20, 5  # 32 columns should be more than enough
 66   # 1
 67   var one-f/xmm0: float <- rational 1, 1
 68   var dummy/eax: int <- render-float-decimal screen, one-f, 3/precision, 0, 0, 3/fg, 0/bg
 69   check-screen-row screen, 0/y, "1 ", "F - test-render-float-decimal-integer 1"
 70   # 2
 71   clear-screen screen
 72   var two-f/xmm0: float <- rational 2, 1
 73   var dummy/eax: int <- render-float-decimal screen, two-f, 3/precision, 0, 0, 3/fg, 0/bg
 74   check-screen-row screen, 0/y, "2 ", "F - test-render-float-decimal-integer 2"
 75   # 10
 76   clear-screen screen
 77   var ten-f/xmm0: float <- rational 0xa, 1
 78   var dummy/eax: int <- render-float-decimal screen, ten-f, 3/precision, 0, 0, 3/fg, 0/bg
 79   check-screen-row screen, 0/y, "10 ", "F - test-render-float-decimal-integer 10"
 80   # -10
 81   clear-screen screen
 82   var minus-ten-f/xmm0: float <- rational -0xa, 1
 83   var dummy/eax: int <- render-float-decimal screen, minus-ten-f, 3/precision, 0, 0, 3/fg, 0/bg
 84   check-screen-row screen, 0/y, "-10 ", "F - test-render-float-decimal-integer -10"
 85   # 999
 86   clear-screen screen
 87   var minus-ten-f/xmm0: float <- rational 0x3e7, 1
 88   var dummy/eax: int <- render-float-decimal screen, minus-ten-f, 3/precision, 0, 0, 3/fg, 0/bg
 89   check-screen-row screen, 0/y, "999 ", "F - test-render-float-decimal-integer 1000"
 90   # 1000 - start using scientific notation
 91   clear-screen screen
 92   var minus-ten-f/xmm0: float <- rational 0x3e8, 1
 93   var dummy/eax: int <- render-float-decimal screen, minus-ten-f, 3/precision, 0, 0, 3/fg, 0/bg
 94   check-screen-row screen, 0/y, "1.00e3 ", "F - test-render-float-decimal-integer 1000"
 95   # 100,000
 96   clear-screen screen
 97   var hundred-thousand/eax: int <- copy 0x186a0
 98   var hundred-thousand-f/xmm0: float <- convert hundred-thousand
 99   var dummy/eax: int <- render-float-decimal screen, hundred-thousand-f, 3/precision, 0, 0, 3/fg, 0/bg
100   check-screen-row screen, 0/y, "1.00e5 ", "F - test-render-float-decimal-integer 100,000"
101 }
102 
103 fn test-render-float-decimal-zero {
104   var screen-on-stack: screen
105   var screen/esi: (addr screen) <- address screen-on-stack
106   initialize-screen screen, 0x20, 5  # 32 columns should be more than enough
107   var zero: float
108   var dummy/eax: int <- render-float-decimal screen, zero, 3/precision, 0, 0, 3/fg, 0/bg
109   check-screen-row screen, 0/y, "0 ", "F - test-render-float-decimal-zero"
110 }
111 
112 fn test-render-float-decimal-negative-zero {
113   var screen-on-stack: screen
114   var screen/esi: (addr screen) <- address screen-on-stack
115   initialize-screen screen, 0x20, 5  # 32 columns should be more than enough
116   var n: int
117   copy-to n, 0x80000000
118   var negative-zero/xmm0: float <- reinterpret n
119   var dummy/eax: int <- render-float-decimal screen, negative-zero, 3/precision, 0, 0, 3/fg, 0/bg
120   check-screen-row screen, 0/y, "-0 ", "F - test-render-float-decimal-negative-zero"
121 }
122 
123 fn test-render-float-decimal-infinity {
124   var screen-on-stack: screen
125   var screen/esi: (addr screen) <- address screen-on-stack
126   initialize-screen screen, 0x20, 5  # 32 columns should be more than enough
127   var n: int
128   #          0|11111111|00000000000000000000000
129   #          0111|1111|1000|0000|0000|0000|0000|0000
130   copy-to n, 0x7f800000
131   var infinity/xmm0: float <- reinterpret n
132   var dummy/eax: int <- render-float-decimal screen, infinity, 3/precision, 0, 0, 3/fg, 0/bg
133   check-screen-row screen, 0/y, "Inf ", "F - test-render-float-decimal-infinity"
134 }
135 
136 fn test-render-float-decimal-negative-infinity {
137   var screen-on-stack: screen
138   var screen/esi: (addr screen) <- address screen-on-stack
139   initialize-screen screen, 0x20, 5  # 32 columns should be more than enough
140   var n: int
141   copy-to n, 0xff800000
142   var negative-infinity/xmm0: float <- reinterpret n
143   var dummy/eax: int <- render-float-decimal screen, negative-infinity, 3/precision, 0, 0, 3/fg, 0/bg
144   check-screen-row screen, 0/y, "-Inf ", "F - test-render-float-decimal-negative-infinity"
145 }
146 
147 fn test-render-float-decimal-not-a-number {
148   var screen-on-stack: screen
149   var screen/esi: (addr screen) <- address screen-on-stack
150   initialize-screen screen, 0x20, 5  # 32 columns should be more than enough
151   var n: int
152   copy-to n, 0xffffffff  # exponent must be all 1's, and mantissa must be non-zero
153   var nan/xmm0: float <- reinterpret n
154   var dummy/eax: int <- render-float-decimal screen, nan, 3/precision, 0, 0, 3/fg, 0/bg
155   check-screen-row screen, 0/y, "NaN ", "F - test-render-float-decimal-not-a-number"
156 }
157 
158 # 'precision' controls the maximum width past which we resort to scientific notation
159 fn render-float-decimal screen: (addr screen), in: float, precision: int, x: int, y: int, color: int, background-color: int -> _/eax: int {
160   # - special names
161   var bits/eax: int <- reinterpret in
162   compare bits, 0
163   {
164     break-if-!=
165     var new-x/eax: int <- draw-text-rightward-over-full-screen screen, "0", x, y, color, background-color
166     return new-x
167   }
168   compare bits, 0x80000000
169   {
170     break-if-!=
171     var new-x/eax: int <- draw-text-rightward-over-full-screen screen, "-0", x, y, color, background-color
172     return new-x
173   }
174   compare bits, 0x7f800000
175   {
176     break-if-!=
177     var new-x/eax: int <- draw-text-rightward-over-full-screen screen, "Inf", x, y, color, background-color
178     return new-x
179   }
180   compare bits, 0xff800000
181   {
182     break-if-!=
183     var new-x/eax: int <- draw-text-rightward-over-full-screen screen, "-Inf", x, y, color, background-color
184     return new-x
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     var new-x/eax: int <- draw-text-rightward-over-full-screen screen, "NaN", x, y, color, background-color
194     return new-x
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     draw-code-point screen, 0x2d/minus, x, y, color, background-color
203     increment x
204   }
205 
206   # v = 1.mantissa (in base 2) << 0x17
207   var v/ebx: int <- copy bits
208   v <- and 0x7fffff
209   v <- or 0x00800000  # insert implicit 1
210   # e = exponent - 0x17
211   var e/ecx: int <- copy exponent
212   e <- subtract 0x17  # move decimal place from before mantissa to after
213 
214   # initialize buffer with decimal representation of v
215   # unlike https://research.swtch.com/ftoa, no ascii here
216   var buf-storage: (array byte 0x7f)
217   var buf/edi: (addr array byte) <- address buf-storage
218   var n/eax: int <- decimal-digits v, buf
219   # I suspect we can do without reversing, but we'll follow https://research.swtch.com/ftoa
220   # closely for now.
221   reverse-digits buf, n
222 
223   # loop if e > 0
224   {
225     compare e, 0
226     break-if-<=
227     n <- double-array-of-decimal-digits buf, n
228     e <- decrement
229     loop
230   }
231 
232   var dp/edx: int <- copy n
233 
234   # loop if e < 0
235   {
236     compare e, 0
237     break-if->=
238     n, dp <- halve-array-of-decimal-digits buf, n, dp
239     e <- increment
240     loop
241   }
242 
243   var new-x/eax: int <- render-float-buffer screen, buf, n, dp, precision, x, y, color, background-color
244   return new-x
245 }
246 
247 # store the decimal digits of 'n' into 'buf', units first
248 # n must be positive
249 fn decimal-digits n: int, _buf: (addr array byte) -> _/eax: int {
250   var buf/edi: (addr array byte) <- copy _buf
251   var i/ecx: int <- copy 0
252   var curr/eax: int <- copy n
253   var curr-byte/edx: int <- copy 0
254   {
255     compare curr, 0
256     break-if-=
257     curr, curr-byte <- integer-divide curr, 0xa
258     var dest/ebx: (addr byte) <- index buf, i
259     copy-byte-to *dest, curr-byte
260     i <- increment
261     loop
262   }
263   return i
264 }
265 
266 fn reverse-digits _buf: (addr array byte), n: int {
267   var buf/esi: (addr array byte) <- copy _buf
268   var left/ecx: int <- copy 0
269   var right/edx: int <- copy n
270   right <- decrement
271   {
272     compare left, right
273     break-if->=
274     {
275       var l-a/ecx: (addr byte) <- index buf, left
276       var r-a/edx: (addr byte) <- index buf, right
277       var l/ebx: byte <- copy-byte *l-a
278       var r/eax: byte <- copy-byte *r-a
279       copy-byte-to *l-a, r
280       copy-byte-to *r-a, l
281     }
282     left <- increment
283     right <- decrement
284     loop
285   }
286 }
287 
288 fn double-array-of-decimal-digits _buf: (addr array byte), _n: int -> _/eax: int {
289   var buf/edi: (addr array byte) <- copy _buf
290   # initialize delta
291   var delta/edx: int <- copy 0
292   {
293     var curr/ebx: (addr byte) <- index buf, 0
294     var tmp/eax: byte <- copy-byte *curr
295     compare tmp, 5
296     break-if-<
297     delta <- copy 1
298   }
299   # loop
300   var x/eax: int <- copy 0
301   var i/ecx: int <- copy _n
302   i <- decrement
303   {
304     compare i, 0
305     break-if-<=
306     # x += 2*buf[i]
307     {
308       var tmp/ecx: (addr byte) <- index buf, i
309       var tmp2/ecx: byte <- copy-byte *tmp
310       x <- add tmp2
311       x <- add tmp2
312     }
313     # x, buf[i+delta] = x/10, x%10
314     {
315       var dest-index/ecx: int <- copy i
316       dest-index <- add delta
317       var dest/edi: (addr byte) <- index buf, dest-index
318       var next-digit/edx: int <- copy 0
319       x, next-digit <- integer-divide x, 0xa
320       copy-byte-to *dest, next-digit
321     }
322     #
323     i <- decrement
324     loop
325   }
326   # final patch-up
327   var n/eax: int <- copy _n
328   compare delta, 1
329   {
330     break-if-!=
331     var curr/ebx: (addr byte) <- index buf, 0
332     var one/edx: int <- copy 1
333     copy-byte-to *curr, one
334     n <- increment
335   }
336   return n
337 }
338 
339 fn halve-array-of-decimal-digits _buf: (addr array byte), _n: int, _dp: int -> _/eax: int, _/edx: int {
340   var buf/edi: (addr array byte) <- copy _buf
341   var n/eax: int <- copy _n
342   var dp/edx: int <- copy _dp
343   # initialize one side
344   {
345     # if buf[n-1]%2 == 0, break
346     var right-index/ecx: int <- copy n
347     right-index <- decrement
348     var right-a/ecx: (addr byte) <- index buf, right-index
349     var right/ecx: byte <- copy-byte *right-a
350     var right-int/ecx: int <- copy right
351     var remainder/edx: int <- copy 0
352     {
353       var dummy/eax: int <- copy 0
354       dummy, remainder <- integer-divide right-int, 2
355     }
356     compare remainder, 0
357     break-if-=
358     # buf[n] = 0
359     var next-a/ecx: (addr byte) <- index buf, n
360     var zero/edx: byte <- copy 0
361     copy-byte-to *next-a, zero
362     # n++
363     n <- increment
364   }
365   # initialize the other
366   var delta/ebx: int <- copy 0
367   var x/esi: int <- copy 0
368   {
369     # if buf[0] >= 2, break
370     var left/ecx: (addr byte) <- index buf, 0
371     var src/ecx: byte <- copy-byte *left
372     compare src, 2
373     break-if->=
374     # delta, x = 1, buf[0]
375     delta <- copy 1
376     x <- copy src
377     # n--
378     n <- decrement
379     # dp--
380     dp <- decrement
381   }
382   # loop
383   var i/ecx: int <- copy 0
384   {
385     compare i, n
386     break-if->=
387     # x = x*10 + buf[i+delta]
388     {
389       var ten/edx: int <- copy 0xa
390       x <- multiply ten
391       var src-index/edx: int <- copy i
392       src-index <- add delta
393       var src-a/edx: (addr byte) <- index buf, src-index
394       var src/edx: byte <- copy-byte *src-a
395       x <- add src
396     }
397     # buf[i], x = x/2, x%2
398     {
399       var quotient/eax: int <- copy 0
400       var remainder/edx: int <- copy 0
401       quotient, remainder <- integer-divide x, 2
402       x <- copy remainder
403       var dest/edx: (addr byte) <- index buf, i
404       copy-byte-to *dest, quotient
405     }
406     #
407     i <- increment
408     loop
409   }
410   return n, dp
411 }
412 
413 fn render-float-buffer screen: (addr screen), _buf: (addr array byte), n: int, dp: int, precision: int, x: int, y: int, color: int, background-color: int -> _/eax: int {
414   var buf/edi: (addr array byte) <- copy _buf
415   {
416     compare dp, 0
417     break-if->=
418     var new-x/eax: int <- render-float-buffer-in-scientific-notation screen, buf, n, dp, precision, x, y, color, background-color
419     return new-x
420   }
421   {
422     var dp2/eax: int <- copy dp
423     compare dp2, precision
424     break-if-<=
425     var new-x/eax: int <- render-float-buffer-in-scientific-notation screen, buf, n, dp, precision, x, y, color, background-color
426     return new-x
427   }
428   {
429     compare dp, 0
430     break-if-!=
431     draw-code-point screen, 0x30/0, x, y, color, background-color
432     increment x
433   }
434   var i/eax: int <- copy 0
435   # bounds = min(n, dp+3)
436   var limit/edx: int <- copy dp
437   limit <- add 3
438   {
439     compare limit, n
440     break-if-<=
441     limit <- copy n
442   }
443   {
444     compare i, limit
445     break-if->=
446     # print '.' if necessary
447     compare i, dp
448     {
449       break-if-!=
450       draw-code-point screen, 0x2e/decimal-point, x, y, color, background-color
451       increment x
452     }
453     var curr-a/ecx: (addr byte) <- index buf, i
454     var curr/ecx: byte <- copy-byte *curr-a
455     curr <- add 0x30/0
456     var curr-grapheme/ecx: grapheme <- copy curr
457     draw-grapheme screen, curr-grapheme, x, y, color, background-color
458     increment x
459     i <- increment
460     loop
461   }
462   return x
463 }
464 
465 fn render-float-buffer-in-scientific-notation screen: (addr screen), _buf: (addr array byte), n: int, dp: int, precision: int, x: int, y: int, color: int, background-color: int -> _/eax: int {
466   var buf/edi: (addr array byte) <- copy _buf
467   var i/eax: int <- copy 0
468   {
469     compare i, n
470     break-if->=
471     compare i, precision
472     break-if->=
473     compare i, 1
474     {
475       break-if-!=
476       draw-code-point screen, 0x2e/decimal-point, x, y, color, background-color
477       increment x
478     }
479     var curr-a/ecx: (addr byte) <- index buf, i
480     var curr/ecx: byte <- copy-byte *curr-a
481     curr <- add 0x30/0
482     var curr-grapheme/ecx: grapheme <- copy curr
483     draw-grapheme screen, curr-grapheme, x, y, color, background-color
484     increment x
485     #
486     i <- increment
487     loop
488   }
489   draw-code-point screen, 0x65/e, x, y, color, background-color
490   increment x
491   decrement dp
492   var new-x/eax: int <- copy 0
493   var new-y/ecx: int <- copy 0
494   new-x, new-y <- draw-int32-decimal-wrapping-right-then-down-over-full-screen screen, dp, x, y, color, background-color
495   return new-x
496 }
497 
498 # follows the structure of render-float-decimal
499 # 'precision' controls the maximum width past which we resort to scientific notation
500 fn float-size in: float, precision: int -> _/eax: int {
501   # - special names
502   var bits/eax: int <- reinterpret in
503   compare bits, 0
504   {
505     break-if-!=
506     return 1  # for "0"
507   }
508   compare bits, 0x80000000
509   {
510     break-if-!=
511     return 2  # for "-0"
512   }
513   compare bits, 0x7f800000
514   {
515     break-if-!=
516     return 3  # for "Inf"
517   }
518   compare bits, 0xff800000
519   {
520     break-if-!=
521     return 4  # for "-Inf"
522   }
523   var exponent/ecx: int <- copy bits
524   exponent <- shift-right 0x17  # 23 bits of mantissa
525   exponent <- and 0xff
526   exponent <- subtract 0x7f
527   compare exponent, 0x80
528   {
529     break-if-!=
530     return 3  # for "NaN"
531   }
532   # - regular numbers
533   # v = 1.mantissa (in base 2) << 0x17
534   var v/ebx: int <- copy bits
535   v <- and 0x7fffff
536   v <- or 0x00800000  # insert implicit 1
537   # e = exponent - 0x17
538   var e/ecx: int <- copy exponent
539   e <- subtract 0x17  # move decimal place from before mantissa to after
540 
541   # initialize buffer with decimal representation of v
542   var buf-storage: (array byte 0x7f)
543   var buf/edi: (addr array byte) <- address buf-storage
544   var n/eax: int <- decimal-digits v, buf
545   reverse-digits buf, n
546 
547   # loop if e > 0
548   {
549     compare e, 0
550     break-if-<=
551     n <- double-array-of-decimal-digits buf, n
552     e <- decrement
553     loop
554   }
555 
556   var dp/edx: int <- copy n
557 
558   # loop if e < 0
559   {
560     compare e, 0
561     break-if->=
562     n, dp <- halve-array-of-decimal-digits buf, n, dp
563     e <- increment
564     loop
565   }
566 
567   compare dp, 0
568   {
569     break-if->=
570     return 8  # hacky for scientific notation
571   }
572   {
573     var dp2/eax: int <- copy dp
574     compare dp2, precision
575     break-if-<=
576     return 8  # hacky for scientific notation
577   }
578 
579   # result = min(n, dp+3)
580   var result/ecx: int <- copy dp
581   result <- add 3
582   {
583     compare result, n
584     break-if-<=
585     result <- copy n
586   }
587 
588   # account for decimal point
589   compare dp, n
590   {
591     break-if->=
592     result <- increment
593   }
594 
595   # account for sign
596   var sign/edx: int <- reinterpret in
597   sign <- shift-right 0x1f
598   {
599     compare sign, 1
600     break-if-!=
601     result <- increment
602   }
603   return result
604 }