bamts-runtime 0.1.2

Execution runtime and built-in intrinsics for BamTS
Documentation
use std::collections::BTreeMap;

use bamts_bytecode::EcmaString;
use bamts_native::Value;

use super::{
    allocate_string, define_data, install_function, range_error, type_error, value_number,
};
use crate::intrinsics::{BuiltinHandler, BuiltinOutcome, BuiltinTable};
use crate::{EvalFailure, HeapEntry, Host, Machine, PropertyKey, PropertyMap};

pub(super) fn install<H: Host>(
    heap: &mut Vec<HeapEntry>,
    globals: &mut BTreeMap<EcmaString, Value>,
    builtins: &mut BuiltinTable<H>,
) {
    let prototype = super::super::ordinary_prototype(heap, builtins.object_prototype());
    let constructor = install_function(heap, builtins, "Date", 7, constructor::<H>);
    builtins.set_constructor_prototype(heap, constructor, prototype);
    let now = install_function(heap, builtins, "now", 0, now::<H>);
    let HeapEntry::NativeFunction { properties, .. } = &mut heap[super::heap_index(constructor)]
    else {
        unreachable!()
    };
    properties.insert(
        PropertyKey::Named(EcmaString::from_utf8("now")),
        super::builtin_property(now),
    );
    for (name, handler) in [
        ("getTime", get_time::<H> as BuiltinHandler<H>),
        ("valueOf", get_time::<H>),
        ("toISOString", to_iso_string::<H>),
    ] {
        let function = install_function(heap, builtins, name, 0, handler);
        define_data(heap, prototype, name, function);
    }
    globals.insert(EcmaString::from_utf8("Date"), constructor);
}

fn constructor<H: Host>(
    machine: &mut Machine<'_, H>,
    _this: Value,
    args: &[Value],
    constructing: bool,
) -> Result<BuiltinOutcome, EvalFailure> {
    let milliseconds = if args.len() == 1 {
        let value = args[0];
        match machine.string_value(value) {
            Some(text) => parse_iso_date(&text).unwrap_or(f64::NAN),
            None => value_number(machine.to_number(value)?),
        }
    } else if let Some(value) = args.first().copied() {
        value_number(machine.to_number(value)?)
    } else {
        machine.host.now_ms() as f64
    };
    if !constructing {
        let text = iso_string(milliseconds).unwrap_or_else(|| "Invalid Date".to_owned());
        return Ok(BuiltinOutcome::Value(allocate_string(
            machine,
            EcmaString::from_utf8(&text),
        )?));
    }
    let constructor = machine.intrinsics.global("Date").expect("Date installed");
    let prototype = machine.get_named_property(constructor, "prototype")?;
    let object = machine
        .allocate(HeapEntry::Date {
            time: milliseconds,
            properties: PropertyMap::default(),
            prototype: Some(prototype),
            extensible: true,
        })
        .map_err(EvalFailure::Runtime)?;
    Ok(BuiltinOutcome::Value(object))
}

fn now<H: Host>(
    machine: &mut Machine<'_, H>,
    _this: Value,
    _args: &[Value],
    _constructing: bool,
) -> Result<BuiltinOutcome, EvalFailure> {
    Ok(BuiltinOutcome::Value(crate::number_value(
        machine.host.now_ms() as f64,
    )))
}

fn get_time<H: Host>(
    machine: &mut Machine<'_, H>,
    this: Value,
    _args: &[Value],
    _constructing: bool,
) -> Result<BuiltinOutcome, EvalFailure> {
    Ok(BuiltinOutcome::Value(crate::number_value(date_time(
        machine, this,
    )?)))
}

fn to_iso_string<H: Host>(
    machine: &mut Machine<'_, H>,
    this: Value,
    _args: &[Value],
    _constructing: bool,
) -> Result<BuiltinOutcome, EvalFailure> {
    let milliseconds = date_time(machine, this)?;
    let text = iso_string(milliseconds).ok_or_else(|| range_error("Invalid time value"))?;
    Ok(BuiltinOutcome::Value(allocate_string(
        machine,
        EcmaString::from_utf8(&text),
    )?))
}

fn date_time<H: Host>(machine: &Machine<'_, H>, this: Value) -> Result<f64, EvalFailure> {
    let Some(index) = machine.runtime_slot(this).map_err(EvalFailure::Runtime)? else {
        return Err(type_error("Date method called on incompatible receiver"));
    };
    let HeapEntry::Date { time, .. } = &machine.heap[index] else {
        return Err(type_error("Date method called on incompatible receiver"));
    };
    Ok(*time)
}

