use crate::gc::trace::{Trace, Tracer};
use crate::objects::js_object::JsObject;
use crate::objects::map::InstanceType;
use crate::objects::value::JsValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElementKind {
PackedSmi,
PackedDouble,
PackedElements,
HoleSmi,
HoleDouble,
HoleElements,
}
impl ElementKind {
fn to_holey(self) -> Self {
match self {
Self::PackedSmi | Self::HoleSmi => Self::HoleSmi,
Self::PackedDouble | Self::HoleDouble => Self::HoleDouble,
Self::PackedElements | Self::HoleElements => Self::HoleElements,
}
}
pub fn is_holey(self) -> bool {
matches!(self, Self::HoleSmi | Self::HoleDouble | Self::HoleElements)
}
fn widen_for_value(self, value: &JsValue) -> Self {
match (self, value) {
(k, JsValue::Smi(_)) => k,
(Self::PackedSmi, JsValue::HeapNumber(_)) => Self::PackedDouble,
(Self::HoleSmi, JsValue::HeapNumber(_)) => Self::HoleDouble,
(k, JsValue::HeapNumber(_)) => k,
(Self::PackedSmi | Self::PackedDouble | Self::PackedElements, _) => {
Self::PackedElements
}
_ => Self::HoleElements,
}
}
}
pub struct JsArray {
object: JsObject,
element_kind: ElementKind,
}
impl JsArray {
pub fn new() -> Self {
Self {
object: JsObject::new_with_instance_type(InstanceType::JsArray),
element_kind: ElementKind::PackedSmi,
}
}
pub fn element_kind(&self) -> ElementKind {
self.element_kind
}
pub fn length(&self) -> u32 {
self.object.elements_length() as u32
}
pub fn get(&self, index: u32) -> JsValue {
self.object.get_element(index as usize)
}
pub fn set(&mut self, index: u32, value: JsValue) {
let current_len = self.object.elements_length() as u32;
if index > current_len {
self.element_kind = self.element_kind.to_holey();
}
self.element_kind = self.element_kind.widen_for_value(&value);
self.object.set_element(index as usize, value);
}
pub fn push(&mut self, value: JsValue) -> u32 {
let idx = self.object.elements_length();
self.element_kind = self.element_kind.widen_for_value(&value);
self.object.set_element(idx, value);
self.object.elements_length() as u32
}
pub fn pop(&mut self) -> JsValue {
let len = self.object.elements_length();
if len == 0 {
return JsValue::Undefined;
}
let value = self.object.get_element(len - 1);
self.object.truncate_elements(len - 1);
value
}
pub fn as_object(&self) -> &JsObject {
&self.object
}
pub fn as_object_mut(&mut self) -> &mut JsObject {
&mut self.object
}
pub fn elements_as_slice(&self) -> &[JsValue] {
self.object.elements_as_slice()
}
pub fn elements_as_mut_slice(&mut self) -> &mut [JsValue] {
self.object.elements_as_mut_slice()
}
pub fn is_hole_at(&self, index: u32) -> bool {
let idx = index as usize;
let slice = self.elements_as_slice();
idx >= slice.len() || slice[idx].is_the_hole()
}
}
impl Default for JsArray {
fn default() -> Self {
Self::new()
}
}
impl Trace for JsArray {
fn trace(&self, tracer: &mut Tracer) {
self.object.trace(tracer);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_array_starts_packed_smi() {
let arr = JsArray::new();
assert_eq!(arr.element_kind(), ElementKind::PackedSmi);
}
#[test]
fn test_push_smi_stays_packed_smi() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1));
arr.push(JsValue::Smi(2));
assert_eq!(arr.element_kind(), ElementKind::PackedSmi);
}
#[test]
fn test_push_heap_number_transitions_to_packed_double() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1));
arr.push(JsValue::HeapNumber(3.14));
assert_eq!(arr.element_kind(), ElementKind::PackedDouble);
}
#[test]
fn test_push_string_transitions_to_packed_elements() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1));
arr.push(JsValue::String("hello".to_string().into()));
assert_eq!(arr.element_kind(), ElementKind::PackedElements);
}
#[test]
fn test_push_double_then_string_transitions_to_packed_elements() {
let mut arr = JsArray::new();
arr.push(JsValue::HeapNumber(1.5));
assert_eq!(arr.element_kind(), ElementKind::PackedDouble);
arr.push(JsValue::String("x".to_string().into()));
assert_eq!(arr.element_kind(), ElementKind::PackedElements);
}
#[test]
fn test_sparse_set_transitions_to_holey_smi() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1)); arr.set(3, JsValue::Smi(2)); assert_eq!(arr.element_kind(), ElementKind::HoleSmi);
}
#[test]
fn test_sparse_set_with_double_transitions_to_hole_double() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1)); arr.set(3, JsValue::HeapNumber(2.5)); assert_eq!(arr.element_kind(), ElementKind::HoleDouble);
}
#[test]
fn test_sparse_set_with_string_transitions_to_hole_elements() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1));
arr.set(5, JsValue::String("z".to_string().into()));
assert_eq!(arr.element_kind(), ElementKind::HoleElements);
}
#[test]
fn test_transition_never_narrows_after_pop() {
let mut arr = JsArray::new();
arr.push(JsValue::HeapNumber(1.0)); arr.pop();
assert_eq!(arr.element_kind(), ElementKind::PackedDouble);
}
#[test]
fn test_transition_packed_to_holey_after_sparse_then_stays_holey() {
let mut arr = JsArray::new();
arr.set(2, JsValue::Smi(5)); assert_eq!(arr.element_kind(), ElementKind::HoleSmi);
arr.push(JsValue::Smi(9));
assert_eq!(arr.element_kind(), ElementKind::HoleSmi);
}
#[test]
fn test_empty_array_length_is_zero() {
let arr = JsArray::new();
assert_eq!(arr.length(), 0);
}
#[test]
fn test_push_increments_length() {
let mut arr = JsArray::new();
assert_eq!(arr.push(JsValue::Smi(1)), 1);
assert_eq!(arr.push(JsValue::Smi(2)), 2);
assert_eq!(arr.push(JsValue::Smi(3)), 3);
assert_eq!(arr.length(), 3);
}
#[test]
fn test_pop_decrements_length() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(10));
arr.push(JsValue::Smi(20));
arr.pop();
assert_eq!(arr.length(), 1);
}
#[test]
fn test_pop_empty_returns_undefined_and_length_stays_zero() {
let mut arr = JsArray::new();
let v = arr.pop();
assert_eq!(v, JsValue::Undefined);
assert_eq!(arr.length(), 0);
}
#[test]
fn test_sparse_set_updates_length() {
let mut arr = JsArray::new();
arr.set(4, JsValue::Smi(1));
assert_eq!(arr.length(), 5); }
#[test]
fn test_set_within_bounds_does_not_change_length() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1));
arr.push(JsValue::Smi(2));
arr.push(JsValue::Smi(3));
arr.set(1, JsValue::Smi(99));
assert_eq!(arr.length(), 3);
}
#[test]
fn test_push_and_pop_roundtrip() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(42));
arr.push(JsValue::Smi(7));
assert_eq!(arr.pop(), JsValue::Smi(7));
assert_eq!(arr.pop(), JsValue::Smi(42));
assert_eq!(arr.pop(), JsValue::Undefined);
}
#[test]
fn test_push_after_pop_reuses_slot() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1));
arr.push(JsValue::Smi(2));
arr.pop();
arr.push(JsValue::Smi(3));
assert_eq!(arr.length(), 2);
assert_eq!(arr.get(1), JsValue::Smi(3));
}
#[test]
fn test_get_returns_correct_elements() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(10));
arr.push(JsValue::Smi(20));
arr.push(JsValue::Smi(30));
assert_eq!(arr.get(0), JsValue::Smi(10));
assert_eq!(arr.get(1), JsValue::Smi(20));
assert_eq!(arr.get(2), JsValue::Smi(30));
}
#[test]
fn test_get_out_of_bounds_returns_undefined() {
let arr = JsArray::new();
assert_eq!(arr.get(0), JsValue::Undefined);
assert_eq!(arr.get(100), JsValue::Undefined);
}
#[test]
fn test_set_updates_existing_element() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(1));
arr.push(JsValue::Smi(2));
arr.set(0, JsValue::Smi(99));
assert_eq!(arr.get(0), JsValue::Smi(99));
assert_eq!(arr.get(1), JsValue::Smi(2));
}
#[test]
fn test_sparse_holes_read_as_undefined() {
let mut arr = JsArray::new();
arr.set(3, JsValue::Smi(7));
assert_eq!(arr.get(0), JsValue::Undefined);
assert_eq!(arr.get(1), JsValue::Undefined);
assert_eq!(arr.get(2), JsValue::Undefined);
assert_eq!(arr.get(3), JsValue::Smi(7));
}
#[test]
fn test_default_equals_new() {
let arr: JsArray = JsArray::default();
assert_eq!(arr.element_kind(), ElementKind::PackedSmi);
assert_eq!(arr.length(), 0);
}
#[test]
fn test_as_object_reflects_elements() {
let mut arr = JsArray::new();
arr.push(JsValue::Smi(5));
assert_eq!(arr.as_object().get_element(0), JsValue::Smi(5));
}
}