///|
fn array_index64_from_string(prop : String) -> Int64? {
  let idx = @string.parse_int(prop) catch { _ => -1 }
  if idx >= 0 && idx.to_string() == prop {
    return Some(idx.to_int64())
  }
  try {
    let n = @string.parse_double(prop)
    let i64 = n.to_int64()
    if n >= 2147483648.0 &&
      n == i64.to_double() &&
      i64 <= 4294967294L &&
      i64.to_string() == prop {
      Some(i64)
    } else {
      None
    }
  } catch {
    _ => None
  }
}

///|
fn string_primitive_has_own_property(value : String, prop : String) -> Bool {
  if prop == "length" {
    return true
  }
  match array_index64_from_string(prop) {
    Some(idx) => idx < utf16_length(value).to_int64()
    None => false
  }
}

///|
fn Interpreter::set_array_sparse_index64(
  self : Interpreter,
  data : ArrayData,
  receiver : Value,
  prop : String,
  idx64 : Int64,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value raise Error {
  let logical_len64 = match get_array_length_override(data) {
    Some(n64) => n64
    None => data.elements.length().to_int64()
  }
  match data.bag.descriptors.get(prop) {
    Some(desc) =>
      match desc.setter {
        Some(setter) => {
          let _ = self.call_value(setter, receiver, [value], loc)
          return value
        }
        None =>
          if !desc.writable {
            if strict {
              raise @errors.TypeError(
                message="Cannot assign to read only property '\{prop}'",
              )
            }
            return value
          }
      }
    None => ()
  }
  let is_own = data.bag.properties.contains(prop) ||
    data.bag.descriptors.contains(prop)
  if !is_own {
    match
      self.set_missing_array_index_via_proto_chain(
        get_array_prototype(self.realm_state, data),
        receiver,
        prop,
        value,
        loc,
        strict,
      ) {
      Some(result) => return result
      None => ()
    }
  }
  // §10.4.2.1 step 3.b: extend requires length_writable.
  if idx64 >= logical_len64 && !data.length_writable {
    if strict {
      raise @errors.TypeError(
        message="Cannot add property \{idx64}, array's length is non-writable",
      )
    }
    return value
  }
  if !is_own && !data.extensible {
    if strict {
      raise @errors.TypeError(
        message="Cannot add property \{idx64}, object is not extensible",
      )
    }
    return value
  }
  set_array_named_prop(data, prop, value)
  let new_len = idx64 + 1L
  if new_len > logical_len64 {
    set_array_length_override(data, new_len)
  }
  value
}

///|
fn update_array_length_after_dense_index_write(
  data : ArrayData,
  new_len : Int64,
  logical_len64 : Int64,
) -> Unit {
  if new_len > logical_len64 &&
    (
      get_array_length_override(data) is Some(_) ||
      new_len > data.elements.length().to_int64()
    ) {
    set_array_length_override(data, new_len)
  }
}

///|
/// Stage B.1 [[Set]] dispatcher.
///
/// `receiver` threads through the prototype chain and proxy fallbacks per
/// ES §10.1.9 (OrdinarySet) and §10.5.9 (ProxyExoticObject.[[Set]]).
///
/// - Inherited setters are invoked with `receiver` as `this`.
/// - Trap-less Proxy forwards to `target.[[Set]](P, V, Receiver)`.
/// - When no own/inherited blocker exists and `receiver !== obj`, the write
///   lands on `receiver` via `define_value_on_receiver` (approximating
///   `CreateDataProperty(Receiver, P, V)` — a direct `[[DefineOwnProperty]]`
///   that bypasses `[[Set]]`, preventing the recursion loop that a naive
///   self-call would produce for nested trap-less proxies).
pub fn Interpreter::set_property(
  self : Interpreter,
  obj : Value,
  prop : String,
  value : Value,
  loc : @token.Loc,
  strict? : Bool = false,
  receiver? : Value = obj,
) -> Value raise Error {
  match obj {
    Proxy(proxy_data) =>
      proxy_set(self, proxy_data, prop, value, receiver, strict)
    Object(data) => {
      let receiver_is_self = strict_equal(obj, receiver)
      // Ordinary object-literal own-data writes can skip the TypedArray string
      // classifier and descriptor hash when the receiver is the object itself.
      if data.class_name == "Object" &&
        receiver_is_self &&
        (
          data.bag.descriptors.is_empty() ||
          !data.bag.descriptors.contains(prop)
        ) {
        match data.bag.properties.get(prop) {
          Some(_) => {
            data.bag.properties[prop] = value
            return value
          }
          None => ()
        }
      }
      // TypedArray indexed write via dot notation (numeric string prop).
      // Receiver-sensitive per §10.4.5.5 step 1.b:
      // - canonical-invalid AND O === Receiver → ToNumber(value), then no-op
      // - canonical-invalid AND O !== Receiver → no-op without coercion
      // - canonical valid AND O === Receiver → write to buffer
      // - canonical valid AND O !== Receiver → land on Receiver (OrdinarySet)
      // The classifier raises nothing; to_number does — keep the raising
      // call outside any try/catch so Symbol → TypeError propagates per
      // §10.4.5.16 (PR #75 wide-catch fix).
      if is_typedarray_class(data.class_name) {
        match classify_typedarray_string_key(prop) {
          Some(idx) =>
            return self.typedarray_set_dispatch(
              data,
              obj,
              receiver,
              String_(prop),
              idx,
              value,
              loc,
              strict,
            )
          None => () // not a canonical numeric string — fall through
        }
      }
      if receiver_is_self &&
        (
          data.bag.descriptors.is_empty() ||
          !data.bag.descriptors.contains(prop)
        ) {
        match data.bag.properties.get(prop) {
          Some(_) => {
            data.bag.properties[prop] = value
            return value
          }
          None => ()
        }
      }
      // Check for accessor descriptor (setter)
      match data.bag.descriptors.get(prop) {
        Some(desc) =>
          match desc.setter {
            Some(setter) => {
              let _ = self.call_value(setter, receiver, [value], loc)
              return value
            }
            None =>
              if desc.getter is Some(_) {
                if strict {
                  raise @errors.TypeError(
                    message="Cannot set property \{prop} which has only a getter",
                  )
                }
                return value
              } else if !desc.writable {
                if strict {
                  raise @errors.TypeError(
                    message="Cannot assign to read only property '\{prop}'",
                  )
                }
                return value
              }
          }
        None => {
          // Walk prototype chain for inherited setters. Per ES §10.1.9.2:
          // - Accessor getter-only / non-writable data → blocked.
          // - Inherited writable data (explicit descriptor OR implicit via
          //   bag.properties): TERMINATES the walk (step 2 data-descriptor
          //   branch lands on Receiver without consulting higher protos).
          //   Missing this short-circuit lets a higher Proxy prototype's
          //   set trap fire with unexpected side effects.
          // - Proxy on chain: delegate to its [[Set]] (§10.1.9.2 step 1.b).
          let mut current = data.prototype
          while true {
            match current {
              Object(proto_data) => {
                if is_typedarray_class(proto_data.class_name) {
                  match classify_typedarray_string_key(prop) {
                    Some(idx) =>
                      return self.typedarray_set_dispatch(
                        proto_data,
                        current,
                        receiver,
                        String_(prop),
                        idx,
                        value,
                        loc,
                        strict,
                      )
                    None => ()
                  }
                }
                let mut stop_at_writable_data = false
                match proto_data.bag.descriptors.get(prop) {
                  Some(desc) =>
                    match desc.setter {
                      Some(setter) => {
                        let _ = self.call_value(setter, receiver, [value], loc)
                        return value
                      }
                      None =>
                        if desc.getter is Some(_) {
                          if strict {
                            raise @errors.TypeError(
                              message="Cannot set property \{prop} which has only a getter",
                            )
                          }
                          return value
                        } else if !desc.writable {
                          if strict {
                            raise @errors.TypeError(
                              message="Cannot assign to read only property '\{prop}'",
                            )
                          }
                          return value
                        } else {
                          // Writable data descriptor → stop walking.
                          stop_at_writable_data = true
                        }
                    }
                  None =>
                    // No explicit descriptor — plain assignments go to
                    // bag.properties and carry an implicit writable data
                    // descriptor per spec.
                    if proto_data.bag.properties.contains(prop) {
                      stop_at_writable_data = true
                    }
                }
                if stop_at_writable_data {
                  break
                }
                current = proto_data.prototype
              }
              Proxy(_) =>
                return self.set_property(
                  current,
                  prop,
                  value,
                  loc,
                  strict~,
                  receiver~,
                )
              _ => break
            }
          }
        }
      }
      // No own / inherited blocker — land the write on `receiver` per
      // ES §10.1.9.2 step 3. When `receiver !== obj` this goes through
      // `define_value_on_receiver` (approximates `CreateDataProperty`).
      if !strict_equal(obj, receiver) {
        return self.define_value_on_receiver(
          receiver,
          String_(prop),
          value,
          loc,
          strict,
        )
      }
      // Common path: receiver === obj, write locally (byte-identical to pre-B.1).
      if !data.bag.properties.contains(prop) && !data.extensible {
        if strict {
          raise @errors.TypeError(
            message="Cannot add property \{prop}, object is not extensible",
          )
        }
        return value
      }
      data.bag.properties[prop] = value
      value
    }
    Array(data) => {
      // Arrays use ordinary [[Set]] (only [[DefineOwnProperty]] is exotic per
      // §10.4.2). When `receiver !== obj` (Array), land on the receiver via
      // the helper — matches `Reflect.set(arr, "0", 1, otherObj)` semantics.
      if !strict_equal(obj, receiver) {
        return self.define_value_on_receiver(
          receiver,
          String_(prop),
          value,
          loc,
          strict,
        )
      }
      if prop == "length" {
        // Ordinary [[Set]] rejects non-writable Array length even when the
        // numeric value is unchanged (push()/pop() with no length change).
        if !data.length_writable {
          if strict {
            raise @errors.TypeError(
              message="Cannot assign to read only property 'length' of array",
            )
          }
          return value
        }
        let ok = self.array_set_length(
          data,
          PartialDescriptor::value_only(value),
        )
        if !ok && strict {
          raise @errors.TypeError(message="Cannot delete property")
        }
      } else {
        // Parse numeric index OUTSIDE any try/catch so strict-mode TypeError
        // raises propagate (Codex blocker 1 fix — the previous `catch { _ =>
        // () }` was swallowing §10.4.2.1 step 3.b TypeErrors and falling
        // through to set_array_named_prop).
        // Only canonical numeric index strings (e.g. "0", "1", "42") are
        // treated as array indices. Non-canonical forms like "01", "1.0",
        // "+1" go to named properties per §10.4.2.1 / §7.1.21
        // CanonicalNumericIndexString.
        let i = @string.parse_int(prop) catch { _ => -1 }
        if i >= 0 && i.to_string() == prop {
          // Compare against logical length (override when set), not physical.
          let logical_len64 = match get_array_length_override(data) {
            Some(n64) => n64
            None => data.elements.length().to_int64()
          }
          match data.bag.descriptors.get(prop) {
            Some(desc) =>
              match desc.setter {
                Some(setter) => {
                  let _ = self.call_value(setter, receiver, [value], loc)
                  return value
                }
                None =>
                  if !desc.writable {
                    if strict {
                      raise @errors.TypeError(
                        message="Cannot assign to read only property '\{prop}'",
                      )
                    }
                    return value
                  }
              }
            None => ()
          }
          let is_sparse_own = data.bag.properties.contains(prop) ||
            data.bag.descriptors.contains(prop)
          let is_own = (i < data.elements.length() && !data.holes.contains(i)) ||
            is_sparse_own
          if !is_own {
            match
              self.set_missing_array_index_via_proto_chain(
                get_array_prototype(self.realm_state, data),
                receiver,
                prop,
                value,
                loc,
                strict,
              ) {
              Some(result) => return result
              None => ()
            }
          }
          // §10.4.2.1 step 3.b: extend requires length_writable.
          if i.to_int64() >= logical_len64 && !data.length_writable {
            if strict {
              raise @errors.TypeError(
                message="Cannot add property \{i}, array's length is non-writable",
              )
            }
            return value
          }
          if !is_own && !data.extensible {
            if strict {
              raise @errors.TypeError(
                message="Cannot add property \{i}, object is not extensible",
              )
            }
            return value
          }
          let gap = i.to_int64() - data.elements.length().to_int64()
          if gap <= 100000L {
            let prev_len = data.elements.length()
            while data.elements.length() <= i {
              data.elements.push(Undefined)
            }
            for j in prev_len..
              return self.set_array_sparse_index64(
                data, receiver, prop, idx64, value, loc, strict,
              )
            None => ()
          }
          match data.bag.descriptors.get(prop) {
            Some(desc) =>
              match desc.setter {
                Some(setter) => {
                  let _ = self.call_value(setter, receiver, [value], loc)
                  return value
                }
                None =>
                  if !desc.writable {
                    if strict {
                      raise @errors.TypeError(
                        message="Cannot assign to read only property '\{prop}'",
                      )
                    }
                    return value
                  } else {
                    data.bag.properties[prop] = value
                    return value
                  }
              }
            None => {
              // Walk Array's prototype chain for an inherited setter/blocker.
              let proto_start = match get_array_prototype_override(data) {
                Some(p) => p
                None => {
                  let ctor = self.global.get("Array") catch { _ => Null }
                  match ctor {
                    Object(ctor_data) =>
                      match ctor_data.bag.properties.get("prototype") {
                        Some(p) => p
                        None => Null
                      }
                    _ => Null
                  }
                }
              }
              let mut current = proto_start
              let mut blocked = false
              while true {
                match current {
                  Object(proto_data) =>
                    match proto_data.bag.descriptors.get(prop) {
                      Some(desc) =>
                        match desc.setter {
                          Some(setter) => {
                            let _ = self.call_value(
                              setter,
                              receiver,
                              [value],
                              loc,
                            )
                            return value
                          }
                          None =>
                            if desc.getter is Some(_) {
                              if strict {
                                raise @errors.TypeError(
                                  message="Cannot set property \{prop} which has only a getter",
                                )
                              }
                              blocked = true
                              break
                            } else if !desc.writable {
                              if strict {
                                raise @errors.TypeError(
                                  message="Cannot assign to read only property '\{prop}'",
                                )
                              }
                              blocked = true
                              break
                            } else {
                              break // writable data: land on receiver
                            }
                        }
                      None =>
                        match proto_data.bag.properties.get(prop) {
                          Some(_) => break // writable data: land on receiver
                          None => current = proto_data.prototype
                        }
                    }
                  _ => break
                }
              }
              if !blocked {
                if !data.extensible {
                  if strict {
                    raise @errors.TypeError(
                      message="Cannot add property \{prop}, array is not extensible",
                    )
                  }
                  return value
                }
                set_array_named_prop(data, prop, value)
              }
            }
          }
        }
      }
      value
    }
    Promise(data) =>
      self.set_string_on_map_set_expando(
        obj,
        receiver,
        data.bag,
        self.promise_instance_prototype_for_set(data),
        prop,
        value,
        loc,
        strict,
      )
    Map(data) =>
      self.set_string_on_map_set_expando(
        obj,
        receiver,
        data.bag,
        data.prototype.unwrap_or_else(fn() { self.realm_state.get_map_proto() }),
        prop,
        value,
        loc,
        strict,
      )
    Set(data) =>
      self.set_string_on_map_set_expando(
        obj,
        receiver,
        data.bag,
        data.prototype.unwrap_or_else(fn() { self.realm_state.get_set_proto() }),
        prop,
        value,
        loc,
        strict,
      )
    Null =>
      raise @errors.TypeError(
        message="Cannot set properties of null (setting '\{prop}')",
      )
    Undefined =>
      raise @errors.TypeError(
        message="Cannot set properties of undefined (setting '\{prop}')",
      )
    _ => {
      // PutValue temporarily boxes String/Number/Boolean, but passes the
      // original primitive as Receiver to [[Set]]. Avoid allocating that
      // wrapper: walk the matching intrinsic prototype directly and preserve
      // the primitive receiver when invoking an inherited setter.
      //
      // String wrappers have non-writable own `length` and indexed properties;
      // those must block a same-named setter higher in the prototype chain.
      let proto = match obj {
        String_(string) =>
          if string_primitive_has_own_property(string, prop) {
            None
          } else {
            Some(self.realm_state.get_string_proto())
          }
        Number(_) => Some(self.realm_state.get_number_proto())
        Bool(_) => Some(self.realm_state.get_boolean_proto())
        _ => None
      }
      match proto {
        Some(primitive_proto) =>
          match
            self.set_key_via_map_set_prototype_chain(
              primitive_proto,
              receiver,
              String_(prop),
              value,
              loc,
              strict,
            ) {
            Some(result) => return result
            None => ()
          }
        None => ()
      }
      // Without an inherited setter, the temporary primitive wrapper cannot
      // retain a new own property. Sloppy assignment is ignored; strict
      // assignment reports failure.
      if strict {
        raise @errors.TypeError(
          message="Cannot set property '\{prop}' of \{type_of(obj)}",
        )
      } else {
        value
      }
    }
  }
}

///|
/// Map/Set/Promise carry their own prototype slot, so string-key [[Set]] must
/// honor inherited setters and inherited blocker descriptors before creating
/// an expando property on the receiver.
fn Interpreter::set_string_via_map_set_prototype_chain(
  self : Interpreter,
  proto : Value,
  receiver : Value,
  prop : String,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value? raise Error {
  self.set_key_via_map_set_prototype_chain(
    proto,
    receiver,
    String_(prop),
    value,
    loc,
    strict,
  )
}

///|
fn Interpreter::promise_instance_prototype_for_set(
  self : Interpreter,
  data : PromiseData,
) -> Value {
  data.prototype.unwrap_or_else(fn() { self.realm_state.get_promise_proto() })
}

///|
fn Interpreter::next_map_set_walk_prototype(
  self : Interpreter,
  current : Value,
) -> Value? {
  match current {
    Object(data) => Some(data.prototype)
    Array(data) =>
      Some(
        get_array_prototype_override(data).unwrap_or(
          self.realm_state.get_array_proto(),
        ),
      )
    Map(data) =>
      Some(
        data.prototype.unwrap_or_else(fn() { self.realm_state.get_map_proto() }),
      )
    Set(data) =>
      Some(
        data.prototype.unwrap_or_else(fn() { self.realm_state.get_set_proto() }),
      )
    Promise(data) => Some(self.promise_instance_prototype_for_set(data))
    _ => None
  }
}

///|
priv enum InheritedSetResult {
  Handled(Value)
  Continue(Value)
  Stop
}

///|
fn Interpreter::set_key_via_map_set_prototype_chain(
  self : Interpreter,
  proto : Value,
  receiver : Value,
  prop_key : Value,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value? raise Error {
  let mut current = proto
  while true {
    match self.next_map_set_walk_prototype(current) {
      Some(next_proto) =>
        match
          self.check_inherited_property_value(
            current, next_proto, receiver, prop_key, value, loc, strict,
          ) {
          Handled(result) => return Some(result)
          Continue(next) => current = next
          Stop => break
        }
      None =>
        match current {
          Proxy(_) =>
            match prop_key {
              String_(prop) =>
                return Some(
                  self.set_property(
                    current,
                    prop,
                    value,
                    loc,
                    strict~,
                    receiver~,
                  ),
                )
              Symbol(_) =>
                return Some(
                  self.set_computed_property(
                    current,
                    prop_key,
                    value,
                    loc,
                    strict~,
                    receiver~,
                  ),
                )
              _ => break
            }
          _ => break
        }
    }
  }
  None
}

///|
fn Interpreter::check_inherited_property_value(
  self : Interpreter,
  owner : Value,
  next_proto : Value,
  receiver : Value,
  prop_key : Value,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> InheritedSetResult raise Error {
  match self.get_own_property(owner, prop_key) {
    Some((desc, _)) =>
      match desc.setter {
        Some(setter) => {
          let _ = self.call_value(setter, receiver, [value], loc)
          Handled(value)
        }
        None =>
          if desc.getter is Some(_) {
            if strict {
              raise @errors.TypeError(
                message=inherited_property_getter_only_message(prop_key),
              )
            }
            Handled(value)
          } else if !desc.writable {
            if strict {
              raise @errors.TypeError(
                message=inherited_property_readonly_message(prop_key),
              )
            }
            Handled(value)
          } else {
            Stop
          }
      }
    None => Continue(next_proto)
  }
}

///|
fn inherited_property_getter_only_message(prop_key : Value) -> String {
  match prop_key {
    String_(prop) => "Cannot set property \{prop} which has only a getter"
    _ => "Cannot set property which has only a getter"
  }
}

///|
fn inherited_property_readonly_message(prop_key : Value) -> String {
  match prop_key {
    String_(prop) => "Cannot assign to read only property '\{prop}'"
    _ => "Cannot assign to read only property"
  }
}

///|
fn Interpreter::set_string_on_map_set_expando(
  self : Interpreter,
  obj : Value,
  receiver : Value,
  bag : PropertyBag,
  proto : Value,
  prop : String,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value raise Error {
  let mut should_walk_proto = false
  match bag.descriptors.get(prop) {
    Some(desc) =>
      match desc.setter {
        Some(setter) => {
          let _ = self.call_value(setter, receiver, [value], loc)
          return value
        }
        None =>
          if desc.getter is Some(_) {
            if strict {
              raise @errors.TypeError(
                message="Cannot set property \{prop} which has only a getter",
              )
            }
            return value
          } else if !desc.writable {
            if strict {
              raise @errors.TypeError(
                message="Cannot assign to read only property '\{prop}'",
              )
            }
            return value
          }
      }
    None => if !bag.properties.contains(prop) { should_walk_proto = true }
  }
  if should_walk_proto {
    match
      self.set_string_via_map_set_prototype_chain(
        proto, receiver, prop, value, loc, strict,
      ) {
      Some(result) => return result
      None => ()
    }
  }
  if !strict_equal(obj, receiver) {
    return self.define_value_on_receiver(
      receiver,
      String_(prop),
      value,
      loc,
      strict,
    )
  }
  self.landing_via_define_own_property(
    receiver,
    String_(prop),
    value,
    loc,
    strict,
  )
}

///|
fn Interpreter::set_symbol_via_map_set_prototype_chain(
  self : Interpreter,
  proto : Value,
  receiver : Value,
  sym : SymbolData,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value? raise Error {
  self.set_key_via_map_set_prototype_chain(
    proto,
    receiver,
    Symbol(sym),
    value,
    loc,
    strict,
  )
}

///|
fn Interpreter::set_symbol_on_map_set_expando(
  self : Interpreter,
  obj : Value,
  receiver : Value,
  bag : PropertyBag,
  proto : Value,
  sym : SymbolData,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value raise Error {
  let mut should_walk_proto = false
  match bag.symbol_descriptors.get(sym.id) {
    Some(desc) =>
      match desc.setter {
        Some(setter) => {
          let _ = self.call_value(setter, receiver, [value], loc)
          return value
        }
        None =>
          if desc.getter is Some(_) {
            if strict {
              raise @errors.TypeError(
                message="Cannot set property which has only a getter",
              )
            }
            return value
          } else if !desc.writable {
            if strict {
              raise @errors.TypeError(
                message="Cannot assign to read only property",
              )
            }
            return value
          }
      }
    None =>
      if !bag.symbol_properties.contains(sym.id) {
        should_walk_proto = true
      }
  }
  if should_walk_proto {
    match
      self.set_symbol_via_map_set_prototype_chain(
        proto, receiver, sym, value, loc, strict,
      ) {
      Some(result) => return result
      None => ()
    }
  }
  if !strict_equal(obj, receiver) {
    return self.define_value_on_receiver(
      receiver,
      Symbol(sym),
      value,
      loc,
      strict,
    )
  }
  self.landing_via_define_own_property(
    receiver,
    Symbol(sym),
    value,
    loc,
    strict,
  )
}

///|
/// Helper predicate for §10.1.9.2 step 3.e: an existing receiver-side
/// descriptor blocks the landing write if it is an accessor descriptor
/// (getter or setter present) or a non-writable data descriptor. Writable
/// data descriptors do NOT block (the write lands as { [[Value]]: V }).
fn existing_receiver_desc_blocks(desc : PropDescriptor) -> Bool {
  desc.getter is Some(_) ||
  desc.setter is Some(_) ||
  (desc.getter is None && desc.setter is None && !desc.writable)
}

///|
/// ES §10.1.9.2 step 3 landing via [[DefineOwnProperty]]. Looks up the
/// existing own descriptor on receiver (honoring Proxy `getOwnPropertyDescriptor`
/// trap), then dispatches:
/// - existing is accessor / non-writable data -> blocker (strict throws,
///   sloppy returns `value` silently).
/// - existing is writable data -> call [[DefineOwnProperty]] with value-only
///   (§10.1.9.2 step 3.e). Preserves existing enumerable/configurable.
/// - no existing -> call [[DefineOwnProperty]] with CreateDataProperty shape
///   (§10.1.9.2 step 3.f).
/// If [[DefineOwnProperty]] returns false: strict throws, sloppy returns value.
fn Interpreter::landing_via_define_own_property(
  self : Interpreter,
  receiver : Value,
  key : Value,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value raise Error {
  // Look up existing own descriptor on receiver.
  let existing = self.get_own_property(receiver, key)
  match existing {
    Some((desc, _)) if existing_receiver_desc_blocks(desc) => {
      if strict {
        let key_display = match key {
          Symbol(sym) => "Symbol(\{sym.description.unwrap_or_default()})"
          _ => to_js_string(key, interp=Some(self))
        }
        raise @errors.TypeError(
          message="Cannot redefine non-writable or accessor property '\{key_display}' on receiver",
        )
      }
      return value
    }
    _ => ()
  }
  // Choose partial shape per step 3.e vs 3.f.
  let partial = match existing {
    Some(_) => PartialDescriptor::value_only(value) // step 3.e
    None => PartialDescriptor::data_default(value) // step 3.f
  }
  let ok = self.define_own_property(receiver, key, partial, loc)
  if !ok {
    if strict {
      let key_display = match key {
        Symbol(sym) => "Symbol(\{sym.description.unwrap_or_default()})"
        _ => to_js_string(key, interp=Some(self))
      }
      raise @errors.TypeError(
        message="Cannot assign to property '\{key_display}' on receiver",
      )
    }
  }
  value
}

///|
/// §10.4.5.5 step 1.b TypedArray `[[Set]]` dispatch for a canonical-numeric
/// index whose classifier already returned `Some(idx)` (`idx == -1` means a
/// canonical numeric string that is not a valid integer-index form).
/// Called from the three TypedArray intercept sites (`set_property` string
/// path, `set_computed_property` `Number` and `String_` paths) to centralize
/// the same-receiver, invalid-index, and cross-receiver landing cases.
///
/// - `O === Receiver` → `TypedArraySetElement`: coerce `value`, then write to
///   the buffer only when `idx` is a valid in-range integer index.
/// - `O !== Receiver` AND `!IsValidIntegerIndex` → no-op per step 1.b.ii,
///   without coercing `value`.
/// - `O !== Receiver` AND in-range → `OrdinarySet` on `Receiver` (lands via
///   `Receiver.[[DefineOwnProperty]]`; the TypedArray's backing buffer is untouched).
///
/// The `to_number(value)` raise must remain outside the classifier's
/// try/catch: Symbol → TypeError must propagate per §10.4.5.16 (the PR #75
/// wide-catch fix).
fn Interpreter::typedarray_set_dispatch(
  self : Interpreter,
  data : ObjectData,
  obj : Value,
  receiver : Value,
  key : Value,
  idx : Int,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value raise Error {
  if strict_equal(obj, receiver) {
    let num = self.to_number(value)
    if idx >= 0 {
      (self.stdlib_hooks.typedarray_set_index)(data, idx, num, self.realm_state)
    }
    value
  } else if idx < 0 ||
    !(self.stdlib_hooks.typedarray_is_valid_index)(data, idx, self.realm_state) {
    value
  } else {
    let landing_key = match key {
      String_(_) => key
      _ => String_(to_js_string(key, interp=Some(self)))
    }
    self.define_value_on_receiver(receiver, landing_key, value, loc, strict)
  }
}

///|
/// Array uses ordinary [[Set]], so a missing array index delegates through the
/// full prototype chain before Array [[DefineOwnProperty]] can extend length.
/// This keeps TypedArray integer-index semantics observable even behind
/// intermediate ordinary prototypes, while preserving ordinary prototype
/// blockers (setters, getter-only accessors, and non-writable data props).
fn Interpreter::set_missing_array_index_via_proto_chain(
  self : Interpreter,
  proto : Value,
  receiver : Value,
  prop : String,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value? raise Error {
  let mut current = proto
  while true {
    match current {
      Object(proto_data) => {
        if is_typedarray_class(proto_data.class_name) {
          match classify_typedarray_string_key(prop) {
            Some(idx) =>
              return Some(
                self.typedarray_set_dispatch(
                  proto_data,
                  current,
                  receiver,
                  String_(prop),
                  idx,
                  value,
                  loc,
                  strict,
                ),
              )
            None => ()
          }
        }
        match proto_data.bag.descriptors.get(prop) {
          Some(desc) =>
            match desc.setter {
              Some(setter) => {
                let _ = self.call_value(setter, receiver, [value], loc)
                return Some(value)
              }
              None =>
                if desc.getter is Some(_) {
                  if strict {
                    raise @errors.TypeError(
                      message="Cannot set property \{prop} which has only a getter",
                    )
                  }
                  return Some(value)
                } else if !desc.writable {
                  if strict {
                    raise @errors.TypeError(
                      message="Cannot assign to read only property '\{prop}'",
                    )
                  }
                  return Some(value)
                } else {
                  return Some(
                    self.define_value_on_receiver(
                      receiver,
                      String_(prop),
                      value,
                      loc,
                      strict,
                    ),
                  )
                }
            }
          None =>
            if proto_data.bag.properties.contains(prop) {
              return Some(
                self.define_value_on_receiver(
                  receiver,
                  String_(prop),
                  value,
                  loc,
                  strict,
                ),
              )
            }
        }
        current = proto_data.prototype
      }
      Proxy(_) =>
        return Some(
          self.set_property(current, prop, value, loc, strict~, receiver~),
        )
      _ => return None
    }
  }
  None
}

///|
/// Approximates ES `CreateDataProperty(Receiver, P, V)` / `Receiver.[[Define\
/// OwnProperty]]` for the [[Set]] landing rule. This is NOT `[[Set]]` — it
/// must not invoke setters on `receiver`'s prototype chain, otherwise a
/// trap-less proxy chain would infinite-loop (per Codex's design review).
///
/// Implements §10.1.9.2 step 3.e as of the receiver-blocker fix: if receiver
/// has an existing own descriptor at `key`, accessor or non-writable data
/// blocks the landing (sloppy returns `value`, strict raises TypeError).
///
/// Object, Array, Map, Set, Promise, and Proxy receivers route through the
/// canonical `landing_via_define_own_property` dispatcher where their
/// [[DefineOwnProperty]] semantics decide whether the write succeeds.
fn Interpreter::define_value_on_receiver(
  self : Interpreter,
  receiver : Value,
  key : Value,
  value : Value,
  loc : @token.Loc,
  strict : Bool,
) -> Value raise Error {
  match receiver {
    // §10.1.9.2 step 3: look up receiver's own descriptor; dispatch per
    // 3.e (existing writable-data) or 3.f (absent → CreateDataProperty).
    // Proxy receivers route through proxy_get_own_property +
    // proxy_define_property so the getOwnPropertyDescriptor and
    // defineProperty traps fire.
    Proxy(_) =>
      self.landing_via_define_own_property(receiver, key, value, loc, strict)
    Object(rdata) => {
      if rdata.class_name == "Module" || is_typedarray_class(rdata.class_name) {
        return self.landing_via_define_own_property(
          receiver, key, value, loc, strict,
        )
      }
      match key {
        Symbol(sym) => {
          // §10.1.9.2 step 3.e: existing own descriptor blocker check.
          match rdata.bag.symbol_descriptors.get(sym.id) {
            Some(desc) =>
              if existing_receiver_desc_blocks(desc) {
                if strict {
                  raise @errors.TypeError(
                    message="Cannot redefine non-writable or accessor property on receiver",
                  )
                }
                return value
              }
            None => ()
          }
          if !rdata.bag.symbol_properties.contains(sym.id) && !rdata.extensible {
            if strict {
              raise @errors.TypeError(
                message="Cannot add property, object is not extensible",
              )
            }
            return value
          }
          rdata.bag.symbol_properties[sym.id] = value
          value
        }
        _ => {
          let prop = to_js_string(key, interp=Some(self))
          match rdata.bag.descriptors.get(prop) {
            Some(desc) =>
              if existing_receiver_desc_blocks(desc) {
                if strict {
                  raise @errors.TypeError(
                    message="Cannot redefine non-writable or accessor property '\{prop}' on receiver",
                  )
                }
                return value
              }
            None => ()
          }
          if !rdata.bag.properties.contains(prop) && !rdata.extensible {
            if strict {
              raise @errors.TypeError(
                message="Cannot add property \{prop}, object is not extensible",
              )
            }
            return value
          }
          rdata.bag.properties[prop] = value
          value
        }
      }
    }
    Promise(_) | Map(_) | Set(_) =>
      self.landing_via_define_own_property(receiver, key, value, loc, strict)
    // §10.1.9.2 step 3 via [[DefineOwnProperty]] (§10.4.2.1 / §10.4.2.4 for
    // Array): existing writable `length` → `ArraySetLength` with just {Value};
    // absent index → `CreateDataProperty` with full defaults;
    // `length_writable: false` rejection honored. Routes through the Stage B.2
    // `landing_via_define_own_property` dispatcher rather than re-firing
    // Array's `[[Set]]` exotic path (which would be the wrong operation per
    // §10.5.9 step 5.b — landing must be `[[DefineOwnProperty]]`, not `[[Set]]`).
    Array(_) =>
      self.landing_via_define_own_property(receiver, key, value, loc, strict)
    // Null/Undefined/primitive receiver: per spec returns false. In strict
    // mode our dispatcher surface raises TypeError; in sloppy mode return
    // `value` to match existing set_property primitive handling.
    Null =>
      if strict {
        raise @errors.TypeError(message="Cannot set property on null receiver")
      } else {
        value
      }
    Undefined =>
      if strict {
        raise @errors.TypeError(
          message="Cannot set property on undefined receiver",
        )
      } else {
        value
      }
    _ =>
      if strict {
        raise @errors.TypeError(
          message="Cannot set property on primitive receiver",
        )
      } else {
        value
      }
  }
}

///|
pub fn Interpreter::set_computed_property(
  self : Interpreter,
  obj : Value,
  key : Value,
  value : Value,
  loc : @token.Loc,
  strict? : Bool = false,
  receiver? : Value = obj,
) -> Value raise Error {
  match obj {
    Proxy(proxy_data) => {
      let trap = get_proxy_trap(proxy_data, "set", self)
      match trap {
        Some(trap_fn) => {
          let target = get_proxy_target(proxy_data)
          let handler = get_proxy_handler(proxy_data)
          let prop_key = match key {
            Symbol(_) => key
            _ => String_(to_js_string(key, interp=Some(self)))
          }
          let result = self.call_value(
            trap_fn,
            handler,
            [target, prop_key, value, receiver],
            loc,
          )
          if !is_truthy(result) {
            if strict {
              raise @errors.TypeError(
                message="'set' on proxy: trap returned falsish",
              )
            }
            return value
          }
          // ES §10.5.9 step 11-12 invariants. Same checks proxy_set runs for
          // the string-key path; must run here so `p[k] = v` and the proto-
          // chain delegation (Proxy(_) arm of the walk) match spec.
          check_proxy_set_trap_invariants_with_interp(
            Some(self),
            target,
            prop_key,
            value,
          )
          return value
        }
        None => {
          // Trap-less fallback: target.[[Set]](P, V, Receiver) — thread
          // receiver through per ES §10.5.9 step 5.
          let target = get_proxy_target(proxy_data)
          return self.set_computed_property(
            target,
            key,
            value,
            loc,
            strict~,
            receiver~,
          )
        }
      }
    }
    Object(data) =>
      match key {
        Symbol(sym) => {
          // Check symbol property accessor descriptor (setter)
          match data.bag.symbol_descriptors.get(sym.id) {
            Some(desc) =>
              match desc.setter {
                Some(setter) => {
                  let _ = self.call_value(setter, receiver, [value], loc)
                  return value
                }
                None =>
                  if desc.getter is Some(_) {
                    if strict {
                      raise @errors.TypeError(
                        message="Cannot set property which has only a getter",
                      )
                    }
                    return value
                  } else if !desc.writable {
                    if strict {
                      raise @errors.TypeError(
                        message="Cannot assign to read only property",
                      )
                    }
                    return value
                  }
              }
            None => {
              // Walk prototype chain for inherited symbol setters. Writable
              // data on a proto (explicit or implicit via symbol_properties)
              // terminates the walk per §10.1.9.2; see string-key branch for
              // full rationale.
              let mut current = data.prototype
              while true {
                match current {
                  Object(proto_data) => {
                    let mut stop_at_writable_data = false
                    match proto_data.bag.symbol_descriptors.get(sym.id) {
                      Some(desc) =>
                        match desc.setter {
                          Some(setter) => {
                            let _ = self.call_value(
                              setter,
                              receiver,
                              [value],
                              loc,
                            )
                            return value
                          }
                          None =>
                            if desc.getter is Some(_) {
                              if strict {
                                raise @errors.TypeError(
                                  message="Cannot set property which has only a getter",
                                )
                              }
                              return value
                            } else if !desc.writable {
                              if strict {
                                raise @errors.TypeError(
                                  message="Cannot assign to read only property",
                                )
                              }
                              return value
                            } else {
                              stop_at_writable_data = true
                            }
                        }
                      None =>
                        if proto_data.bag.symbol_properties.contains(sym.id) {
                          stop_at_writable_data = true
                        }
                    }
                    if stop_at_writable_data {
                      break
                    }
                    current = proto_data.prototype
                  }
                  Proxy(_) =>
                    return self.set_computed_property(
                      current,
                      key,
                      value,
                      loc,
                      strict~,
                      receiver~,
                    )
                  _ => break
                }
              }
            }
          }
          // Landing rule: if receiver differs, write lands on receiver.
          if !strict_equal(obj, receiver) {
            return self.define_value_on_receiver(
              receiver, key, value, loc, strict,
            )
          }
          if !data.bag.symbol_properties.contains(sym.id) && !data.extensible {
            if strict {
              raise @errors.TypeError(
                message="Cannot add property, object is not extensible",
              )
            }
            return value
          }
          data.bag.symbol_properties[sym.id] = value
          value
        }
        _ => {
          // TypedArray indexed write: intercept numeric keys per
          // §10.4.5.5 [[Set]] (receiver-sensitive) + §10.4.5.16
          // IntegerIndexedElementSet.
          //
          // Per §10.4.5.5 step 1.b:
          // - If P is canonical numeric AND SameValue(O, Receiver):
          //   IntegerIndexedElementSet — write to buffer (or no-op if
          //   IsValidIntegerIndex is false).
          // - If P is canonical numeric AND O !== Receiver AND not a
          //   valid index: return true (no-op, do NOT land on receiver).
          // - If P is canonical numeric AND O !== Receiver AND valid
          //   index: fall through to OrdinarySet on Receiver (lands as
          //   ordinary data property on the receiver, NOT in O's buffer).
          if is_typedarray_class(data.class_name) {
            match key {
              Number(n) => {
                let idx = n.to_int()
                if !(idx.to_double() == n && idx >= 0) {
                  // Canonical numeric (Number always is) but not a valid
                  // integer-index form. Same-receiver writes still perform
                  // ToNumber(value); cross-receiver writes are no-ops.
                  return self.typedarray_set_dispatch(
                    data, obj, receiver, key, -1, value, loc, strict,
                  )
                }
                return self.typedarray_set_dispatch(
                  data, obj, receiver, key, idx, value, loc, strict,
                )
              }
              String_(s) =>
                match classify_typedarray_string_key(s) {
                  Some(idx) =>
                    return self.typedarray_set_dispatch(
                      data, obj, receiver, key, idx, value, loc, strict,
                    )
                  None => () // not canonical numeric → fall through
                }
              _ => ()
            }
          }
          let prop = to_js_string(key, interp=Some(self))
          // Check for accessor descriptor (setter) on string key
          match data.bag.descriptors.get(prop) {
            Some(desc) =>
              match desc.setter {
                Some(setter) => {
                  let _ = self.call_value(setter, receiver, [value], loc)
                  return value
                }
                None =>
                  if desc.getter is Some(_) {
                    if strict {
                      raise @errors.TypeError(
                        message="Cannot set property \{prop} which has only a getter",
                      )
                    }
                    return value
                  } else if !desc.writable {
                    if strict {
                      raise @errors.TypeError(
                        message="Cannot assign to read only property '\{prop}'",
                      )
                    }
                    return value
                  }
              }
            None => {
              // Walk prototype chain for inherited setters. Writable data on
              // a proto (explicit or implicit via bag.properties) terminates
              // the walk per §10.1.9.2; see set_property for full rationale.
              let mut current = data.prototype
              while true {
                match current {
                  Object(proto_data) => {
                    if is_typedarray_class(proto_data.class_name) {
                      match classify_typedarray_string_key(prop) {
                        Some(idx) =>
                          return self.typedarray_set_dispatch(
                            proto_data,
                            current,
                            receiver,
                            String_(prop),
                            idx,
                            value,
                            loc,
                            strict,
                          )
                        None => ()
                      }
                    }
                    let mut stop_at_writable_data = false
                    match proto_data.bag.descriptors.get(prop) {
                      Some(desc) =>
                        match desc.setter {
                          Some(setter) => {
                            let _ = self.call_value(
                              setter,
                              receiver,
                              [value],
                              loc,
                            )
                            return value
                          }
                          None =>
                            if desc.getter is Some(_) {
                              if strict {
                                raise @errors.TypeError(
                                  message="Cannot set property \{prop} which has only a getter",
                                )
                              }
                              return value
                            } else if !desc.writable {
                              if strict {
                                raise @errors.TypeError(
                                  message="Cannot assign to read only property '\{prop}'",
                                )
                              }
                              return value
                            } else {
                              stop_at_writable_data = true
                            }
                        }
                      None =>
                        if proto_data.bag.properties.contains(prop) {
                          stop_at_writable_data = true
                        }
                    }
                    if stop_at_writable_data {
                      break
                    }
                    current = proto_data.prototype
                  }
                  Proxy(_) =>
                    return self.set_computed_property(
                      current,
                      key,
                      value,
                      loc,
                      strict~,
                      receiver~,
                    )
                  _ => break
                }
              }
            }
          }
          // Landing rule: if receiver differs, write lands on receiver.
          if !strict_equal(obj, receiver) {
            return self.define_value_on_receiver(
              receiver,
              String_(prop),
              value,
              loc,
              strict,
            )
          }
          if !data.bag.properties.contains(prop) && !data.extensible {
            if strict {
              raise @errors.TypeError(
                message="Cannot add property \{prop}, object is not extensible",
              )
            }
            return value
          }
          data.bag.properties[prop] = value
          value
        }
      }
    Map(data) => {
      let map_proto = data.prototype.unwrap_or_else(fn() {
        self.realm_state.get_map_proto()
      })
      match key {
        Symbol(sym) =>
          self.set_symbol_on_map_set_expando(
            obj,
            receiver,
            data.bag,
            map_proto,
            sym,
            value,
            loc,
            strict,
          )
        _ => {
          let prop = to_js_string(key, interp=Some(self))
          self.set_string_on_map_set_expando(
            obj,
            receiver,
            data.bag,
            map_proto,
            prop,
            value,
            loc,
            strict,
          )
        }
      }
    }
    Set(data) => {
      let set_proto = data.prototype.unwrap_or_else(fn() {
        self.realm_state.get_set_proto()
      })
      match key {
        Symbol(sym) =>
          self.set_symbol_on_map_set_expando(
            obj,
            receiver,
            data.bag,
            set_proto,
            sym,
            value,
            loc,
            strict,
          )
        _ => {
          let prop = to_js_string(key, interp=Some(self))
          self.set_string_on_map_set_expando(
            obj,
            receiver,
            data.bag,
            set_proto,
            prop,
            value,
            loc,
            strict,
          )
        }
      }
    }
    Array(data) => {
      if !strict_equal(obj, receiver) {
        return self.define_value_on_receiver(receiver, key, value, loc, strict)
      }
      match key {
        Symbol(sym) => {
          set_array_symbol_prop(data, sym.id, value)
          value
        }
        Number(n) => {
          let prop64 = n.to_string()
          match array_index64_from_string(prop64) {
            Some(idx64) if idx64 > 0x7FFFFFFFL =>
              return self.set_array_sparse_index64(
                data, receiver, prop64, idx64, value, loc, strict,
              )
            _ => ()
          }
          let i = n.to_int()
          // Only treat as array index if n is an exact, non-negative integer.
          // Non-integer keys like 1.1 must be stored as named properties.
          if i >= 0 && i.to_double() == n {
            let prop = i.to_string()
            let logical_len64 = match get_array_length_override(data) {
              Some(n64) => n64
              None => data.elements.length().to_int64()
            }
            match data.bag.descriptors.get(prop) {
              Some(desc) =>
                match desc.setter {
                  Some(setter) => {
                    let _ = self.call_value(setter, receiver, [value], loc)
                    return value
                  }
                  None =>
                    if !desc.writable {
                      if strict {
                        raise @errors.TypeError(
                          message="Cannot assign to read only property '\{prop}'",
                        )
                      }
                      return value
                    }
                }
              None => ()
            }
            let is_sparse_own = data.bag.properties.contains(prop) ||
              data.bag.descriptors.contains(prop)
            let is_own = (i < data.elements.length() && !data.holes.contains(i)) ||
              is_sparse_own
            if !is_own {
              match
                self.set_missing_array_index_via_proto_chain(
                  get_array_prototype(self.realm_state, data),
                  receiver,
                  prop,
                  value,
                  loc,
                  strict,
                ) {
                Some(result) => return result
                None => ()
              }
            }
            // §10.4.2.1 step 3.b: extending beyond length requires length to be writable.
            if i.to_int64() >= logical_len64 && !data.length_writable {
              if strict {
                raise @errors.TypeError(
                  message="Cannot add property \{i}, array's length is non-writable",
                )
              }
              return value
            }
            if !is_own && !data.extensible {
              if strict {
                raise @errors.TypeError(
                  message="Cannot add property \{i}, object is not extensible",
                )
              }
              return value
            }
            let gap = i.to_int64() - data.elements.length().to_int64()
            if gap <= 100000L {
              let prev_len = data.elements.length()
              while data.elements.length() <= i {
                data.elements.push(Undefined)
              }
              for j in prev_len.. {
          let prop = to_js_string(key, interp=Some(self))
          if prop == "length" {
            // Ordinary [[Set]] rejects non-writable Array length even when the
            // numeric value is unchanged (push()/pop() with no length change).
            if !data.length_writable {
              if strict {
                raise @errors.TypeError(
                  message="Cannot assign to read only property 'length' of array",
                )
              }
              return value
            }
            let ok = self.array_set_length(
              data,
              PartialDescriptor::value_only(value),
            )
            if !ok && strict {
              raise @errors.TypeError(message="Cannot delete property")
            }
          } else {
            // Canonical numeric index string check: parseDouble(s).toString() === s
            // Parse OUTSIDE try/catch so strict TypeErrors propagate.
            let maybe_n : Double? = Some(@string.parse_double(prop)) catch {
              _ => None
            }
            let mut handled = false
            match array_index64_from_string(prop) {
              Some(idx64) if idx64 > 0x7FFFFFFFL =>
                return self.set_array_sparse_index64(
                  data, receiver, prop, idx64, value, loc, strict,
                )
              _ => ()
            }
            match maybe_n {
              Some(n) =>
                if n.to_string() == prop {
                  let idx = n.to_int()
                  if idx.to_double() == n && idx >= 0 {
                    let logical_len64 = match get_array_length_override(data) {
                      Some(n64) => n64
                      None => data.elements.length().to_int64()
                    }
                    match data.bag.descriptors.get(prop) {
                      Some(desc) =>
                        match desc.setter {
                          Some(setter) => {
                            let _ = self.call_value(
                              setter,
                              receiver,
                              [value],
                              loc,
                            )
                            return value
                          }
                          None =>
                            if !desc.writable {
                              if strict {
                                raise @errors.TypeError(
                                  message="Cannot assign to read only property '\{prop}'",
                                )
                              }
                              return value
                            }
                        }
                      None => ()
                    }
                    let is_sparse_own = data.bag.properties.contains(prop) ||
                      data.bag.descriptors.contains(prop)
                    let is_own = (
                        idx < data.elements.length() &&
                        !data.holes.contains(idx)
                      ) ||
                      is_sparse_own
                    if !is_own {
                      match
                        self.set_missing_array_index_via_proto_chain(
                          get_array_prototype(self.realm_state, data),
                          receiver,
                          prop,
                          value,
                          loc,
                          strict,
                        ) {
                        Some(result) => return result
                        None => ()
                      }
                    }
                    // §10.4.2.1 step 3.b: extend requires writable length.
                    if idx.to_int64() >= logical_len64 && !data.length_writable {
                      if strict {
                        raise @errors.TypeError(
                          message="Cannot add property \{idx}, array's length is non-writable",
                        )
                      }
                      return value
                    }
                    if !is_own && !data.extensible {
                      if strict {
                        raise @errors.TypeError(
                          message="Cannot add property \{idx}, object is not extensible",
                        )
                      }
                      return value
                    }
                    let gap = idx.to_int64() - data.elements.length().to_int64()
                    if gap <= 100000L {
                      let prev_len = data.elements.length()
                      while data.elements.length() <= idx {
                        data.elements.push(Undefined)
                      }
                      for j in prev_len.. ()
            }
            if !handled {
              match data.bag.descriptors.get(prop) {
                Some(desc) =>
                  match desc.setter {
                    Some(setter) => {
                      let _ = self.call_value(setter, receiver, [value], loc)
                      return value
                    }
                    None =>
                      if !desc.writable {
                        if strict {
                          raise @errors.TypeError(
                            message="Cannot assign to read only property '\{prop}'",
                          )
                        }
                        return value
                      }
                  }
                None => ()
              }
              if !data.extensible {
                if strict {
                  raise @errors.TypeError(
                    message="Cannot add property '\{prop}' on a non-extensible object",
                  )
                }
                return value
              }
              set_array_named_prop(data, prop, value)
            }
          }
          value
        }
      }
    }
    Promise(data) => {
      let promise_proto = self.promise_instance_prototype_for_set(data)
      match key {
        Symbol(sym) =>
          self.set_symbol_on_map_set_expando(
            obj,
            receiver,
            data.bag,
            promise_proto,
            sym,
            value,
            loc,
            strict,
          )
        _ => {
          let prop = to_js_string(key, interp=Some(self))
          self.set_string_on_map_set_expando(
            obj,
            receiver,
            data.bag,
            promise_proto,
            prop,
            value,
            loc,
            strict,
          )
        }
      }
    }
    Null => {
      let key_str = match key {
        Symbol(sym) =>
          match sym.description {
            Some(d) => "Symbol(\{d})"
            None => "Symbol()"
          }
        String_(s) => s
        Number(n) => n.to_string()
        Bool(b) => b.to_string()
        _ => ""
      }
      raise @errors.TypeError(
        message="Cannot set properties of null (setting '\{key_str}')",
      )
    }
    Undefined => {
      let key_str = match key {
        Symbol(sym) =>
          match sym.description {
            Some(d) => "Symbol(\{d})"
            None => "Symbol()"
          }
        String_(s) => s
        Number(n) => n.to_string()
        Bool(b) => b.to_string()
        _ => ""
      }
      raise @errors.TypeError(
        message="Cannot set properties of undefined (setting '\{key_str}')",
      )
    }
    _ =>
      match key {
        Symbol(_) => {
          let proto = match obj {
            String_(_) => Some(self.realm_state.get_string_proto())
            Number(_) => Some(self.realm_state.get_number_proto())
            Bool(_) => Some(self.realm_state.get_boolean_proto())
            _ => None
          }
          match proto {
            Some(primitive_proto) =>
              match
                self.set_key_via_map_set_prototype_chain(
                  primitive_proto, receiver, key, value, loc, strict,
                ) {
                Some(result) => return result
                None => ()
              }
            None => ()
          }
          if strict {
            raise @errors.TypeError(
              message="Cannot set property of \{type_of(obj)}",
            )
          } else {
            value
          }
        }
        _ => {
          let prop = to_js_string(key, interp=Some(self))
          self.set_property(obj, prop, value, loc, strict~, receiver~)
        }
      }
  }
}

///|
pub fn Interpreter::get_console_member(
  self : Interpreter,
  prop : String,
) -> Value raise Error {
  if prop == "log" {
    let output = self.host.output
    make_native_func(name="log", realm_state=Some(self.realm_state), fn(args) {
      let parts : Array[String] = args.map(fn(a) { a.to_string() })
      output.push(parts.join(" "))
      Undefined
    })
  } else {
    raise @errors.TypeError(message="console.\{prop} is not supported")
  }
}

///|
fn Interpreter::eval_member(
  self : Interpreter,
  ctx : ExecContext,
  obj_expr : @ast.Expr,
  prop : String,
  env : Environment,
  loc : @token.Loc,
) -> Value raise Error {
  // Special case: console.log
  match obj_expr {
    Ident("console", _) => self.get_console_member(prop)
    _ => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      self.get_property(obj, prop, loc)
    }
  }
}

///|
pub fn get_bound_func_name(target : Value) -> String {
  match target {
    Object(td) => {
      // First check properties["name"] (set by Object.defineProperty or bind)
      let prop_name = match td.bag.properties.get("name") {
        Some(String_(s)) => s
        _ => ""
      }
      if prop_name != "" {
        return "bound " + prop_name
      }
      match td.callable {
        Some(UserFunc(fd)) => "bound " + fd.name.unwrap_or("")
        Some(UserFuncExt(fd)) => "bound " + fd.name.unwrap_or("")
        Some(NativeCallable(n, _)) | Some(NativeCallableWithContext(n, _)) =>
          "bound " + n
        Some(NonConstructableCallable(n, _)) => "bound " + n
        Some(BoundFunc(inner, _, _)) => "bound " + get_bound_func_name(inner)
        Some(ArrowFunc(fd)) => "bound " + fd.name.unwrap_or("")
        Some(ArrowFuncExt(fd)) => "bound " + fd.name.unwrap_or("")
        Some(ClassConstructor({ name: n, .. })) => "bound " + n
        Some(MethodCallable(n, _)) => "bound " + n
        Some(InterpreterCallable(n, _))
        | Some(InterpreterCallableWithContext(n, _)) => "bound " + n
        Some(ExecutorCallable(executable)) => "bound " + executable.name()
        Some(NonConstructableInterpreterCallable(n, _)) => "bound " + n
        Some(_) => "bound "
        None => "bound "
      }
    }
    _ => "bound "
  }
}

///|
pub fn get_func_length(callable : Callable) -> Int {
  match callable {
    UserFunc(func_data) => func_data.params.length()
    UserFuncExt(func_data) => {
      let mut len = 0
      for p in func_data.params {
        if p.default_val is Some(_) {
          break
        }
        len += 1
      }
      len
    }
    ArrowFunc(func_data) => func_data.params.length()
    ArrowFuncExt(func_data) => {
      let mut len = 0
      for p in func_data.params {
        if p.default_val is Some(_) {
          break
        }
        len += 1
      }
      len
    }
    BoundFunc(target, _, bound_args) => {
      let target_len = get_function_length(target)
      let diff = target_len - bound_args.length()
      if diff < 0 {
        0
      } else {
        diff
      }
    }
    NativeCallable(_, _) => 0
    NativeCallableWithContext(_, _) => 0
    NonConstructableCallable(_, _) => 0
    FuncCallMethod(_) => 1
    FuncApplyMethod(_) => 2
    MethodCallable(_, _) => 0
    InterpreterCallable(_, _) => 0
    InterpreterCallableWithContext(_, _) => 0
    ExecutorCallable(executable) => executable.length()
    NonConstructableInterpreterCallable(_, _) => 0
    ConstructorOnlyCallable(_, _) => 0
    ClassConstructor({ ctor_fn, .. }) =>
      match ctor_fn {
        Some((params, _, _)) => params.length()
        None => 0
      }
  }
}

///|
/// Return the observable function length used by Function.prototype.bind.
/// Prefer an own numeric `length` property, falling back to callable metadata
/// for older function objects that synthesize length from their Callable tag.
pub fn get_function_length(target : Value) -> Int {
  match target {
    Object(data) =>
      match data.bag.properties.get("length") {
        Some(Number(n)) => if n <= 0.0 { 0 } else { n.to_int() }
        _ =>
          match data.callable {
            Some(callable) => get_func_length(callable)
            None => 0
          }
      }
    _ => 0
  }
}