Skip to content

Commit 91346f5

Browse files
HerschelToad06
andcommitted
avm1: Format floating-point numbers
Co-authored-by: Toad06 <[email protected]>
1 parent 478f970 commit 91346f5

File tree

2 files changed

+235
-35
lines changed

2 files changed

+235
-35
lines changed

core/src/avm1/value.rs

Lines changed: 235 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ use crate::avm1::error::Error;
33
use crate::avm1::object::value_object::ValueObject;
44
use crate::avm1::{Object, TObject};
55
use crate::ecma_conversions::{
6-
f64_to_string, f64_to_wrapping_i16, f64_to_wrapping_i32, f64_to_wrapping_u16,
7-
f64_to_wrapping_u32, f64_to_wrapping_u8,
6+
f64_to_wrapping_i16, f64_to_wrapping_i32, f64_to_wrapping_u16, f64_to_wrapping_u32,
7+
f64_to_wrapping_u8,
88
};
99
use crate::string::{AvmString, Integer, WStr};
1010
use gc_arena::Collect;
11-
use std::borrow::Cow;
12-
use std::num::Wrapping;
11+
use std::{borrow::Cow, io::Write, num::Wrapping};
1312

1413
#[derive(Debug, Clone, Copy, Collect)]
1514
#[collect(no_drop)]
@@ -496,6 +495,218 @@ impl<'gc> Value<'gc> {
496495
}
497496
}
498497

