///|
const JS_MAX_SAFE_INTEGER_I64 = 9007199254740991L
///|
const JS_MAX_SAFE_INTEGER_DOUBLE = 9007199254740991.0
///|
const JS_MAX_SAFE_INTEGER_EXCLUSIVE_DOUBLE = 9007199254740992.0
///|
const JS_MAX_ARRAY_LENGTH_I64 = 4294967295L
///|
const ARRAY_DENSE_MATERIALIZE_LIMIT_I64 = 10000000L
///|
/// Classification of an array-indexed lookup per ES2024 §10.1.5
/// OrdinaryGetOwnProperty. `Present` subsumes both "dense element, no
/// descriptor" and "own data descriptor" — data descriptors store
/// their value in `data.elements[i]`, not in the descriptor itself
/// (verified via `array_define_own_property:920`).
priv enum ArrayIndexHit {
Present(Value)
Hole
OutOfRange
OwnAccessor(Value?)
}
///|
/// Classify an array-indexed lookup without invoking any getter or
/// walking the prototype chain. Caller decides what to do with each
/// case (see `get_array_like_element_interp`, `has_array_like_element`,
/// and `Interpreter::get_property`).
fn array_index_lookup_result(data : ArrayData, i : Int) -> ArrayIndexHit {
let key = i.to_string()
match data.bag.descriptors.get(key) {
Some(desc) =>
if desc.is_accessor {
OwnAccessor(desc.getter)
} else if i >= 0 && i < data.elements.length() {
Present(data.elements[i])
} else {
match data.bag.properties.get(key) {
Some(v) => Present(v)
None => OutOfRange
}
}
None =>
if i < 0 {
OutOfRange
} else if i < data.elements.length() {
// Defensive read: `data.holes` is authoritative only when the
// dense slot itself is `Undefined` (Phase 3 padding semantics
// — Array(n), a.length=N, sparse assignment). A non-Undefined
// value here means some future direct `data.elements` mutation
// wrote to the slot without updating holes; trust the value
// rather than the stale marker.
//
// An explicit `undefined` cannot be distinguished here, so
// reordering/writing mutators must maintain `data.holes` when
// moving values or materializing holes (issue #123).
let v = data.elements[i]
if data.holes.contains(i) && v is Undefined {
Hole
} else {
Present(v)
}
} else if data.holes.contains(i) {
Hole
} else {
match data.bag.properties.get(key) {
Some(v) => Present(v)
None => OutOfRange
}
}
}
}
///|
fn array_index_lookup_result64(
data : ArrayData,
index : Int64,
) -> ArrayIndexHit {
if index < 0L {
return OutOfRange
}
if index <= 0x7FFFFFFFL {
return array_index_lookup_result(data, index.to_int())
}
let key = index.to_string()
match data.bag.descriptors.get(key) {
Some(desc) =>
if desc.is_accessor {
OwnAccessor(desc.getter)
} else {
match data.bag.properties.get(key) {
Some(v) => Present(v)
None => OutOfRange
}
}
None =>
match data.bag.properties.get(key) {
Some(v) => Present(v)
None => OutOfRange
}
}
}
///|
pub fn flatten_array_val(
elements : Array[Value],
depth : Int,
result : Array[Value],
) -> Unit {
for el in elements {
match el {
Array(inner) =>
if depth > 0 {
flatten_array_val(inner.elements, depth - 1, result)
} else {
result.push(el)
}
_ => result.push(el)
}
}
}
///|
fn array_sparse_own_length(data : ArrayData) -> Int64 {
let mut observed = data.elements.length().to_int64()
let include_key = fn(key : String) -> Unit {
match array_index64_from_string(key) {
Some(idx64) => {
let len64 = idx64 + 1L
if len64 > observed {
observed = len64
}
}
_ => ()
}
}
data.bag.properties.each(fn(key, _) { include_key(key) })
data.bag.descriptors.each(fn(key, _) { include_key(key) })
observed
}
///|
fn array_logical_length(data : ArrayData) -> Int64 {
let observed = array_sparse_own_length(data)
match get_array_length_override(data) {
Some(n64) => n64
None => observed
}
}
///|
/// ToLength: Get length from any value as per ECMAScript spec (array-like objects)
pub fn to_array_like_length(val : Value) -> Int64 raise {
match val {
Null =>
raise @errors.TypeError(
message="Cannot convert undefined or null to object",
)
Undefined =>
raise @errors.TypeError(
message="Cannot convert undefined or null to object",
)
Array(data) => array_logical_length(data)
Object(data) => {
// Check for getter-based length first, then fall back to properties
let length_val : Value? = match data.bag.properties.get("length") {
Some(v) => Some(v)
None =>
// Check if length is defined as an accessor (getter)
match data.bag.descriptors.get("length") {
Some(desc) =>
match desc.getter {
Some(Object(getter_data)) =>
match getter_data.callable {
Some(NativeCallable(_, f)) => Some(f([]))
Some(NativeCallableWithContext(_, f)) => Some(f(Call, []))
Some(NonConstructableCallable(_, f)) => Some(f([]))
Some(MethodCallable(_, f)) => Some(f(Object(data), []))
_ => None
}
_ => None
}
None => None
}
}
to_length_from_value(length_val)
}
String_(s) => s.length().to_int64()
_ => 0L
}
}
///|
/// ToLength with full interpreter support for user-defined getter-based length
pub fn to_array_like_length_interp(
val : Value,
interp : Interpreter,
) -> Int64 raise {
match val {
Null =>
raise @errors.TypeError(
message="Cannot convert undefined or null to object",
)
Undefined =>
raise @errors.TypeError(
message="Cannot convert undefined or null to object",
)
Array(data) => array_logical_length(data)
String_(s) => s.length().to_int64()
_ => {
// Use interpreter's get_property to properly handle getters and prototype chains
let length_val = interp.get_property(val, "length", @token.Loc::default())
to_length_from_value(Some(length_val), interp=Some(interp))
}
}
}
///|
fn to_length_number(n : Double) -> Int64 {
if n.is_nan() || n <= 0.0 {
0L
} else if n.is_inf() || n >= JS_MAX_SAFE_INTEGER_DOUBLE {
JS_MAX_SAFE_INTEGER_I64
} else {
n.floor().to_int64()
}
}
///|
fn to_length_from_value(
v : Value?,
interp? : Interpreter? = None,
) -> Int64 raise {
match v {
Some(Number(n)) => to_length_number(n)
Some(v) => to_length_number(to_number(v, interp~))
None => 0L
}
}
///|
/// Get indexed element from array-like value
pub fn get_array_like_element(val : Value, index : Int64) -> Value {
let key = index.to_string()
match val {
Array(data) =>
match array_index_lookup_result64(data, index) {
Present(v) => v
_ => Undefined
}
Object(data) => {
// Check for accessor descriptor (getter) on own property first
let getter_result : Value? = match data.bag.descriptors.get(key) {
Some(desc) =>
match desc.getter {
Some(Object(getter_data)) =>
match getter_data.callable {
Some(NativeCallable(_, f)) => Some(f([])) catch { _ => None }
Some(NativeCallableWithContext(_, f)) =>
Some(f(Call, [])) catch {
_ => None
}
Some(NonConstructableCallable(_, f)) =>
Some(f([])) catch {
_ => None
}
Some(MethodCallable(_, f)) =>
Some(f(Object(data), [])) catch {
_ => None
}
_ => None
}
_ => None
}
None => None
}
match getter_result {
Some(v) => v
None =>
// Walk prototype chain for property lookup
match data.bag.properties.get(key) {
Some(v) => v
None => {
let mut current = data.prototype
let mut result : Value = Undefined
while true {
match current {
Object(proto_data) =>
match proto_data.bag.properties.get(key) {
Some(v) => {
result = v
break
}
None => current = proto_data.prototype
}
_ => break
}
}
result
}
}
}
}
String_(s) => {
let chars = s.to_array()
if index >= 0L && index <= 0x7FFFFFFFL && index.to_int() < chars.length() {
let i = index.to_int()
let buf = StringBuilder::new()
buf.write_char(chars[i])
String_(buf.to_string())
} else {
Undefined
}
}
_ => Undefined
}
}
///|
/// Get indexed element from array-like value using interpreter (handles user-defined getters)
pub fn get_array_like_element_interp(
interp : Interpreter,
val : Value,
index : Int64,
) -> Value raise {
match val {
Array(data) =>
match array_index_lookup_result64(data, index) {
Present(v) => v
OwnAccessor(Some(getter)) =>
interp.call_value(getter, val, [], @token.Loc::default())
OwnAccessor(None) => Undefined
Hole | OutOfRange => {
let proto = get_array_prototype(interp.realm_state, data)
interp.get_property_from_prototype(
val,
proto,
index.to_string(),
@token.Loc::default(),
)
}
}
_ => interp.get_property(val, index.to_string(), @token.Loc::default())
}
}
///|
/// Non-raising HasProperty walk along an explicit prototype chain for an
/// indexed key. Returns true if any reachable object has an own data or
/// accessor property at the given key. Does NOT invoke Proxy traps
/// (would require raising); Proxy-as-prototype is out of scope per the
/// iteration-model spec v5.
fn proto_chain_has_index(
interp : Interpreter,
proto : Value,
key : String,
index : Int64,
) -> Bool {
let mut current = proto
while true {
match current {
Object(proto_data) => {
if proto_data.bag.properties.contains(key) {
return true
}
match proto_data.bag.descriptors.get(key) {
Some(_) => return true
None => current = proto_data.prototype
}
}
Array(arr_data) =>
match array_index_lookup_result64(arr_data, index) {
Present(_) | OwnAccessor(_) => return true
Hole | OutOfRange =>
// Array-as-prototype: continue walking via the array's own
// prototype so inherited indices on Object.prototype are seen.
current = get_array_prototype(interp.realm_state, arr_data)
}
_ => return false
}
}
false
}
///|
/// Check if array-like value has indexed property.
/// `interp` is required so the Array branch can resolve `%Array.prototype%`
/// via `get_array_prototype(interp.realm_state,data)`.
pub fn has_array_like_element(
interp : Interpreter,
val : Value,
index : Int64,
) -> Bool {
let key = index.to_string()
match val {
Array(data) =>
match array_index_lookup_result64(data, index) {
Present(_) | OwnAccessor(_) => true
Hole | OutOfRange => {
let proto = get_array_prototype(interp.realm_state, data)
proto_chain_has_index(interp, proto, key, index)
}
}
Object(data) => {
if data.bag.properties.contains(key) {
return true
}
// Check for accessor descriptor
match data.bag.descriptors.get(key) {
Some(desc) if desc.is_accessor => return true
_ => ()
}
// Walk prototype chain
let mut current = data.prototype
while true {
match current {
Object(proto_data) => {
if proto_data.bag.properties.contains(key) {
return true
}
match proto_data.bag.descriptors.get(key) {
Some(desc) if desc.is_accessor => return true
_ => ()
}
current = proto_data.prototype
}
Array(arr_data) =>
// Don't terminate the walk on Hole/OutOfRange — an array used
// as a prototype only owns its dense slots; missing indices
// must fall through to the next prototype (typically
// Array.prototype, then Object.prototype).
match array_index_lookup_result64(arr_data, index) {
Present(_) | OwnAccessor(_) => return true
Hole | OutOfRange =>
current = get_array_prototype(interp.realm_state, arr_data)
}
_ => break
}
}
false
}
String_(s) =>
index >= 0L && index <= 0x7FFFFFFFL && index.to_int() < s.length()
Proxy(_) => interp.has_property(val, key) catch { _ => false }
_ => false
}
}
///|
/// Drop hole entries at indices >= `new_len` after a length shrink.
/// Used by both `Interpreter::set_property` and
/// `Interpreter::set_computed_property` length-set paths to avoid the
/// near-identical 12-line block being kept in sync by hand.
///
/// Hole keys are `Int` (≤ 2^31-1); if `new_len` exceeds Int range, no
/// in-range hole can be out-of-range, so the cleanup is a no-op — we
/// skip it to avoid an overflowing Int64→Int cast.
pub fn cleanup_holes_above_length(
holes : Map[Int, Unit],
new_len : Int64,
) -> Unit {
if new_len > 0x7FFFFFFFL {
return
}
let new_len_int = new_len.to_int()
let stale : Array[Int] = []
holes.each(fn(k, _) { if k >= new_len_int { stale.push(k) } })
for k in stale {
holes.remove(k)
}
}
///|
fn cleanup_sparse_array_indices_above_length(
arr : ArrayData,
new_len : Int64,
) -> Int64? {
let entries : Array[(Int64, String)] = []
let dense_len64 = arr.elements.length().to_int64()
arr.bag.properties.each(fn(key, _) {
match array_index64_from_string(key) {
Some(idx64) if idx64 >= new_len && idx64 >= dense_len64 =>
entries.push((idx64, key))
_ => ()
}
})
arr.bag.descriptors.each(fn(key, _) {
match array_index64_from_string(key) {
Some(idx64) =>
if idx64 >= new_len &&
idx64 >= dense_len64 &&
!arr.bag.properties.contains(key) {
entries.push((idx64, key))
}
_ => ()
}
})
entries.sort_by(fn(a, b) {
if a.0 > b.0 {
-1
} else if a.0 < b.0 {
1
} else {
0
}
})
for entry in entries {
let idx64 = entry.0
let key = entry.1
match arr.bag.descriptors.get(key) {
Some(desc) if !desc.configurable => return Some(idx64 + 1L)
_ => ()
}
let _ = arr.bag.properties.remove(key)
let _ = arr.bag.descriptors.remove(key)
}
None
}
///|
/// Set indexed element on array-like value
pub fn set_array_like_element(
val : Value,
index : Int64,
value : Value,
) -> Unit {
let key = index.to_string()
match val {
Array(data) =>
if index >= 0L &&
index <= 0x7FFFFFFFL &&
index.to_int() < data.elements.length() {
let i = index.to_int()
data.elements[i] = value
data.holes.remove(i)
} else if index >= 0L {
data.bag.properties[key] = value
}
Object(data) => data.bag.properties[key] = value
_ => ()
}
}
///|
/// Set the length property on an array-like value
pub fn set_array_like_length(val : Value, len : Int64) -> Unit {
match val {
Array(data) => {
if len > data.elements.length().to_int64() {
set_array_length_override(data, len)
} else {
data.elements.truncate(len.to_int())
clear_array_length_override(data)
}
cleanup_holes_above_length(data.holes, len)
}
Object(data) => data.bag.properties["length"] = Number(len.to_double())
_ => ()
}
}
///|
/// Delete indexed element from array-like value
pub fn delete_array_like_element(val : Value, index : Int) -> Unit {
let key = index.to_string()
match val {
Object(data) => data.bag.properties.remove(key) |> ignore
_ => ()
}
}
///|
/// CreateDataPropertyOrThrow(O, P, V) per ES spec 7.3.5.
/// Sets an indexed property on a species result value and throws if
/// `[[DefineOwnProperty]]` rejects the creation.
pub fn create_data_property_or_throw(
interp : Interpreter,
target : Value,
index : Int64,
value : Value,
) -> Unit raise {
let key = index.to_string()
let ok = match target {
Array(data) =>
if index >= 0L &&
index <= ARRAY_DENSE_MATERIALIZE_LIMIT_I64 &&
data.extensible &&
data.length_writable &&
!data.bag.descriptors.contains(key) {
let i = index.to_int()
if i == data.elements.length() {
data.elements.push(value)
} else if i >= 0 && i < data.elements.length() {
data.elements[i] = value
} else {
// Sparse case: pad with Undefined up to index (these are holes),
// then set value at the target index (not a hole).
while data.elements.length() < i {
let pad_idx = data.elements.length()
data.elements.push(Undefined)
data.holes[pad_idx] = ()
}
data.elements.push(value)
}
data.holes.remove(i)
match get_array_length_override(data) {
Some(len) if index + 1L > len =>
set_array_length_override(data, index + 1L)
_ => ()
}
true
} else {
interp.define_own_property(
target,
String_(key),
PartialDescriptor::data_default(value),
@token.Loc::default(),
)
}
_ =>
interp.define_own_property(
target,
String_(key),
PartialDescriptor::data_default(value),
@token.Loc::default(),
)
}
if !ok {
raise @errors.TypeError(message="Cannot create data property")
}
}
///|
/// §7.2.2 IsArray — returns true for Array exotic objects and Object values
/// whose `class_name` is `"Array"`, recursively unwrapping Proxy chains.
/// Throws TypeError when a revoked Proxy is encountered (spec step 3.a).
pub fn is_array(val : Value) -> Bool raise Error {
match val {
Array(_) => true
Object(data) => data.class_name == "Array"
Proxy(proxy_data) => {
guard proxy_data.handler is Some(_) else {
raise @errors.TypeError(
message="Cannot perform 'IsArray' on a proxy that has been revoked",
)
}
match proxy_data.target {
Some(target) => is_array(target)
None => false
}
}
_ => false
}
}
///|
/// ArraySpeciesCreate(originalArray, length) per ES spec 9.4.2.3
/// Looks up Symbol.species on the original array's constructor to determine
/// what constructor to use for the result. Returns a plain Array if no
/// custom species is found.
pub fn array_species_create(
interp : Interpreter,
original : Value,
len : Int64,
) -> Value raise Error {
// Step 1: If originalArray is not an Array, return a plain array.
// IsArray unwraps proxies, so ArraySpeciesCreate must do the same before
// deciding whether to consult constructor/@@species.
if !is_array(original) {
if len > JS_MAX_ARRAY_LENGTH_I64 {
raise @errors.RangeError(message="Invalid array length")
}
return make_array([])
}
let loc = @token.Loc::default()
// Step 2: Get C = Get(originalArray, "constructor")
let c = interp.get_property(original, "constructor", loc)
// Steps 4-5: If Type(C) is Object, set C = C[@@species]; if null/undefined use default.
// If Type(C) is not Object and not undefined, preserve C so step 6 (IsConstructor check)
// throws TypeError — e.g. when a.constructor is set to null, a number, or a string.
let species : Value = match c {
Undefined => Undefined
_ =>
if is_object_value(c) {
let species_sym = interp.realm_state.well_known_symbols.species
let s = interp.get_computed_property(c, Symbol(species_sym), loc)
match s {
Null | Undefined => Undefined
_ => s
}
} else {
c
}
}
// Step 5: If C is undefined, return plain array
if species is Undefined {
if len > JS_MAX_ARRAY_LENGTH_I64 {
raise @errors.RangeError(message="Invalid array length")
}
return make_array([])
}
// Step 6: If IsConstructor(C) is false, throw TypeError
if !is_constructor_value(species) {
raise @errors.TypeError(message="Species constructor is not a constructor")
}
// Step 7: Construct(C, « length »)
interp.construct_value(species, [Number(len.to_double())], loc)
}
///|
/// Convert array-like value to an Array of Values
pub fn to_array_like_elements(val : Value) -> Array[Value] raise {
let len = to_array_like_length(val)
let result : Array[Value] = []
for i = 0L; i < len; i = i + 1L {
result.push(get_array_like_element(val, i))
}
result
}