tsz-solver 0.1.8

TypeScript type solver for the tsz compiler
Documentation
//! Shared utility functions for the solver module.
//!
//! This module contains common utilities used across multiple solver components
//! to avoid code duplication.

use crate::db::TypeDatabase;
use crate::types::{ObjectShapeId, PropertyInfo, PropertyLookup, TypeId};
use tsz_common::interner::Atom;

/// Checks if a property name is numeric by resolving the atom and checking its string representation.
///
/// This function consolidates the previously duplicated `is_numeric_property_name` implementations
/// from operations.rs, evaluate.rs, subtype.rs, and infer.rs.
pub fn is_numeric_property_name(interner: &dyn TypeDatabase, name: Atom) -> bool {
    let prop_name = interner.resolve_atom_ref(name);
    is_numeric_literal_name(prop_name.as_ref())
}

/// Checks if a string represents a numeric literal name.
///
/// Returns `true` for:
/// - "`NaN`", "Infinity", "-Infinity"
/// - Numeric strings that round-trip correctly through JavaScript's number-to-string conversion
pub fn is_numeric_literal_name(name: &str) -> bool {
    if name == "NaN" || name == "Infinity" || name == "-Infinity" {
        return true;
    }

    let value: f64 = match name.parse() {
        Ok(value) => value,
        Err(_) => return false,
    };
    if !value.is_finite() {
        return false;
    }

    js_number_to_string(value) == name
}

/// Canonicalizes a numeric property name to its JavaScript canonical form.
///
/// If the input parses as a finite number, returns `Some(canonical_form)` where
/// `canonical_form` matches JavaScript's `Number.prototype.toString()`.
/// For example, `"1."`, `"1.0"`, and `"1"` all canonicalize to `"1"`.
/// Returns `None` if the name is not a numeric literal.
pub fn canonicalize_numeric_name(name: &str) -> Option<String> {
    let value: f64 = tsz_common::numeric::parse_numeric_literal_value(name)?;
    if !value.is_finite() && !value.is_nan() {
        return None;
    }
    Some(js_number_to_string(value))
}

/// Converts a JavaScript number to its string representation.
///
/// This matches JavaScript's `Number.prototype.toString()` behavior for proper
/// numeric literal name checking.
fn js_number_to_string(value: f64) -> String {
    if value.is_nan() {
        return "NaN".to_string();
    }
    if value == 0.0 {
        return "0".to_string();
    }
    if value.is_infinite() {
        return if value.is_sign_negative() {
            "-Infinity".to_string()
        } else {
            "Infinity".to_string()
        };
    }

    let abs = value.abs();
    if !(1e-6..1e21).contains(&abs) {
        let mut formatted = format!("{value:e}");
        if let Some(split) = formatted.find('e') {
            let (mantissa, exp) = formatted.split_at(split);
            let exp_digits = exp.strip_prefix('e').unwrap_or("");
            let (sign, digits) = if let Some(digits) = exp_digits.strip_prefix('-') {
                ('-', digits)
            } else {
                ('+', exp_digits)
            };
            let trimmed = digits.trim_start_matches('0');
            let digits = if trimmed.is_empty() { "0" } else { trimmed };
            formatted = format!("{mantissa}e{sign}{digits}");
        }
        return formatted;
    }

    let formatted = value.to_string();
    if formatted == "-0" {
        "0".to_string()
    } else {
        formatted
    }
}

/// Reduces a vector of types to a union, single type, or NEVER.
///
/// This helper eliminates the common pattern:
/// ```ignore
/// if types.is_empty() {
///     TypeId::NEVER
/// } else if types.len() == 1 {
///     types[0]
/// } else {
///     db.union(types)
/// }
/// ```
///
/// # Examples
///
/// ```ignore
/// let narrowed = union_or_single(db, filtered_members);
/// ```
pub fn union_or_single(db: &dyn TypeDatabase, types: Vec<TypeId>) -> TypeId {
    match types.len() {
        0 => TypeId::NEVER,
        1 => types[0],
        _ => db.union(types),
    }
}

/// Reduces a vector of types to an intersection, single type, or NEVER.
///
/// This helper eliminates the common pattern:
/// ```ignore
/// if types.is_empty() {
///     TypeId::NEVER
/// } else if types.len() == 1 {
///     types[0]
/// } else {
///     db.intersection(types)
/// }
/// ```
///
/// # Examples
///
/// ```ignore
/// let narrowed = intersection_or_single(db, instance_types);
/// ```
pub fn intersection_or_single(db: &dyn TypeDatabase, types: Vec<TypeId>) -> TypeId {
    match types.len() {
        0 => TypeId::NEVER,
        1 => types[0],
        _ => db.intersection(types),
    }
}

/// Extension trait for `TypeId` with chainable methods for common operations.
///
/// This trait provides idiomatic Rust methods to reduce boilerplate when
/// working with `TypeId` values. Methods are designed to be chainable and
/// composable with iterator combinators.
///
/// # Examples
///
/// ```ignore
/// // Filter out NEVER types in a map operation
/// .filter_map(|&id| some_operation(id).non_never())
/// ```
pub trait TypeIdExt {
    /// Returns Some(self) if self is not NEVER, otherwise None.
    ///
    /// This is useful for `filter_map` chains where you want to skip NEVER results.
    fn non_never(self) -> Option<Self>
    where
        Self: Sized;
}

impl TypeIdExt for TypeId {
    #[inline]
    fn non_never(self) -> Option<Self> {
        (self != Self::NEVER).then_some(self)
    }
}

/// Look up a property by name, using the cached property index if available.
///
/// This consolidates the duplicated `lookup_property` implementations from
/// `subtype_rules/objects.rs` and `infer_bct.rs`.
pub fn lookup_property<'props>(
    db: &dyn TypeDatabase,
    props: &'props [PropertyInfo],
    shape_id: Option<ObjectShapeId>,
    name: Atom,
) -> Option<&'props PropertyInfo> {
    if let Some(shape_id) = shape_id {
        match db.object_property_index(shape_id, name) {
            PropertyLookup::Found(idx) => return props.get(idx),
            PropertyLookup::NotFound => return None,
            PropertyLookup::Uncached => {}
        }
    }
    props
        .binary_search_by_key(&name, |p| p.name)
        .ok()
        .map(|idx| &props[idx])
}

#[cfg(test)]
#[path = "../tests/utils_tests.rs"]
mod tests;