geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use ecow::EcoString;

pub(super) fn parse_literal(source: &EcoString) -> Option<f64> {
    let bytes = source.as_bytes();
    let mut index = usize::from(matches!(bytes.first(), Some(b'+' | b'-')));
    let integer_start = index;
    while bytes.get(index).is_some_and(u8::is_ascii_digit) {
        index += 1;
    }
    if index == integer_start || bytes.get(index) != Some(&b'.') {
        return None;
    }

    index += 1;
    let fraction_start = index;
    while bytes.get(index).is_some_and(u8::is_ascii_digit) {
        index += 1;
    }
    if index == fraction_start {
        return None;
    }

    if matches!(bytes.get(index), Some(b'e' | b'E')) {
        index += 1;
        if matches!(bytes.get(index), Some(b'+' | b'-')) {
            index += 1;
        }
        let exponent_start = index;
        while bytes.get(index).is_some_and(u8::is_ascii_digit) {
            index += 1;
        }
        if index == exponent_start {
            return None;
        }
    }

    (index == bytes.len())
        .then(|| source.parse::<f64>().ok())
        .flatten()
}

pub(super) fn format(value: f64) -> EcoString {
    let mut output = format!("{value:?}");
    if let Some(exponent) = output.find('e')
        && !output[..exponent].contains('.')
    {
        output.insert_str(exponent, ".0");
    }
    output.into()
}

#[cfg(test)]
mod tests {
    use super::{format, parse_literal};

    #[test]
    fn parses_only_the_official_decimal_and_exponent_grammar() {
        let accepted = [
            ("0.0", 0.0),
            ("-0.0", -0.0),
            ("+12.5", 12.5),
            ("1.25e3", 1250.0),
            ("1.25E-2", 0.0125),
        ];
        for (source, expected) in accepted {
            assert_eq!(parse_literal(&source.into()), Some(expected));
        }

        for source in [
            "", "+", "1", ".5", "1.", "1.0e", "1.0e+", "1.0x", " 1.0", "1.0 ", "NaN", "inf",
        ] {
            assert_eq!(parse_literal(&source.into()), None, "{source}");
        }
    }

    #[test]
    fn formats_floats_with_explicit_float_mantissas() {
        assert_eq!(format(1.0), "1.0");
        assert_eq!(format(-0.0), "-0.0");
        assert_eq!(format(1e100), "1.0e100");
        assert_eq!(format(1.5e100), "1.5e100");
        assert_eq!(format(f64::INFINITY), "inf");
        assert_eq!(format(f64::NEG_INFINITY), "-inf");
        assert_eq!(format(f64::NAN), "NaN");
    }
}