///|
/// If `key` is a canonical array-index string — a non-negative integer with no
/// redundant representation (e.g. "0" or "42", but not "01", "-0", or "1.0") —
/// return that index, else `None`. Mirrors the partition test used throughout
/// property enumeration, including sparse array indices beyond MoonBit `Int`
/// range.
fn array_index_of_key(key : String) -> Int64? {
array_index64_from_string(key)
}
///|
/// Sort property keys per OrdinaryOwnPropertyKeys spec order:
/// 1. Integer indices in ascending numeric order
/// 2. Other string keys in insertion order
pub fn sort_property_keys(props : Map[String, Value]) -> Array[String] {
let int_keys : Array[String] = []
let str_keys : Array[String] = []
props.each(fn(k, _v) {
match array_index_of_key(k) {
Some(_) => int_keys.push(k)
None => str_keys.push(k)
}
})
int_keys.sort_by(fn(a, b) {
let na = array_index_of_key(a).unwrap_or(0L)
let nb = array_index_of_key(b).unwrap_or(0L)
if na < nb {
-1
} else if na > nb {
1
} else {
0
}
})
[..int_keys, ..str_keys]
}
///|
/// Validate non-configurable property constraints per ES spec [[DefineOwnProperty]].
/// Throws TypeError if the proposed descriptor change violates invariants.
/// This is the sole authority for descriptor mutation constraints in the runtime.
pub fn validate_non_configurable(
existing : PropDescriptor,
prop_display : String,
is_accessor : Bool,
is_data : Bool,
has_value : Bool,
new_writable : Bool?,
new_enumerable : Bool?,
new_configurable : Bool?,
new_getter : Value?,
new_setter : Value?,
get_old_value : () -> Value,
get_new_value : () -> Value?,
) -> Unit raise Error {
if existing.configurable {
return
}
let existing_is_accessor = existing.is_accessor
// Cannot change between accessor and data
if existing_is_accessor && is_data {
raise @errors.TypeError(
message="Cannot redefine non-configurable property: \{prop_display}",
)
}
if !existing_is_accessor && is_accessor {
raise @errors.TypeError(
message="Cannot redefine non-configurable property: \{prop_display}",
)
}
let enumerable_ok = match new_enumerable {
Some(e) => e == existing.enumerable
None => true
}
let configurable_ok = match new_configurable {
Some(c) => c == existing.configurable
None => true
}
// Non-configurable accessor: validate getter/setter identity
if existing_is_accessor && is_accessor {
let getter_ok = match new_getter {
Some(g) =>
match existing.getter {
Some(eg) => strict_equal(g, eg)
None => g is Undefined
}
None => true
}
let setter_ok = match new_setter {
Some(s) =>
match existing.setter {
Some(es) => strict_equal(s, es)
None => s is Undefined
}
None => true
}
if !(enumerable_ok && configurable_ok && getter_ok && setter_ok) {
raise @errors.TypeError(
message="Cannot redefine non-configurable property: \{prop_display}",
)
}
}
// Non-configurable data: validate writable/value
if !existing_is_accessor && !is_accessor {
let writable_ok = match new_writable {
Some(w) => if !existing.writable && w { false } else { true }
None => true
}
let value_ok = if !existing.writable && has_value {
match get_new_value() {
Some(v) => strict_equal(v, get_old_value())
None => true
}
} else {
true
}
if !(writable_ok && enumerable_ok && configurable_ok && value_ok) {
raise @errors.TypeError(
message="Cannot redefine non-configurable property: \{prop_display}",
)
}
}
// Generic descriptor (no accessor/data fields)
if !is_accessor && !is_data {
if !(enumerable_ok && configurable_ok) {
raise @errors.TypeError(
message="Cannot redefine non-configurable property: \{prop_display}",
)
}
}
}
// ---------------------------------------------------------------------------
// Stage B.2: ordinary [[GetOwnProperty]] / [[DefineOwnProperty]]
// ---------------------------------------------------------------------------
///|
/// ES §7.2.11 SameValue. Differs from `strict_equal` on two cases:
/// - SameValue(NaN, NaN) = true (strict_equal: false)
/// - SameValue(+0, -0) = false (strict_equal: true)
/// Shared by ValidateAndApplyPropertyDescriptor and Proxy invariant checks for
/// non-writable, non-configurable data properties.
fn same_value(a : Value, b : Value) -> Bool {
match (a, b) {
(Number(x), Number(y)) =>
if x.is_nan() && y.is_nan() {
true
} else if x == 0.0 && y == 0.0 {
// +0 and -0 compare equal under ==; distinguish by reciprocal sign
1.0 / x == 1.0 / y
} else {
x == y
}
_ => strict_equal(a, b)
}
}
///|
/// Extract the stored own value for `key` on `val`. Mirrors the key-aware
/// reader used in proxy helpers but surfaced here for VAP's value-change
/// check (step 4.b.iii).
pub fn ordinary_get_own_value_for_descriptor(
val : Value,
key : Value,
) -> Value raise Error {
match key {
Symbol(sym) => {
let bag_opt = match val {
Object(data) => Some(data.bag)
Map(data) => Some(data.bag)
Set(data) => Some(data.bag)
Promise(data) => Some(data.bag)
Array(arr) => Some(arr.bag)
_ => None
}
match bag_opt {
Some(bag) =>
match bag.symbol_properties.get(sym.id) {
Some(v) => v
None => Undefined
}
None => Undefined
}
}
_ => {
let k = to_js_string(key)
match val {
Object(data) =>
match data.bag.properties.get(k) {
Some(v) => v
None => Undefined
}
Map(data) =>
match data.bag.properties.get(k) {
Some(v) => v
None => Undefined
}
Set(data) =>
match data.bag.properties.get(k) {
Some(v) => v
None => Undefined
}
Promise(data) =>
match data.bag.properties.get(k) {
Some(v) => v
None => Undefined
}
Array(arr) => {
if k == "length" {
return match get_array_length_override(arr) {
Some(n64) => Value::Number(n64.to_double())
None => Value::Number(arr.elements.length().to_double())
}
}
let idx = @string.parse_int(k) catch { _ => -1 }
if idx >= 0 && idx.to_string() == k && idx < arr.elements.length() {
return arr.elements[idx]
}
match arr.bag.properties.get(k) {
Some(v) => v
None => Undefined
}
}
String_(s) => {
if k == "length" {
return Number(utf16_length(s).to_double())
}
let idx = @string.parse_int(k) catch { _ => -1 }
let units = string_to_utf16(s)
if idx >= 0 && idx.to_string() == k && idx < units.length() {
String_(String::make(1, units[idx].unsafe_to_char()))
} else {
Undefined
}
}
_ => Undefined
}
}
}
}
///|
/// ES §10.1.5 `OrdinaryGetOwnProperty(O, P)`. Returns the stored descriptor
/// for an own property on `val`. For Array, synthesizes descriptors for
/// `length` and in-range indexed elements per §10.4.2. Returns `None` if the
/// property is not own.
pub fn ordinary_get_own_property(
val : Value,
key : Value,
) -> PropDescriptor? raise Error {
match key {
Symbol(sym) => {
let bag_opt = match val {
Object(data) => Some(data.bag)
Map(data) => Some(data.bag)
Set(data) => Some(data.bag)
Promise(data) => Some(data.bag)
Array(arr) => Some(arr.bag)
_ => None
}
match bag_opt {
Some(bag) =>
// A symbol property might have a descriptor OR just a value entry.
match bag.symbol_descriptors.get(sym.id) {
Some(d) => Some(d)
None =>
if bag.symbol_properties.contains(sym.id) {
Some({
writable: true,
enumerable: true,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
})
} else {
None
}
}
None => None
}
}
_ => {
let k = to_js_string(key)
match val {
Object(data) => ordinary_get_own_string_desc(data.bag, k)
Map(data) => ordinary_get_own_string_desc(data.bag, k)
Set(data) => ordinary_get_own_string_desc(data.bag, k)
Promise(data) => ordinary_get_own_string_desc(data.bag, k)
Array(arr) => {
if k == "length" {
return Some({
writable: arr.length_writable,
enumerable: false,
configurable: false,
getter: None,
setter: None,
is_accessor: false,
})
}
let idx = @string.parse_int(k) catch { _ => -1 }
if idx >= 0 &&
idx.to_string() == k &&
idx < arr.elements.length() &&
!arr.holes.contains(idx) {
return match arr.bag.descriptors.get(k) {
Some(desc) => Some(desc)
None =>
Some({
writable: true,
enumerable: true,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
})
}
}
ordinary_get_own_string_desc(arr.bag, k)
}
String_(s) => {
if k == "length" {
return Some({
writable: false,
enumerable: false,
configurable: false,
getter: None,
setter: None,
is_accessor: false,
})
}
let idx = @string.parse_int(k) catch { _ => -1 }
if idx >= 0 && idx.to_string() == k && idx < utf16_length(s) {
Some({
writable: false,
enumerable: true,
configurable: false,
getter: None,
setter: None,
is_accessor: false,
})
} else {
None
}
}
_ => None
}
}
}
}
///|
fn ordinary_get_own_string_desc(
bag : PropertyBag,
key : String,
) -> PropDescriptor? {
match bag.descriptors.get(key) {
Some(d) => Some(d)
None =>
if bag.properties.contains(key) {
Some({
writable: true,
enumerable: true,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
})
} else {
None
}
}
}
///|
/// Is `val` extensible per ES §10.1.3?
fn ordinary_is_extensible(val : Value) -> Bool {
match val {
Object(data) => data.extensible
Array(data) => data.extensible
Map(data) => data.extensible
Set(data) => data.extensible
Promise(data) => data.extensible
_ => false
}
}
///|
/// Is `val` one of the "object-like" Value variants that can own properties?
/// Primitives and Proxy are excluded from the ordinary path.
fn is_ordinary_object_like(val : Value) -> Bool {
match val {
Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) => true
_ => false
}
}
///|
/// VAP step 4 compatibility check: given `current` is non-configurable and
/// `partial` proposes a change, decide whether the change is permitted.
/// Returns `true` if compatible (proceed to apply); `false` if the change
/// must be rejected (VAP returns false without writing).
fn is_compatible_with_non_configurable(
current : PropDescriptor,
partial : PartialDescriptor,
current_value : Value,
) -> Bool {
// §10.1.6.3 step 4.a: cannot change configurable false -> true.
match partial.configurable {
Some(true) => return false
_ => ()
}
// Step 4.b: cannot change enumerable.
match partial.enumerable {
Some(e) => if e != current.enumerable { return false }
None => ()
}
let current_is_accessor = current.is_accessor
let partial_is_accessor = partial.is_accessor()
let partial_is_data = partial.is_data()
// Step 4.c: cannot toggle accessor <-> data when non-configurable.
if current_is_accessor && partial_is_data {
return false
}
if !current_is_accessor && partial_is_accessor {
return false
}
if current_is_accessor && partial_is_accessor {
// Step 4.d: non-configurable accessor — getter/setter identity must
// not change (SameValue).
if partial.has_getter {
let g_matches = match current.getter {
Some(cg) => same_value(partial.getter.unwrap_or(Undefined), cg)
None => partial.getter is Some(Undefined) || partial.getter is None
}
if !g_matches {
return false
}
}
if partial.has_setter {
let s_matches = match current.setter {
Some(cs) => same_value(partial.setter.unwrap_or(Undefined), cs)
None => partial.setter is Some(Undefined) || partial.setter is None
}
if !s_matches {
return false
}
}
}
if !current_is_accessor && !partial_is_accessor {
// Step 4.e: non-configurable non-writable data — value change must be
// SameValue; writable false -> true is forbidden.
if !current.writable {
match partial.writable {
Some(true) => return false
_ => ()
}
match partial.value {
Some(v) => if !same_value(v, current_value) { return false }
None => ()
}
}
}
true
}
///|
/// Merge a PartialDescriptor into an existing PropDescriptor, returning the
/// new descriptor. Fields absent from `partial` preserve `current`'s value.
/// When accessor fields transition between has/hasn't, the storage reflects
/// the partial's presence.
fn merge_partial_into(
current : PropDescriptor,
partial : PartialDescriptor,
) -> PropDescriptor {
let writable = match partial.writable {
Some(w) => w
None => current.writable
}
let enumerable = match partial.enumerable {
Some(e) => e
None => current.enumerable
}
let configurable = match partial.configurable {
Some(c) => c
None => current.configurable
}
let getter = if partial.has_getter {
match partial.getter {
Some(Undefined) | None => None
Some(g) => Some(g)
}
} else {
current.getter
}
let setter = if partial.has_setter {
match partial.setter {
Some(Undefined) | None => None
Some(s) => Some(s)
}
} else {
current.setter
}
// Preserve accessor type: if partial explicitly introduces accessor fields
// (has_getter or has_setter), result is accessor. If partial introduces data
// fields (value or writable), result is data. Otherwise inherit from current.
let is_accessor = if partial.has_getter || partial.has_setter {
true
} else if partial.value is Some(_) || partial.writable is Some(_) {
false
} else {
current.is_accessor
}
{ writable, enumerable, configurable, getter, setter, is_accessor }
}
///|
/// Build a fresh PropDescriptor for an absent key, filling omitted fields
/// with defaults per ES §6.2.5.5 CompletePropertyDescriptor: absent
/// writable/enumerable/configurable default to `false`; accessor fields
/// default to `None`.
fn complete_partial_for_new(partial : PartialDescriptor) -> PropDescriptor {
let writable = partial.writable.unwrap_or(false)
let enumerable = partial.enumerable.unwrap_or(false)
let configurable = partial.configurable.unwrap_or(false)
let getter = if partial.has_getter {
match partial.getter {
Some(Undefined) | None => None
Some(g) => Some(g)
}
} else {
None
}
let setter = if partial.has_setter {
match partial.setter {
Some(Undefined) | None => None
Some(s) => Some(s)
}
} else {
None
}
let is_accessor = partial.has_getter || partial.has_setter
{ writable, enumerable, configurable, getter, setter, is_accessor }
}
///|
/// Write a merged/completed descriptor into `bag` at `key`. Also writes the
/// value portion if the partial contained one (data descriptor), so subsequent
/// `bag.properties` reads return the correct value.
fn apply_descriptor_to_bag(
bag : PropertyBag,
key : Value,
desc : PropDescriptor,
value_opt : Value?,
) -> Unit raise Error {
match key {
Symbol(sym) => {
bag.symbol_descriptors[sym.id] = desc
match value_opt {
Some(v) => bag.symbol_properties[sym.id] = v
None =>
if !bag.symbol_properties.contains(sym.id) {
bag.symbol_properties[sym.id] = Undefined
}
}
}
_ => {
// ToPropertyKey-aligned coercion — for object-typed keys this invokes
// Symbol.toPrimitive / toString via to_js_string rather than MoonBit's
// Value::to_string (which produces a debug string for objects and
// would write to a different slot than the validation pipeline read).
let k = to_js_string(key)
bag.descriptors[k] = desc
match value_opt {
Some(v) => bag.properties[k] = v
None => if !bag.properties.contains(k) { bag.properties[k] = Undefined }
}
}
}
}
///|
/// ES §6.2.5.5 `ToPropertyDescriptor`. Extract a PartialDescriptor from a JS
/// attrs object, invoking getters and reading inherited properties per spec.
/// Throws TypeError if:
/// - attrs is not an object
/// - both data (value/writable) and accessor (get/set) fields appear
/// - get or set is non-callable non-undefined
pub fn Interpreter::partial_descriptor_from_attrs(
self : Interpreter,
attrs : Value,
loc : @token.Loc,
) -> PartialDescriptor raise Error {
if !is_object_value(attrs) {
raise @errors.TypeError(
message="Property description must be an object: \{attrs.to_string()}",
)
}
let mut enumerable : Bool? = None
let mut configurable : Bool? = None
let mut value : Value? = None
let mut writable : Bool? = None
let mut getter : Value? = None
let mut setter : Value? = None
let mut has_value = false
let mut has_writable = false
let mut has_getter = false
let mut has_setter = false
if self.has_property(attrs, "enumerable") {
enumerable = Some(is_truthy(self.get_property(attrs, "enumerable", loc)))
}
if self.has_property(attrs, "configurable") {
configurable = Some(
is_truthy(self.get_property(attrs, "configurable", loc)),
)
}
if self.has_property(attrs, "value") {
has_value = true
value = Some(self.get_property(attrs, "value", loc))
}
if self.has_property(attrs, "writable") {
has_writable = true
writable = Some(is_truthy(self.get_property(attrs, "writable", loc)))
}
if self.has_property(attrs, "get") {
has_getter = true
let get_val = self.get_property(attrs, "get", loc)
match get_val {
Undefined => ()
_ => {
if !is_callable(get_val) {
raise @errors.TypeError(
message="Getter must be a function: \{get_val.to_string()}",
)
}
getter = Some(get_val)
}
}
}
if self.has_property(attrs, "set") {
has_setter = true
let set_val = self.get_property(attrs, "set", loc)
match set_val {
Undefined => ()
_ => {
if !is_callable(set_val) {
raise @errors.TypeError(
message="Setter must be a function: \{set_val.to_string()}",
)
}
setter = Some(set_val)
}
}
}
if (has_value || has_writable) && (has_getter || has_setter) {
raise @errors.TypeError(
message="Invalid property descriptor. Cannot both specify accessors and a value or writable attribute",
)
}
{
value,
writable,
enumerable,
configurable,
getter,
setter,
has_getter,
has_setter,
}
}
///|
/// Convert a stored PropDescriptor into a plain JS descriptor object for
/// Object.getOwnPropertyDescriptor / Reflect.getOwnPropertyDescriptor
/// callers. Emits value + writable for data descriptors; get + set for
/// accessor descriptors.
pub fn descriptor_to_value(
desc : PropDescriptor,
value : Value,
realm_state? : RealmState? = None,
) -> Value {
let props : Map[String, Value] = Map([])
if desc.is_accessor {
props["get"] = desc.getter.unwrap_or(Undefined)
props["set"] = desc.setter.unwrap_or(Undefined)
} else {
props["value"] = value
props["writable"] = Value::Bool(desc.writable)
}
props["enumerable"] = Value::Bool(desc.enumerable)
props["configurable"] = Value::Bool(desc.configurable)
Value::Object({
bag: {
properties: props,
symbol_properties: Map([]),
descriptors: Map([]),
symbol_descriptors: Map([]),
internal_slots: Map([]),
host_slots: Map([]),
},
prototype: get_obj_proto(realm_state~),
extensible: true,
arraybuffer_state: None,
callable: None,
class_name: "Object",
})
}
///|
/// ES [[DefineOwnProperty]] dispatcher — routes Proxy targets through the
/// defineProperty trap (`proxy_define_property`), others through
/// `ordinary_define_own_property`. This is the main entry point builtins
/// and internal callers should use.
pub fn Interpreter::define_own_property(
self : Interpreter,
val : Value,
key : Value,
partial : PartialDescriptor,
loc : @token.Loc,
) -> Bool raise Error {
match val {
Value::Proxy(pdata) => proxy_define_property(self, pdata, key, partial)
_ => self.ordinary_define_own_property(val, key, partial, loc)
}
}
///|
fn typedarray_index_own_descriptor() -> PropDescriptor {
{
writable: true,
enumerable: true,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
}
///|
fn ordinary_get_own_property_pair(
val : Value,
key : Value,
) -> (PropDescriptor, Value)? raise Error {
match ordinary_get_own_property(val, key) {
None => None
Some(desc) => Some((desc, ordinary_get_own_value_for_descriptor(val, key)))
}
}
///|
fn Interpreter::typedarray_get_own_property_pair(
self : Interpreter,
data : ObjectData,
key : Value,
) -> (PropDescriptor, Value)? raise Error {
match key {
String_(s) =>
match classify_typedarray_string_key(s) {
Some(idx) =>
if idx >= 0 &&
(self.stdlib_hooks.typedarray_is_valid_index)(
data,
idx,
self.realm_state,
) {
let value = (self.stdlib_hooks.typedarray_get_index)(
data,
idx,
self.realm_state,
)
if value is Undefined {
None
} else {
Some((typedarray_index_own_descriptor(), value))
}
} else {
None
}
None => ordinary_get_own_property_pair(Object(data), key)
}
_ => ordinary_get_own_property_pair(Object(data), key)
}
}
///|
/// ES [[GetOwnProperty]] dispatcher — Proxy targets route through
/// `proxy_get_own_property`, TypedArrays synthesize integer-indexed element
/// descriptors per §10.4.5.1, and others route through
/// `ordinary_get_own_property`.
/// Returns `(descriptor, value)` pairs so the trap-provided value in the
/// Proxy case flows through (PropDescriptor has no `value` slot — the
/// value is stored separately on the bag or synthesized by the trap).
pub fn Interpreter::get_own_property(
self : Interpreter,
val : Value,
key : Value,
) -> (PropDescriptor, Value)? raise Error {
let prop_key = to_property_key(key, interp=Some(self))
match val {
Value::Proxy(pdata) => proxy_get_own_property(self, pdata, prop_key)
Object(data) if data.class_name == "Module" =>
self.module_namespace_get_own_property_pair(data, prop_key)
Object(data) if is_typedarray_class(data.class_name) =>
self.typedarray_get_own_property_pair(data, prop_key)
_ => ordinary_get_own_property_pair(val, prop_key)
}
}
///|
fn Interpreter::typedarray_define_own_property_numeric(
self : Interpreter,
data : ObjectData,
key : Value,
partial : PartialDescriptor,
) -> Bool? raise Error {
match key {
String_(s) =>
match classify_typedarray_string_key(s) {
Some(idx) => {
if idx < 0 ||
!(self.stdlib_hooks.typedarray_is_valid_index)(
data,
idx,
self.realm_state,
) {
return Some(false)
}
if partial.is_accessor() {
return Some(false)
}
match partial.configurable {
Some(false) => return Some(false)
_ => ()
}
match partial.enumerable {
Some(false) => return Some(false)
_ => ()
}
match partial.writable {
Some(false) => return Some(false)
_ => ()
}
match partial.value {
Some(value) => {
let num = self.to_number(value)
(self.stdlib_hooks.typedarray_set_index)(
data,
idx,
num,
self.realm_state,
)
}
None => ()
}
Some(true)
}
None => None
}
_ => None
}
}
///|
/// ES §10.1.6 `OrdinaryDefineOwnProperty(O, P, Desc)` + §10.4.2.1 Array
/// exotic dispatch + §10.4.5.3 IntegerIndexed exotic dispatch. Returns `true`
/// on success, `false` on rejection (the caller — Object.defineProperty
/// throws, Reflect.defineProperty returns false).
pub fn Interpreter::ordinary_define_own_property(
self : Interpreter,
val : Value,
key : Value,
partial : PartialDescriptor,
loc : @token.Loc,
) -> Bool raise Error {
// Exotic dispatch
match val {
Object(data) if data.class_name == "Module" =>
match
self.module_namespace_define_own_property(
data,
to_property_key(key, interp=Some(self)),
partial,
) {
Some(result) => return result
None => ()
}
Object(data) if is_typedarray_class(data.class_name) =>
match self.typedarray_define_own_property_numeric(data, key, partial) {
Some(result) => return result
None => ()
}
Array(arr) => return self.array_define_own_property(arr, key, partial, loc)
_ => ()
}
if !is_ordinary_object_like(val) {
return false
}
let existing = ordinary_get_own_property(val, key)
let extensible = ordinary_is_extensible(val)
match existing {
None => {
if !extensible {
return false
}
let full = complete_partial_for_new(partial)
let value_opt = partial.value
ordinary_write(val, key, full, value_opt)
true
}
Some(current) => {
// Fast-pass: all configurable changes are allowed.
if !current.configurable {
let cur_val = ordinary_get_own_value_for_descriptor(val, key)
if !is_compatible_with_non_configurable(current, partial, cur_val) {
return false
}
}
let merged = merge_partial_into(current, partial)
// §10.1.6.3 step 4.b: when descriptor type changes, clear the fields
// that don't apply to the new type.
let current_is_accessor = current.is_accessor
let final_desc = if partial.is_accessor() && !current_is_accessor {
// Data → Accessor: writable not meaningful for accessor; set to false.
{ ..merged, writable: false }
} else if partial.is_data() && current_is_accessor {
// Accessor → Data: clear residual getter/setter so the merged
// descriptor is a pure data descriptor. Without this, reads via
// ordinary_get_value would still fire the old getter.
{ ..merged, getter: None, setter: None }
} else {
merged
}
ordinary_write(val, key, final_desc, partial.value)
true
}
}
}
///|
/// Dispatch descriptor + value write to the appropriate bag, given the
/// receiver has already been classified as object-like (not Array).
fn ordinary_write(
val : Value,
key : Value,
desc : PropDescriptor,
value_opt : Value?,
) -> Unit raise Error {
match val {
Object(data) => apply_descriptor_to_bag(data.bag, key, desc, value_opt)
Map(data) => apply_descriptor_to_bag(data.bag, key, desc, value_opt)
Set(data) => apply_descriptor_to_bag(data.bag, key, desc, value_opt)
Promise(data) => apply_descriptor_to_bag(data.bag, key, desc, value_opt)
_ => ()
}
}
///|
/// ES §10.4.2.1 Array exotic `[[DefineOwnProperty]]`. Dispatches on:
/// - key == "length" -> array_set_length (§10.4.2.4 with partial truncation).
/// - key is array index -> ordinary-style write against `bag`, plus grow
/// elements to idx+1 (ArraySetLength auto-grow) ONLY when descriptor is the
/// default-data shape. Non-default (configurable:false / writable:false /
/// enumerable:false) descriptors on indexed elements return false pending
/// Stage C's per-index descriptor storage.
/// - Other keys -> ordinary-style write against arr.bag.
pub fn Interpreter::array_define_own_property(
self : Interpreter,
arr : ArrayData,
key : Value,
partial : PartialDescriptor,
loc : @token.Loc,
) -> Bool raise Error {
let _ = loc
match key {
Symbol(sym) => {
// Symbol keys flow through bag.symbol_*; treat as ordinary ordinary.
let existing = match arr.bag.symbol_descriptors.get(sym.id) {
Some(d) => Some(d)
None =>
if arr.bag.symbol_properties.contains(sym.id) {
Some({
writable: true,
enumerable: true,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
})
} else {
None
}
}
match existing {
None => {
let full = complete_partial_for_new(partial)
apply_descriptor_to_bag(arr.bag, key, full, partial.value)
true
}
Some(current) => {
if !current.configurable {
let cur_val = match arr.bag.symbol_properties.get(sym.id) {
Some(v) => v
None => Undefined
}
if !is_compatible_with_non_configurable(current, partial, cur_val) {
return false
}
}
let merged = merge_partial_into(current, partial)
let final_desc = if !merged.is_accessor && current.is_accessor {
{ ..merged, getter: None, setter: None }
} else if merged.is_accessor && !current.is_accessor {
{ ..merged, writable: false }
} else {
merged
}
apply_descriptor_to_bag(arr.bag, key, final_desc, partial.value)
true
}
}
}
_ => {
// ToPropertyKey-aligned coercion (matches ordinary_define_own_property's
// apply_descriptor_to_bag path). Value::to_string() produces a debug
// string for object keys; to_js_string invokes ToPrimitive/toString.
let k = to_js_string(key)
if k == "length" {
return self.array_set_length(arr, partial)
}
// Is this an array index? Parse through Int64 so large sparse indices
// never wrap through MoonBit Int or force dense materialization.
let index64 : Int64? = try {
let n = @string.parse_double(k)
let i64 = n.to_int64()
if n >= 0.0 &&
n == i64.to_double() &&
i64 <= 4294967294L &&
i64.to_string() == k {
Some(i64)
} else {
None
}
} catch {
_ => None
}
match index64 {
Some(idx64) => {
let existing_override = get_array_length_override(arr)
let logical_len = match existing_override {
Some(n64) => n64
None => arr.elements.length().to_int64()
}
if idx64 >= logical_len && !arr.length_writable {
return false
}
if idx64 <= ARRAY_DENSE_MATERIALIZE_LIMIT_I64 {
let idx = idx64.to_int()
let had_index = idx < arr.elements.length() &&
!arr.holes.contains(idx)
if !had_index && !arr.extensible {
return false
}
// Grow elements to idx+1 with Undefined filler. Intermediate
// indices (old_length..idx) become spec-level holes; the target
// index `idx` is real and has its hole flag cleared below.
while arr.elements.length() < idx {
let pad_idx = arr.elements.length()
arr.elements.push(Undefined)
arr.holes[pad_idx] = ()
}
while arr.elements.length() <= idx {
arr.elements.push(Undefined)
}
match arr.bag.descriptors.get(k) {
Some(current) => {
if !current.configurable {
if !is_compatible_with_non_configurable(
current,
partial,
arr.elements[idx],
) {
return false
}
}
let merged = merge_partial_into(current, partial)
// Clear fields that become invalid after a descriptor-kind transition.
arr.bag.descriptors[k] = if !merged.is_accessor &&
current.is_accessor {
{ ..merged, getter: None, setter: None }
} else if merged.is_accessor && !current.is_accessor {
{ ..merged, writable: false }
} else {
merged
}
}
None =>
if had_index {
let current = {
writable: true,
enumerable: true,
configurable: true,
getter: None,
setter: None,
is_accessor: false,
}
arr.bag.descriptors[k] = merge_partial_into(current, partial)
} else {
arr.bag.descriptors[k] = complete_partial_for_new(partial)
}
}
arr.holes.remove(idx)
match partial.value {
Some(v) => arr.elements[idx] = v
None => ()
}
let new_len = idx64 + 1L
if existing_override is Some(_) && new_len > logical_len {
set_array_length_override(arr, new_len)
}
} else {
let existing = ordinary_get_own_string_desc(arr.bag, k)
match existing {
None => {
if !arr.extensible {
return false
}
let full = complete_partial_for_new(partial)
apply_descriptor_to_bag(arr.bag, key, full, partial.value)
}
Some(current) => {
if !current.configurable {
let cur_val = match arr.bag.properties.get(k) {
Some(v) => v
None => Undefined
}
if !is_compatible_with_non_configurable(
current, partial, cur_val,
) {
return false
}
}
let merged = merge_partial_into(current, partial)
let final_desc = if !merged.is_accessor && current.is_accessor {
{ ..merged, getter: None, setter: None }
} else if merged.is_accessor && !current.is_accessor {
{ ..merged, writable: false }
} else {
merged
}
apply_descriptor_to_bag(arr.bag, key, final_desc, partial.value)
}
}
let new_len = idx64 + 1L
if new_len > logical_len {
set_array_length_override(arr, new_len)
}
}
return true
}
None => ()
}
// Named non-index property — ordinary write against bag.
let existing = ordinary_get_own_string_desc(arr.bag, k)
match existing {
None => {
if !arr.extensible {
return false
}
let full = complete_partial_for_new(partial)
apply_descriptor_to_bag(arr.bag, key, full, partial.value)
true
}
Some(current) => {
if !current.configurable {
let cur_val = match arr.bag.properties.get(k) {
Some(v) => v
None => Undefined
}
if !is_compatible_with_non_configurable(current, partial, cur_val) {
return false
}
}
let merged = merge_partial_into(current, partial)
let final_desc = if !merged.is_accessor && current.is_accessor {
{ ..merged, getter: None, setter: None }
} else if merged.is_accessor && !current.is_accessor {
{ ..merged, writable: false }
} else {
merged
}
apply_descriptor_to_bag(arr.bag, key, final_desc, partial.value)
true
}
}
}
}
}
///|
/// ES §10.4.2.4 `ArraySetLength`. The partial truncation loop is the
/// crux of this algorithm — see the Learn by Doing contribution point.
pub fn Interpreter::array_set_length(
self : Interpreter,
arr : ArrayData,
partial : PartialDescriptor,
) -> Bool raise Error {
let _ = self
// §15.4.5.1 step 3.a.i: length is always a non-configurable data property.
// Trying to make it an accessor → TypeError.
if partial.is_accessor() {
return false
}
// length is always non-configurable and non-enumerable — reject any
// attempt to flip those attributes regardless of whether value is present.
if partial.configurable is Some(true) {
return false
}
if partial.enumerable is Some(true) {
return false
}
// Step 1-2: if Desc.[[Value]] absent, just adjust attributes.
let new_len_value = match partial.value {
Some(v) => v
None => {
// No value change — only length_writable / other attrs. Validate:
// can't flip non-configurable (length is always non-configurable).
if partial.configurable is Some(true) {
return false
}
match partial.enumerable {
Some(true) => return false
_ => ()
}
// writable false->true is forbidden once frozen.
if !arr.length_writable && partial.writable is Some(true) {
return false
}
match partial.writable {
Some(false) => arr.length_writable = false
_ => ()
}
return true
}
}
// Step 3-5: coerce to uint32 and validate. Use Int64 and match the
// set_property "length" path (§7.1.6 ToUint32 + integer-valued check):
// n must be a non-NaN integer in [0, 2^32-1]. Int32 narrowing would
// incorrectly reject valid uint32 values > 2^31-1.
let n = self.to_number(new_len_value)
if n.is_nan() || n < 0.0 || n > 4294967295.0 || n != @math.floor(n) {
raise @errors.RangeError(message="Invalid array length")
}
let new_len64 = n.to_int64()
let cur_len64 = match get_array_length_override(arr) {
Some(len) => len
None => arr.elements.length().to_int64()
}
// Step 6: extension requires writable length; no-op (equal) always succeeds.
if new_len64 > cur_len64 {
if !arr.length_writable {
return false
}
// Mirror the bounded grow in `set_property`'s length path
// (property_set.mbt §10.4.2.4 step 3.b): only materialise small gaps.
// Larger gaps fall back to the logical-length override so something like
// `Object.defineProperty(arr, "length", { value: 1e9 })` doesn't try to
// allocate a billion slots.
let physical_len64 = arr.elements.length().to_int64()
if new_len64 > physical_len64 &&
new_len64 - physical_len64 <= 100000L &&
new_len64 <= 0x7FFFFFFFL {
let prev_len = arr.elements.length()
while arr.elements.length().to_int64() < new_len64 {
arr.elements.push(Undefined)
}
for j in prev_len.. arr.elements.length().to_int64() {
set_array_length_override(arr, new_len64)
} else {
clear_array_length_override(arr)
}
match partial.writable {
Some(false) => arr.length_writable = false
_ => ()
}
return true
}
if new_len64 == cur_len64 {
match partial.writable {
Some(false) => arr.length_writable = false
_ => ()
}
return true
}
// Step 7: if length is non-writable, truncation rejected.
if !arr.length_writable {
return false
}
// Step 8-9: defer writable:false until after truncation; remember intent.
let lock_after = partial.writable is Some(false)
match cleanup_sparse_array_indices_above_length(arr, new_len64) {
Some(blocked_len) => {
set_array_length_override(arr, blocked_len)
if lock_after {
arr.length_writable = false
}
return false
}
None => ()
}
// Step 10: partial-truncation loop. Delete indices in descending order.
// `new_len64` safely fits in Int for truncation targets (must be less
// than current length, which is bounded by JS engine).
let new_len = if new_len64 > 0x7FFFFFFFL {
0x7FFFFFFF
} else {
new_len64.to_int()
}
let mut i = arr.elements.length() - 1
while i >= new_len {
// Per §10.4.2.4 step 14.b: if element is non-configurable, stop.
match arr.bag.descriptors.get(i.to_string()) {
Some(desc) =>
if !desc.configurable {
let blocked_len = i.to_int64() + 1L
if blocked_len > arr.elements.length().to_int64() {
set_array_length_override(arr, blocked_len)
} else {
clear_array_length_override(arr)
}
if lock_after {
arr.length_writable = false
}
return false
}
None => ()
}
let _removed = arr.elements.pop()
let _ = arr.bag.descriptors.remove(i.to_string())
let _ = arr.holes.remove(i)
i = i - 1
}
cleanup_holes_above_length(arr.holes, new_len64)
if new_len64 > arr.elements.length().to_int64() {
set_array_length_override(arr, new_len64)
} else {
clear_array_length_override(arr)
}
if lock_after {
arr.length_writable = false
}
true
}