fn iso_string(milliseconds: f64) -> Option<String> {
    if !milliseconds.is_finite() || milliseconds.abs() > 8_640_000_000_000_000.0 {
        return None;
    }
    let millis = milliseconds.trunc() as i64;
    let seconds = millis.div_euclid(1000);
    let millisecond = millis.rem_euclid(1000);
    let days = seconds.div_euclid(86_400);
    let day_seconds = seconds.rem_euclid(86_400);
    let (year, month, day) = civil_from_days(days);
    let hour = day_seconds / 3600;
    let minute = day_seconds % 3600 / 60;
    let second = day_seconds % 60;
    let year_text = if (0..=9999).contains(&year) {
        format!("{year:04}")
    } else if year < 0 {
        format!("-{abs:06}", abs = year.unsigned_abs())
    } else {
        format!("+{year:06}")
    };
    Some(format!(
        "{year_text}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millisecond:03}Z"
    ))
}

fn parse_iso_date(text: &EcmaString) -> Option<f64> {
    let units = text.as_units();
    if units.len() != 24
        || units[4] != u16::from(b'-')
        || units[7] != u16::from(b'-')
        || units[10] != u16::from(b'T')
        || units[13] != u16::from(b':')
        || units[16] != u16::from(b':')
        || units[19] != u16::from(b'.')
        || units[23] != u16::from(b'Z')
    {
        return None;
    }

    let year = decimal_component(units, 0, 4)?;
    let month = decimal_component(units, 5, 2)?;
    let day = decimal_component(units, 8, 2)?;
    let hour = decimal_component(units, 11, 2)?;
    let minute = decimal_component(units, 14, 2)?;
    let second = decimal_component(units, 17, 2)?;
    let millisecond = decimal_component(units, 20, 3)?;
    if !(1..=12).contains(&month)
        || day == 0
        || day > days_in_month(year, month)
        || hour > 23
        || minute > 59
        || second > 59
    {
        return None;
    }

    let days = days_from_civil(year, month, day);
    Some(
        (days * 86_400_000 + hour * 3_600_000 + minute * 60_000 + second * 1_000 + millisecond)
            as f64,
    )
}

fn decimal_component(units: &[u16], start: usize, len: usize) -> Option<i64> {
    let mut value = 0_i64;
    for &unit in &units[start..start + len] {
        if !(u16::from(b'0')..=u16::from(b'9')).contains(&unit) {
            return None;
        }
        value = value * 10 + i64::from(unit - u16::from(b'0'));
    }
    Some(value)
}

fn days_in_month(year: i64, month: i64) -> i64 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
        2 => 28,
        _ => 0,
    }
}

fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
    let year = year - i64::from(month <= 2);
    let era = year.div_euclid(400);
    let year_of_era = year - era * 400;
    let month_prime = month + if month > 2 { -3 } else { 9 };
    let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
    era * 146_097 + year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year - 719_468
}

fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64) {
    let z = days_since_epoch + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let day_of_era = z - era * 146_097;
    let year_of_era =
        (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
    let mut year = year_of_era + era * 400;
    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
    let month_prime = (5 * day_of_year + 2) / 153;
    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
    year += i64::from(month <= 2);
    (year, month, day)
}

#[cfg(test)]
mod tests {
    use super::{iso_string, parse_iso_date};
    use bamts_bytecode::EcmaString;

    #[test]
    fn formats_node_24_iso_dates() {
        assert_eq!(iso_string(0.0).as_deref(), Some("1970-01-01T00:00:00.000Z"));
        assert_eq!(
            iso_string(-1.0).as_deref(),
            Some("1969-12-31T23:59:59.999Z")
        );
        assert_eq!(
            iso_string(1_704_067_200_123.0).as_deref(),
            Some("2024-01-01T00:00:00.123Z")
        );
    }

    #[test]
    fn parses_pinned_utc_iso_dates_without_utf8_flattening() {
        for (text, milliseconds) in [
            ("1970-01-01T00:00:00.000Z", 0.0),
            ("2024-02-29T23:59:59.123Z", 1_709_251_199_123.0),
            ("2026-01-01T00:00:00.000Z", 1_767_225_600_000.0),
        ] {
            assert_eq!(
                parse_iso_date(&EcmaString::from_utf8(text)),
                Some(milliseconds),
                "{text}"
            );
        }

        for text in [
            "2024-02-30T00:00:00.000Z",
            "2023-02-29T00:00:00.000Z",
            "2024-01-01T24:00:00.000Z",
            "2024-01-01T00:60:00.000Z",
            "2024-01-01T00:00:60.000Z",
            "2024-01-01T00:00:00.000+00:00",
            "2024-01-01T00:00:00.00Z",
            "2024-01-01 00:00:00.000Z",
        ] {
            assert_eq!(parse_iso_date(&EcmaString::from_utf8(text)), None, "{text}");
        }
    }
}