498+
/// Converts an `f64` to a String with (hopefully) the same output as Flash AVM1.
499+
/// 15 digits are displayed (not including leading 0s in a decimal <1).
500+
/// Exponential notation is used for numbers <= 1e-5 and >= 1e15.
501+
/// Rounding done with ties rounded away from zero.
502+
/// NAN returns `"NaN"`, and infinity returns `"Infinity"`.
503+
#[allow(clippy::approx_constant)]
504+
fn f64_to_string(mut n: f64) -> Cow<'static, str> {
505+
if n.is_nan() {
506+
Cow::Borrowed("NaN")
507+
} else if n == f64::INFINITY {
508+
Cow::Borrowed("Infinity")
509+
} else if n == f64::NEG_INFINITY {
510+
Cow::Borrowed("-Infinity")
511+
} else if n == 0.0 {
512+
Cow::Borrowed("0")
513+
} else if n >= -2147483648.0 && n <= 2147483647.0 && n.fract() == 0.0 {
514+
// Fast path for integers.
515+
(n as i32).to_string().into()
516+
} else {
517+
// AVM1 f64 -> String (also trying to reproduce bugs).
518+
// Flash Player's AVM1 does this in a straightforward way, shifting the float into the
519+
// range of [0.0, 10.0), repeatedly multiplying by 10 to extract digits, and then finally
520+
// rounding the result. However, the rounding is buggy, when carrying 9.999 -> 10.
521+
// For example, -9999999999999999.0 results in "-e+16".
522+
let mut buf: Vec<u8> = Vec::with_capacity(25);
523+
let is_negative = if n < 0.0 {
524+
n = -n;
525+
buf.push(b'-');
526+
true
527+
} else {
528+
false
529+
};
530+
531+
// Extract base-2 exponent from double-precision float (11 bits, biased by 1023).
532+
const MANTISSA_BITS: u64 = 52;
533+
const EXPONENT_MASK: u64 = 0x7ff;
534+
const EXPONENT_BIAS: i32 = 1023;
535+
let mut exp_base2: i32 =
536+
((n.to_bits() >> MANTISSA_BITS) & EXPONENT_MASK) as i32 - EXPONENT_BIAS;
537+
538+
if exp_base2 == -EXPONENT_BIAS {
539+
// Subnormal float; scale back into normal range and retry getting the exponent.
540+
const NORMAL_SCALE: f64 = 1.801439850948198e16; // 2^54
541+
let n = n * NORMAL_SCALE;
542+
exp_base2 =
543+
((n.to_bits() >> MANTISSA_BITS) & EXPONENT_MASK) as i32 - EXPONENT_BIAS - 54;
544+
}
545+
546+
// Convert to base-10 exponent.
547+
const LOG10_2: f64 = 0.301029995663981; // log_10(2) value (less precise than Rust's f64::LOG10_2).
548+
let mut exp = f64::round(f64::from(exp_base2) * LOG10_2) as i32;
549+
550+
// Calculate `value * 10^exp` through repeated multiplication or division.
551+
fn decimal_shift(mut value: f64, mut exp: i32) -> f64 {
552+
let mut base: f64 = 10.0;
553+
// The multiply and division branches are intentionally separate to match Flash's behavior.
554+
if exp > 0 {
555+
while exp > 0 {
556+
if (exp & 1) != 0 {
557+
value *= base;
558+
}
559+
exp >>= 1;
560+
base *= base;
561+
}
562+
} else {
563+
exp = -exp;
564+
while exp > 0 {
565+
if (exp & 1) != 0 {
566+
value /= base;
567+
}
568+
exp >>= 1;
569+
base *= base;
570+
}
571+
};
572+
value
573+
}
574+
575+
// Shift the decimal value so that it's in the range of [0.0, 10.0).
576+
let mut mantissa: f64 = decimal_shift(n, -exp);
577+
578+
// The exponent calculation can be off by 1; try the next exponent if so.
579+
if mantissa as i32 == 0 {
580+
exp -= 1;
581+
mantissa = decimal_shift(n, -exp);
582+
}
583+
if mantissa as i32 >= 10 {
584+
exp += 1;
585+
mantissa = decimal_shift(n, -exp);
586+
}
587+
588+
// Generates the next digit character.
589+
let mut digit = || {
590+
let digit: i32 = mantissa as i32;
591+
debug_assert!(digit >= 0 && digit < 10);
592+
mantissa -= f64::from(digit);
593+
mantissa *= 10.0;
594+
b'0' + digit as u8
595+
};
596+
597+
const MAX_DECIMAL_PLACES: i32 = 15;
598+
match exp {
599+
15.. => {
600+
// 1.2345e+15
601+
// This case fails to push an extra 0 to handle the rounding 9.9999 -> 10, which
602+
// causes the -9999999999999999.0 -> "-e+16" bug later.
603+
buf.push(digit());
604+
buf.push(b'.');
605+
for _ in 0..MAX_DECIMAL_PLACES - 1 {
606+
buf.push(digit());
607+
}
608+
}
609+
0..=14 => {
610+
// 12345.678901234
611+
buf.push(b'0');
612+
for _ in 0..=exp {
613+
buf.push(digit());
614+
}
615+
buf.push(b'.');
616+
for _ in 0..MAX_DECIMAL_PLACES - exp - 1 {
617+
buf.push(digit());
618+
}
619+
exp = 0;
620+
}
621+
-5..=-1 => {
622+
// 0.0012345678901234
623+
buf.extend_from_slice(b"00.");
624+
buf.resize(buf.len() + (-exp) as usize - 1, b'0');
625+
for _ in 0..MAX_DECIMAL_PLACES {
626+
buf.push(digit());
627+
}
628+
exp = 0;
629+
}
630+
_ => {
631+
// 1.345e-15
632+
buf.push(b'0');
633+
let n = digit();
634+
if n != 0 {
635+
buf.push(n);
636+
}
637+
buf.push(b'.');
638+
for _ in 0..MAX_DECIMAL_PLACES - 1 {
639+
buf.push(digit());
640+
}
641+
}
642+
};
643+
644+
// Rounding: Peek at the next generated digit and round accordingly.
645+
// Ties round away from zero.
646+
if digit() >= b'5' {
647+
// Add 1 to the right-most digit, carrying if we hit a 9.
648+
for c in buf.iter_mut().rev() {
649+
if *c == b'9' {
650+
*c = b'0';
651+
} else if *c >= b'0' {
652+
*c += 1;
653+
break;
654+
}
655+
}
656+
}
657+
658+
// Trim any trailing zeros and decimal point.
659+
while buf.last() == Some(&b'0') {
660+
buf.pop();
661+
}
662+
if buf.last() == Some(&b'.') {
663+
buf.pop();
664+
}
665+
666+
let mut start = 0;
667+
if exp != 0 {
668+
// Write exponent (e+###).
669+
670+
// Lots of band-aids here to attempt to clean up the rounding above.
671+
// Negative values are not correctly handled in the Flash Player, causing several bugs.
672+
// PLAYER-SPECIFIC: I think these checks were added in Flash Player 6.
673+
// Trim leading zeros.
674+
let pos = buf.iter().position(|&c| c != b'0').unwrap_or(buf.len());
675+
if pos != 0 {
676+
buf.copy_within(pos.., 0);
677+
buf.truncate(buf.len() - pos);
678+
}
679+
if buf.is_empty() {
680+
// Fix up 9.99999 being rounded to 0.00000 when there is no space for the carried 1.
681+
// If we have no digits, the value was all 0s that were trimmed, so round to 1.
682+
buf.push(b'1');
683+
exp += 1;
684+
} else {
685+
// Fix up 100e15 to 1e17.
686+
let pos = buf.iter().rposition(|&c| c != b'0').unwrap_or_default();
687+
if pos == 0 {
688+
exp += buf.len() as i32 - 1;
689+
buf.truncate(1);
690+
}
691+
}
692+
let _ = write!(&mut buf, "e{:+}", exp);
693+
}
694+
695+
// One final band-aid to eliminate any leading zeros.
696+
let i = if is_negative { 1 } else { 0 };
697+
if buf.get(i) == Some(&b'0') && buf.get(i + 1) != Some(&b'.') {
698+
if i > 0 {
699+
buf[i] = buf[i - 1];
700+
}
701+
start = 1;
702+
}
703+
704+
// SAFETY: Buffer is guaranteed to only contain ASCII digits.
705+
let s = unsafe { std::str::from_utf8_unchecked(&buf[start..]) };
706+
s.to_string().into()
707+
}
708+
}
709+
499710
#[cfg(test)]
500711
#[allow(clippy::unreadable_literal)] // Large numeric literals in tests
501712
mod test {
@@ -768,5 +979,25 @@ mod test {
768979
assert_eq!(f64_to_string(-1e-5), "-0.00001");
769980
assert_eq!(f64_to_string(0.999e-5), "9.99e-6");
770981
assert_eq!(f64_to_string(-0.999e-5), "-9.99e-6");
982+
assert_eq!(f64_to_string(0.19999999999999996), "0.2");
983+
assert_eq!(f64_to_string(-0.19999999999999996), "-0.2");
984+
assert_eq!(f64_to_string(100000.12345678912), "100000.123456789");
985+
assert_eq!(f64_to_string(-100000.12345678912), "-100000.123456789");
986+
assert_eq!(f64_to_string(0.8000000000000005), "0.800000000000001");
987+
assert_eq!(f64_to_string(-0.8000000000000005), "-0.800000000000001");
988+
assert_eq!(f64_to_string(0.8300000000000005), "0.83");
989+
assert_eq!(f64_to_string(1e-320), "9.99988867182684e-321");
990+
assert_eq!(f64_to_string(f64::MIN), "-1.79769313486231e+308");
991+
assert_eq!(f64_to_string(f64::MIN_POSITIVE), "2.2250738585072e-308");
992+
assert_eq!(f64_to_string(f64::MAX), "1.79769313486231e+308");
993+
assert_eq!(f64_to_string(5e-324), "4.94065645841247e-324");
994+
assert_eq!(f64_to_string(9.999999999999999), "10");
995+
assert_eq!(f64_to_string(-9.999999999999999), "-10");
996+
assert_eq!(f64_to_string(9999999999999996.0), "1e+16");
997+
assert_eq!(f64_to_string(-9999999999999996.0), "-e+16"); // wat
998+
assert_eq!(f64_to_string(0.000009999999999999996), "1e-5");
999+
assert_eq!(f64_to_string(-0.000009999999999999996), "-10e-6");
1000+
assert_eq!(f64_to_string(0.00009999999999999996), "0.0001");
1001+
assert_eq!(f64_to_string(-0.00009999999999999996), "-0.0001");
7711002
}
7721003
}

core/src/ecma_conversions.rs

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,5 @@
11
//! ECMA-262 compliant numerical conversions
22
3-
use std::borrow::Cow;
4-
5-
/// Converts an `f64` to a String with (hopefully) the same output as Flash.
6-
/// For example, NAN returns `"NaN"`, and infinity returns `"Infinity"`.
7-
pub fn f64_to_string(n: f64) -> Cow<'static, str> {
8-
if n.is_nan() {
9-
Cow::Borrowed("NaN")
10-
} else if n == f64::INFINITY {
11-
Cow::Borrowed("Infinity")
12-
} else if n == f64::NEG_INFINITY {
13-
Cow::Borrowed("-Infinity")
14-
} else if n != 0.0 && (n.abs() >= 1e15 || n.abs() < 1e-5) {
15-
// Exponential notation.
16-
// Cheating a bit here; Flash always put a sign in front of the exponent, e.g. 1e+15.
17-
// Can't do this with rust format params, so shove it in there manually.
18-
let mut s = format!("{:e}", n);
19-
if let Some(i) = s.find('e') {
20-
if s.as_bytes().get(i + 1) != Some(&b'-') {
21-
s.insert(i + 1, '+');
22-
}
23-
}
24-
Cow::Owned(s)
25-
} else if n == 0.0 {
26-
// As of Rust nightly 4/13, Rust can return "-0" for f64, which Flash doesn't want.
27-
Cow::Borrowed("0")
28-
} else {
29-
// Normal number.
30-
Cow::Owned(n.to_string())
31-
}
32-
}
33-
343
/// Converts an `f64` to a `u8` with ECMAScript `ToUInt8` wrapping behavior.
354
/// The value will be wrapped modulo 2^8.
365
pub fn f64_to_wrapping_u8(n: f64) -> u8 {

0 commit comments

Comments
 (0)