///|
/// Spec [[Get]] for an integer index on an Array exotic object.
/// Centralises the descriptor / dense / hole / OOB dispatch and the
/// receiver-preserving prototype walk so every [[Get]] entry point
/// (`get_property` and both `get_computed_property` index branches)
/// stays in lockstep. The prototype-walk loop in
/// `get_property_from_prototype` keeps its own in-line dispatch
/// because it must `continue` the walk, not recurse.
fn Interpreter::array_get_indexed(
  self : Interpreter,
  obj : Value,
  data : ArrayData,
  index : Int,
  key : String,
  loc : @token.Loc,
) -> Value raise Error {
  match array_index_lookup_result(data, index) {
    Present(v) => v
    OwnAccessor(Some(getter)) => self.call_value(getter, obj, [], loc)
    OwnAccessor(None) => Undefined
    Hole | OutOfRange => {
      let proto = get_array_prototype(self.realm_state, data)
      self.get_property_from_prototype(obj, proto, key, loc)
    }
  }
}

///|
fn callable_to_source_string(data : ObjectData, callable : Callable) -> String {
  match callable {
    UserFunc(func_data) =>
      match func_data.source_text {
        Some(text) => text
        None =>
          "function " + func_data.name.unwrap_or("") + "() { [native code] }"
      }
    UserFuncExt(func_data) =>
      match func_data.source_text {
        Some(text) => text
        None =>
          "function " + func_data.name.unwrap_or("") + "() { [native code] }"
      }
    ArrowFunc(func_data) =>
      match func_data.source_text {
        Some(text) => text
        None => "() => { [native code] }"
      }
    ArrowFuncExt(func_data) =>
      match func_data.source_text {
        Some(text) => text
        None => "() => { [native code] }"
      }
    BoundFunc(_, _, _) => "function () { [native code] }"
    NativeCallable(n, _) | NativeCallableWithContext(n, _) =>
      "function " + n + "() { [native code] }"
    NonConstructableCallable(n, _) => "function " + n + "() { [native code] }"
    FuncCallMethod(_) => "function call() { [native code] }"
    FuncApplyMethod(_) => "function apply() { [native code] }"
    MethodCallable(n, _) => "function " + n + "() { [native code] }"
    InterpreterCallable(n, _) | InterpreterCallableWithContext(n, _) =>
      match data.bag.internal_slots.get(SourceText) {
        Some(String_(text)) => text
        _ => "function " + n + "() { [native code] }"
      }
    ExecutorCallable(executable) =>
      match data.bag.internal_slots.get(SourceText) {
        Some(String_(text)) => text
        _ => "function " + executable.name() + "() { [native code] }"
      }
    NonConstructableInterpreterCallable(n, _) =>
      "function " + n + "() { [native code] }"
    ConstructorOnlyCallable(n, _) => "function " + n + "() { [native code] }"
    ClassConstructor({ name: n, source_text, .. }) =>
      match source_text {
        Some(text) => text
        None => "class " + n + " { [native code] }"
      }
  }
}

///|
fn Interpreter::with_active_property_access_value(
  self : Interpreter,
  eval : () -> Value raise Error,
) -> Value raise Error {
  with_cleared_active_callee_realm(self.realm_state, fn() raise { eval() })
}

///|
pub fn Interpreter::get_property(
  self : Interpreter,
  obj : Value,
  prop : String,
  loc : @token.Loc,
) -> Value raise Error {
  match obj {
    Object(data) => {
      // Descriptor maps are empty for ordinary object-literal data slots.
      // Check emptiness first so that hot own-data reads avoid hashing the
      // property name into the descriptor map before the value-map lookup.
      let descriptors = data.bag.descriptors
      if descriptors.is_empty() || !descriptors.contains(prop) {
        match data.bag.properties.get(prop) {
          Some(v) => return v
          None => ()
        }
      }
    }
    _ => ()
  }
  self.with_active_property_access_value(fn() raise {
    self.get_property_impl(obj, prop, loc)
  })
}

///|
fn Interpreter::get_property_impl(
  self : Interpreter,
  obj : Value,
  prop : String,
  loc : @token.Loc,
) -> Value raise Error {
  match obj {
    Proxy(proxy_data) => proxy_get(self, proxy_data, prop, obj)
    Object(data) => self.get_property_of_object(obj, data, prop, loc)
    Array(data) =>
      if prop == "length" {
        match get_array_length_override(data) {
          Some(n64) => Number(n64.to_double())
          None => Number(data.elements.length().to_double())
        }
      } else {
        // Check numeric index access first
        let num_idx = @string.parse_int(prop) catch { _ => -1 }
        if num_idx >= 0 && num_idx.to_string() == prop {
          self.array_get_indexed(obj, data, num_idx, prop, loc)
        } else {
          // Check named properties (e.g., index, input on exec results)
          match data.bag.descriptors.get(prop) {
            Some(desc) =>
              match desc.getter {
                Some(getter) => return self.call_value(getter, obj, [], loc)
                None => if desc.setter is Some(_) { return Undefined }
              }
            None => ()
          }
          match get_array_named_prop(data, prop) {
            Some(v) => v
            None =>
              match get_array_prototype_override(data) {
                Some(proto) =>
                  self.get_property_from_prototype(obj, proto, prop, loc)
                None => {
                  let result = (self.stdlib_hooks.get_array_method_with_interp)(
                    data,
                    prop,
                    self.realm_state,
                  )
                  match result {
                    Undefined =>
                      lookup_builtin_proto(self, obj, "Array", prop, loc)
                    _ => result
                  }
                }
              }
          }
        }
      }
    String_(s) =>
      if prop == "length" {
        Number(utf16_length(s).to_double())
      } else {
        let result = (self.stdlib_hooks.get_string_method)(
          s,
          prop,
          self.realm_state,
          self.annex_b,
        )
        match result {
          Undefined => lookup_builtin_proto(self, obj, "String", prop, loc)
          _ => result
        }
      }
    Number(_) => {
      let result = (self.stdlib_hooks.get_number_method)(
        obj,
        prop,
        self.realm_state,
      )
      match result {
        Undefined => lookup_builtin_proto(self, obj, "Number", prop, loc)
        _ => result
      }
    }
    Bool(_) => lookup_builtin_proto(self, obj, "Boolean", prop, loc)
    Symbol(sym) =>
      // Symbol properties: description, toString
      if prop == "description" {
        match sym.description {
          Some(desc) => String_(desc)
          None => Undefined
        }
      } else if prop == "toString" {
        make_native_func(name="toString", realm_state=Some(self.realm_state), fn(
          _args,
        ) {
          match sym.description {
            Some(desc) => String_("Symbol(\{desc})")
            None => String_("Symbol()")
          }
        })
      } else {
        lookup_builtin_proto(self, obj, "Symbol", prop, loc)
      }
    Map(data) => {
      // Check expando properties first (instance fields from subclasses)
      match data.bag.descriptors.get(prop) {
        Some(desc) =>
          match desc.getter {
            Some(getter) => return self.call_value(getter, obj, [], loc)
            None => ()
          }
        None => ()
      }
      match data.bag.properties.get(prop) {
        Some(v) => return v
        None => ()
      }
      let proto = data.prototype.unwrap_or_else(fn() {
        self.realm_state.get_map_proto()
      })
      self.get_property_from_prototype(obj, proto, prop, loc)
    }
    Set(data) => {
      // Check expando properties first (instance fields from subclasses)
      match data.bag.descriptors.get(prop) {
        Some(desc) =>
          match desc.getter {
            Some(getter) => return self.call_value(getter, obj, [], loc)
            None => ()
          }
        None => ()
      }
      match data.bag.properties.get(prop) {
        Some(v) => return v
        None => ()
      }
      let proto = data.prototype.unwrap_or_else(fn() {
        self.realm_state.get_set_proto()
      })
      self.get_property_from_prototype(obj, proto, prop, loc)
    }
    Promise(data) => {
      // Check user-defined properties first, then prototype methods.
      match data.bag.descriptors.get(prop) {
        Some(desc) =>
          match desc.getter {
            Some(getter) => return self.call_value(getter, obj, [], loc)
            None => ()
          }
        None => ()
      }
      match data.bag.properties.get(prop) {
        Some(v) => return v
        None => ()
      }
      let proto = data.prototype.unwrap_or_else(fn() {
        self.realm_state.get_promise_proto()
      })
      self.get_property_from_prototype(obj, proto, prop, loc)
    }
    Null =>
      raise @errors.TypeError(
        message="Cannot read properties of null (reading '\{prop}')",
      )
    Undefined =>
      raise @errors.TypeError(
        message="Cannot read properties of undefined (reading '\{prop}')",
      )
  }
}

///|
/// ES [[Get]] with an explicit Receiver and an already-evaluated property-key
/// input. This is the shared receiver-aware boundary for ordinary objects and
/// every object-like Value variant. The key is canonicalized once at entry;
/// recursive prototype steps keep the resulting String/Symbol unchanged.
pub fn Interpreter::get_property_key_with_receiver(
  self : Interpreter,
  target : Value,
  key : Value,
  receiver : Value,
  loc : @token.Loc,
) -> Value raise Error {
  let prop_key = to_property_key(key, interp=Some(self))
  self.get_property_key_with_receiver_canonical(target, prop_key, receiver, loc)
}

///|
fn Interpreter::get_property_key_with_receiver_canonical(
  self : Interpreter,
  target : Value,
  prop_key : Value,
  receiver : Value,
  loc : @token.Loc,
) -> Value raise Error {
  match target {
    Proxy(proxy_data) => proxy_get_key(self, proxy_data, prop_key, receiver)
    Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) =>
      match self.get_own_property(target, prop_key) {
        Some((desc, own_value)) =>
          match desc.getter {
            Some(getter) => self.call_value(getter, receiver, [], loc)
            None =>
              if desc.is_accessor || desc.setter is Some(_) {
                Undefined
              } else {
                own_value
              }
          }
        None => {
          let proto = match target {
            Object(data) => data.prototype
            Array(data) => get_array_prototype(self.realm_state, data)
            Map(data) =>
              data.prototype.unwrap_or_else(fn() {
                self.realm_state.get_map_proto()
              })
            Set(data) =>
              data.prototype.unwrap_or_else(fn() {
                self.realm_state.get_set_proto()
              })
            Promise(data) =>
              data.prototype.unwrap_or_else(fn() {
                self.realm_state.get_promise_proto()
              })
            _ => fail("non-object reached object [[Get]] prototype dispatch")
          }
          self.get_property_key_with_receiver_canonical(
            proto, prop_key, receiver, loc,
          )
        }
      }
    Null | Undefined => Undefined
    _ => self.get_computed_property(target, prop_key, loc)
  }
}

///|
/// Look up an inherited string property while preserving the original
/// receiver for accessors and Proxy `get` traps.
fn Interpreter::get_property_from_prototype(
  self : Interpreter,
  receiver : Value,
  proto : Value,
  prop : String,
  loc : @token.Loc,
) -> Value raise Error {
  self.get_property_key_with_receiver(proto, String_(prop), receiver, loc)
}

///|
/// Property access for Object values: checks own properties, walks prototype chain,
/// and falls back to built-in Function/Object methods for callable objects.
fn Interpreter::get_property_of_object(
  self : Interpreter,
  obj : Value,
  data : ObjectData,
  prop : String,
  loc : @token.Loc,
) -> Value raise Error {
  // Check for accessor descriptor (getter) on own property
  match data.bag.descriptors.get(prop) {
    Some(desc) =>
      match desc.getter {
        Some(getter) => return self.call_value(getter, obj, [], loc)
        None => ()
      }
    None => ()
  }
  match data.bag.properties.get(prop) {
    Some(v) => v
    None => {
      // Walk prototype chain
      let mut current = data.prototype
      let mut func_proto_checked = false
      while true {
        match current {
          Object(proto_data) => {
            // Check for accessor descriptor (getter) on prototype property
            match proto_data.bag.descriptors.get(prop) {
              Some(desc) =>
                match desc.getter {
                  Some(getter) => return self.call_value(getter, obj, [], loc)
                  None => ()
                }
              None => ()
            }
            match proto_data.bag.properties.get(prop) {
              Some(v) => return v
              None => current = proto_data.prototype
            }
          }
          Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) =>
            return self.get_property_key_with_receiver(
              current,
              String_(prop),
              obj,
              loc,
            )
          Null =>
            // For function objects, fall back to Function.prototype (once)
            if !func_proto_checked {
              match data.callable {
                Some(_) => {
                  let fp = self.global.get("[[FunctionPrototype]]") catch {
                    _ => break
                  }
                  func_proto_checked = true
                  current = fp
                }
                None => break
              }
            } else {
              break
            }
          _ => break
        }
      }
      // Built-in Function methods (for callable objects)
      match data.callable {
        Some(callable) =>
          if prop == "call" {
            return stamp_function_realm(
              Object({
                bag: PropertyBag(),
                prototype: Null,
                callable: Some(FuncCallMethod(obj)),
                class_name: "Function",
                extensible: true,
                arraybuffer_state: None,
              }),
              realm_state=Some(self.realm_state),
            )
          } else if prop == "apply" {
            return stamp_function_realm(
              Object({
                bag: PropertyBag(),
                prototype: Null,
                callable: Some(FuncApplyMethod(obj)),
                class_name: "Function",
                extensible: true,
                arraybuffer_state: None,
              }),
              realm_state=Some(self.realm_state),
            )
          } else if prop == "bind" {
            let target = obj
            let target_length = get_function_length(target)
            let target_proto = data.prototype
            return make_native_func(
              name="bind",
              realm_state=Some(self.realm_state),
              fn(args) {
                let bind_this = if args.length() > 0 {
                  args[0]
                } else {
                  Undefined
                }
                let bound_args : Array[Value] = []
                for i = 1; i < args.length(); i = i + 1 {
                  bound_args.push(args[i])
                }
                let bound_name = get_bound_func_name(target)
                let bound_length = {
                  let diff = target_length - bound_args.length()
                  if diff < 0 {
                    0
                  } else {
                    diff
                  }
                }
                make_bound_func(
                  target,
                  bind_this,
                  bound_args,
                  prototype=target_proto,
                  name=bound_name,
                  length=bound_length.to_double(),
                  realm_state=Some(self.realm_state),
                )
              },
            )
          } else if prop == "length" {
            // Return the number of formal parameters
            let param_count = get_func_length(callable)
            return Number(param_count.to_double())
          } else if prop == "name" {
            // Return the function name
            let func_name : String = match callable {
              UserFunc(func_data) => func_data.name.unwrap_or("")
              UserFuncExt(func_data) => func_data.name.unwrap_or("")
              ArrowFunc(_) => ""
              ArrowFuncExt(_) => ""
              BoundFunc(target, _, _) => get_bound_func_name(target)
              NativeCallable(n, _) | NativeCallableWithContext(n, _) => n
              NonConstructableCallable(n, _) => n
              FuncCallMethod(_) => "call"
              FuncApplyMethod(_) => "apply"
              MethodCallable(n, _) => n
              InterpreterCallable(n, _)
              | InterpreterCallableWithContext(n, _) => n
              ExecutorCallable(executable) => executable.name()
              NonConstructableInterpreterCallable(n, _) => n
              ConstructorOnlyCallable(n, _) => n
              ClassConstructor({ name: n, .. }) => n
            }
            return String_(func_name)
          } else if prop == "toString" {
            // Return function string representation
            let func_str = callable_to_source_string(data, callable)
            return make_native_func(
              name="toString",
              realm_state=Some(self.realm_state),
              fn(_args) { String_(func_str) },
            )
          } else if prop == "prototype" {
            // Only constructable functions have a prototype property
            // Non-constructable (arrow functions, bound functions, etc.) return undefined
            let is_constructable = match callable {
              UserFunc(fd) => !fd.is_method
              UserFuncExt(fd) => !fd.is_method
              NativeCallable(_, _)
              | NativeCallableWithContext(_, _)
              | ConstructorOnlyCallable(_, _)
              | ClassConstructor(_) => true
              ArrowFunc(_)
              | ArrowFuncExt(_)
              | BoundFunc(_, _, _)
              | NonConstructableCallable(_, _)
              | NonConstructableInterpreterCallable(_, _)
              | FuncCallMethod(_)
              | FuncApplyMethod(_)
              | MethodCallable(_, _)
              | InterpreterCallable(_, _)
              | InterpreterCallableWithContext(_, _) => false
              ExecutorCallable(executable) => executable.is_constructable()
            }
            if !is_constructable || callable is ConstructorOnlyCallable(_, _) {
              return Undefined
            }
            // For constructable functions, check if prototype exists
            match data.bag.properties.get("prototype") {
              Some(p) => return p
              None => {
                // Create and memoize default prototype
                let proto : Value = Object({
                  bag: PropertyBag(),
                  prototype: get_obj_proto(realm_state=Some(self.realm_state)),
                  callable: None,
                  class_name: "Object",
                  extensible: true,
                  arraybuffer_state: None,
                })
                data.bag.properties["prototype"] = proto
                return proto
              }
            }
          }
        None => ()
      }
      // Built-in Object methods (only for objects that inherit from Object.prototype)
      // Objects with null prototype (Object.create(null)) should NOT have these
      if !(data.prototype is Null && data.callable is None) {
        if prop == "hasOwnProperty" {
          return make_method_func(
            name="hasOwnProperty",
            length=1,
            realm_state=Some(self.realm_state),
            fn(this_val, args) {
              let key_val = if args.length() > 0 { args[0] } else { Undefined }
              match this_val {
                Object(d) =>
                  match key_val {
                    Symbol(sym) =>
                      Bool(d.bag.symbol_properties.contains(sym.id))
                    _ => Bool(d.bag.properties.contains(key_val.to_string()))
                  }
                Array(ad) =>
                  match key_val {
                    Symbol(sym) =>
                      Bool(get_array_symbol_prop(ad, sym.id) is Some(_))
                    _ => {
                      let k = key_val.to_string()
                      if k == "length" {
                        return Bool(true)
                      }
                      let idx = @string.parse_int(k) catch { _ => -1 }
                      if idx >= 0 &&
                        idx.to_string() == k &&
                        idx < ad.elements.length() {
                        Bool(true)
                      } else {
                        Bool(get_array_named_prop(ad, k) is Some(_))
                      }
                    }
                  }
                _ => Bool(false)
              }
            },
          )
        }
        if prop == "toString" {
          return make_native_func(
            name="toString",
            realm_state=Some(self.realm_state),
            fn(_args) { String_("[object Object]") },
          )
        }
      }
      Undefined
    }
  }
}

///|
/// Look up a property on a built-in constructor's prototype (e.g. Array.prototype.constructor)
fn lookup_builtin_proto(
  interp : Interpreter,
  receiver : Value,
  ctor_name : String,
  prop : String,
  loc : @token.Loc,
) -> Value raise Error {
  // Resolve the built-in prototype through the active callee realm rather
  // than re-deriving it through `globalThis[ctor_name].prototype`.
  // Unknown ctor names fall through to Undefined, matching the previous
  // `interp.global.get(...) catch { _ => Undefined }` behavior.
  let proto = match ctor_name {
    "Array" => get_array_proto(realm_state=Some(interp.realm_state))
    "String" => get_string_proto(realm_state=Some(interp.realm_state))
    "Number" => get_number_proto(realm_state=Some(interp.realm_state))
    "Boolean" => get_boolean_proto(realm_state=Some(interp.realm_state))
    "Symbol" => get_symbol_proto(realm_state=Some(interp.realm_state))
    _ => return Undefined
  }
  match proto {
    Object(proto_data) => {
      // Walk up prototype chain and honor accessor getters with `receiver`.
      let mut current : Value = Object(proto_data)
      while true {
        match current {
          Object(pdata) => {
            match pdata.bag.descriptors.get(prop) {
              Some(desc) =>
                match desc.getter {
                  Some(getter) =>
                    return interp.call_value(getter, receiver, [], loc)
                  None =>
                    match pdata.bag.properties.get(prop) {
                      Some(v) => return v
                      None => ()
                    }
                }
              None =>
                match pdata.bag.properties.get(prop) {
                  Some(v) => return v
                  None => ()
                }
            }
            current = pdata.prototype
          }
          _ => break
        }
      }
      Undefined
    }
    _ => Undefined
  }
}

///|
pub fn Interpreter::get_computed_property(
  self : Interpreter,
  obj : Value,
  key : Value,
  loc : @token.Loc,
) -> Value raise Error {
  self.with_active_property_access_value(fn() raise {
    self.get_computed_property_impl(obj, key, loc)
  })
}

///|
fn Interpreter::get_computed_property_impl(
  self : Interpreter,
  obj : Value,
  key : Value,
  loc : @token.Loc,
) -> Value raise Error {
  // Handle Proxy objects first
  match obj {
    Proxy(proxy_data) => return proxy_get_key(self, proxy_data, key, obj)
    _ => ()
  }
  // Handle Symbol keys specially
  match key {
    Symbol(sym) => {
      let realm_state = self.realm_state
      let well_known_symbols = self.realm_state.well_known_symbols
      match obj {
        Object(data) => {
          // Check for symbol accessor descriptor (getter)
          match data.bag.symbol_descriptors.get(sym.id) {
            Some(desc) =>
              match desc.getter {
                Some(getter) => return self.call_value(getter, obj, [], loc)
                None => ()
              }
            None => ()
          }
          // Look up symbol property
          match data.bag.symbol_properties.get(sym.id) {
            Some(v) => return v
            None => {
              // Walk prototype chain for symbol properties
              let mut current = data.prototype
              while true {
                match current {
                  Object(proto_data) => {
                    // Check for accessor on prototype
                    match proto_data.bag.symbol_descriptors.get(sym.id) {
                      Some(desc) =>
                        match desc.getter {
                          Some(getter) =>
                            return self.call_value(getter, obj, [], loc)
                          None => ()
                        }
                      None => ()
                    }
                    match proto_data.bag.symbol_properties.get(sym.id) {
                      Some(v) => return v
                      None => current = proto_data.prototype
                    }
                  }
                  Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) =>
                    return self.get_property_key_with_receiver(
                      current,
                      Symbol(sym),
                      obj,
                      loc,
                    )
                  _ => break
                }
              }
              // Boxed primitive objects: delegate Symbol.iterator to primitive
              let iterator_sym = well_known_symbols.iterator
              if sym.id == iterator_sym.id {
                if data.class_name == "String" {
                  match data.bag.internal_slots.get(StringData) {
                    Some(String_(_)) =>
                      return make_method_func(
                        name="[Symbol.iterator]",
                        length=0,
                        realm_state=Some(realm_state),
                        fn(this_val, _args) raise {
                          let str = match this_val {
                            String_(sv) => sv
                            Object(d) =>
                              if d.class_name == "String" {
                                match d.bag.internal_slots.get(StringData) {
                                  Some(String_(sv)) => sv
                                  _ => this_val.to_string()
                                }
                              } else {
                                this_val.to_string()
                              }
                            Null | Undefined =>
                              raise @errors.TypeError(
                                message="Cannot convert undefined or null to object",
                              )
                            _ => this_val.to_string()
                          }
                          realm_state.make_string_iterator_value(str)
                        },
                      )
                    _ => ()
                  }
                } else if data.class_name == "Array" {
                  // Shouldn't reach here for arrays normally, but fallback
                  ()
                }
              }
              return Undefined
            }
          }
        }
        String_(s) => {
          // Handle Symbol.iterator for strings
          let iterator_sym = well_known_symbols.iterator
          if sym.id == iterator_sym.id {
            // Return a function that creates a string iterator
            return make_method_func(
              name="[Symbol.iterator]",
              length=0,
              realm_state=Some(realm_state),
              fn(_this_val, _args) { realm_state.make_string_iterator_value(s) },
            )
          }
          return Undefined
        }
        Array(arr) => {
          // Check for accessor descriptor (getter) on own symbol property.
          match arr.bag.symbol_descriptors.get(sym.id) {
            Some(desc) =>
              match desc.getter {
                Some(getter) => return self.call_value(getter, obj, [], loc)
                None => ()
              }
            None => ()
          }
          // Check array's own symbol property bag.
          match get_array_symbol_prop(arr, sym.id) {
            Some(v) => return v
            None => ()
          }
          // Check Array.prototype and the rest of the prototype chain for symbol
          // properties (e.g. overridden Symbol.iterator, @@toPrimitive on
          // Object.prototype, etc.) while honoring per-array prototype overrides.
          match get_array_prototype(self.realm_state, arr) {
            Object(proto_data) => {
              // Check symbol descriptors for getters
              match proto_data.bag.symbol_descriptors.get(sym.id) {
                Some(desc) =>
                  match desc.getter {
                    Some(getter) => return self.call_value(getter, obj, [], loc)
                    None => ()
                  }
                None => ()
              }
              // Check symbol properties
              match proto_data.bag.symbol_properties.get(sym.id) {
                Some(v) => return v
                None => ()
              }
              // Walk prototype chain beyond Array.prototype (e.g. Object.prototype)
              let mut current = proto_data.prototype
              while true {
                match current {
                  Object(ancestor) => {
                    match ancestor.bag.symbol_descriptors.get(sym.id) {
                      Some(desc) =>
                        match desc.getter {
                          Some(getter) =>
                            return self.call_value(getter, obj, [], loc)
                          None => ()
                        }
                      None => ()
                    }
                    match ancestor.bag.symbol_properties.get(sym.id) {
                      Some(v) => return v
                      None => current = ancestor.prototype
                    }
                  }
                  _ => break
                }
              }
            }
            Null | Undefined => ()
            proto_val =>
              return self.get_computed_property(proto_val, Symbol(sym), loc)
          }
          return Undefined
        }
        Map(map_data) => {
          // Check expando symbol properties first (instance fields from subclasses)
          match map_data.bag.symbol_descriptors.get(sym.id) {
            Some(desc) =>
              match desc.getter {
                Some(getter) => return self.call_value(getter, obj, [], loc)
                None => ()
              }
            None => ()
          }
          match map_data.bag.symbol_properties.get(sym.id) {
            Some(v) => return v
            None => ()
          }
          // Well-known symbols
          let iterator_sym = well_known_symbols.iterator
          if sym.id == iterator_sym.id {
            return (self.stdlib_hooks.get_map_method)(
              map_data,
              "entries",
              self.realm_state,
            )
          }
          let tostringtag_sym = well_known_symbols.to_string_tag
          if sym.id == tostringtag_sym.id {
            return String_("Map")
          }
          return Undefined
        }
        Set(set_data) => {
          // Check expando symbol properties first (instance fields from subclasses)
          match set_data.bag.symbol_descriptors.get(sym.id) {
            Some(desc) =>
              match desc.getter {
                Some(getter) => return self.call_value(getter, obj, [], loc)
                None => ()
              }
            None => ()
          }
          match set_data.bag.symbol_properties.get(sym.id) {
            Some(v) => return v
            None => ()
          }
          // Well-known symbols
          let iterator_sym = well_known_symbols.iterator
          if sym.id == iterator_sym.id {
            return (self.stdlib_hooks.get_set_method)(
              set_data,
              "values",
              self.realm_state,
            )
          }
          let tostringtag_sym = well_known_symbols.to_string_tag
          if sym.id == tostringtag_sym.id {
            return String_("Set")
          }
          return Undefined
        }
        Promise(data) => {
          // Check expando symbol descriptors first (instance fields from subclasses,
          // accessor-type symbol properties)
          match data.bag.symbol_descriptors.get(sym.id) {
            Some(desc) =>
              match desc.getter {
                Some(getter) => return self.call_value(getter, obj, [], loc)
                None => ()
              }
            None => ()
          }
          match data.bag.symbol_properties.get(sym.id) {
            Some(v) => return v
            None => ()
          }
          let proto = data.prototype.unwrap_or_else(fn() {
            self.realm_state.get_promise_proto()
          })
          return self.get_symbol_property_from_prototype(obj, proto, sym, loc)
        }
        Symbol(_) => {
          // Delegate to Symbol.prototype for symbol-keyed property access
          let proto = self.global.get("[[SymbolPrototype]]") catch { _ => Null }
          match proto {
            Object(proto_data) => {
              match proto_data.bag.symbol_descriptors.get(sym.id) {
                Some(desc) =>
                  match desc.getter {
                    Some(getter) => return self.call_value(getter, obj, [], loc)
                    None => ()
                  }
                None => ()
              }
              match proto_data.bag.symbol_properties.get(sym.id) {
                Some(v) => return v
                None => return Undefined
              }
            }
            _ => return Undefined
          }
        }
        Bool(_) => {
          // Walk full prototype chain (Boolean.prototype → Object.prototype etc.)
          let proto = self.realm_state.get_boolean_proto()
          return self.get_symbol_property_from_prototype(obj, proto, sym, loc)
        }
        _ => return Undefined
      }
    }
    _ => ()
  }
  match obj {
    Object(data) => {
      // TypedArray indexed access: intercept numeric keys before regular property lookup
      if is_typedarray_class(data.class_name) {
        match key {
          Number(n) => {
            let idx = n.to_int()
            if idx.to_double() == n && idx >= 0 {
              return (self.stdlib_hooks.typedarray_get_index)(
                data,
                idx,
                self.realm_state,
              )
            }
            // Non-integer or negative Number key: return undefined per spec
            return Undefined
          }
          String_(s) =>
            match classify_typedarray_string_key(s) {
              Some(-1) => return Undefined
              Some(idx) =>
                return (self.stdlib_hooks.typedarray_get_index)(
                  data,
                  idx,
                  self.realm_state,
                )
              None => ()
            }
          _ => ()
        }
      }
      let prop = to_js_string(key, interp=Some(self))
      // Check for accessor descriptor (getter) on own property
      match data.bag.descriptors.get(prop) {
        Some(desc) =>
          match desc.getter {
            Some(getter) => return self.call_value(getter, obj, [], loc)
            None => ()
          }
        None => ()
      }
      match data.bag.properties.get(prop) {
        Some(v) => v
        None => {
          // Walk prototype chain
          let mut current = data.prototype
          while true {
            match current {
              Object(proto_data) => {
                // Check for accessor descriptor on prototype
                match proto_data.bag.descriptors.get(prop) {
                  Some(desc) =>
                    match desc.getter {
                      Some(getter) =>
                        return self.call_value(getter, obj, [], loc)
                      None => ()
                    }
                  None => ()
                }
                match proto_data.bag.properties.get(prop) {
                  Some(v) => return v
                  None => current = proto_data.prototype
                }
              }
              Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) =>
                return self.get_property_key_with_receiver(
                  current,
                  String_(prop),
                  obj,
                  loc,
                )
              _ => break
            }
          }
          // Built-in Function methods (for callable objects)
          match data.callable {
            Some(callable) =>
              if prop == "call" {
                return stamp_function_realm(
                  Object({
                    bag: PropertyBag(),
                    prototype: Null,
                    callable: Some(FuncCallMethod(obj)),
                    class_name: "Function",
                    extensible: true,
                    arraybuffer_state: None,
                  }),
                  realm_state=Some(self.realm_state),
                )
              } else if prop == "apply" {
                return stamp_function_realm(
                  Object({
                    bag: PropertyBag(),
                    prototype: Null,
                    callable: Some(FuncApplyMethod(obj)),
                    class_name: "Function",
                    extensible: true,
                    arraybuffer_state: None,
                  }),
                  realm_state=Some(self.realm_state),
                )
              } else if prop == "bind" {
                let target = obj
                let target_length = get_function_length(target)
                let target_proto = data.prototype
                return make_native_func(
                  name="bind",
                  realm_state=Some(self.realm_state),
                  fn(args) {
                    let bind_this = if args.length() > 0 {
                      args[0]
                    } else {
                      Undefined
                    }
                    let bound_args : Array[Value] = []
                    for i = 1; i < args.length(); i = i + 1 {
                      bound_args.push(args[i])
                    }
                    let bound_name = get_bound_func_name(target)
                    let bound_length = {
                      let diff = target_length - bound_args.length()
                      if diff < 0 {
                        0
                      } else {
                        diff
                      }
                    }
                    make_bound_func(
                      target,
                      bind_this,
                      bound_args,
                      prototype=target_proto,
                      name=bound_name,
                      length=bound_length.to_double(),
                      realm_state=Some(self.realm_state),
                    )
                  },
                )
              } else if prop == "length" {
                // Return the number of formal parameters
                let param_count = get_func_length(callable)
                return Number(param_count.to_double())
              } else if prop == "name" {
                // Return the function name
                let func_name : String = match callable {
                  UserFunc(func_data) => func_data.name.unwrap_or("")
                  UserFuncExt(func_data) => func_data.name.unwrap_or("")
                  ArrowFunc(_) => ""
                  ArrowFuncExt(_) => ""
                  BoundFunc(target, _, _) => get_bound_func_name(target)
                  NativeCallable(n, _) | NativeCallableWithContext(n, _) => n
                  NonConstructableCallable(n, _) => n
                  FuncCallMethod(_) => "call"
                  FuncApplyMethod(_) => "apply"
                  MethodCallable(n, _) => n
                  InterpreterCallable(n, _)
                  | InterpreterCallableWithContext(n, _) => n
                  ExecutorCallable(executable) => executable.name()
                  NonConstructableInterpreterCallable(n, _) => n
                  ConstructorOnlyCallable(n, _) => n
                  ClassConstructor({ name: n, .. }) => n
                }
                return String_(func_name)
              } else if prop == "toString" {
                // Return function string representation
                let func_str = callable_to_source_string(data, callable)
                return make_native_func(
                  name="toString",
                  realm_state=Some(self.realm_state),
                  fn(_args) { String_(func_str) },
                )
              } else if prop == "prototype" {
                // Only constructable functions have a prototype property
                // Non-constructable (arrow functions, bound functions, etc.) return undefined
                let is_constructable = match callable {
                  UserFunc(fd) => !fd.is_method
                  UserFuncExt(fd) => !fd.is_method
                  NativeCallable(_, _)
                  | NativeCallableWithContext(_, _)
                  | ConstructorOnlyCallable(_, _)
                  | ClassConstructor(_) => true
                  ArrowFunc(_)
                  | ArrowFuncExt(_)
                  | BoundFunc(_, _, _)
                  | NonConstructableCallable(_, _)
                  | NonConstructableInterpreterCallable(_, _)
                  | FuncCallMethod(_)
                  | FuncApplyMethod(_)
                  | MethodCallable(_, _)
                  | InterpreterCallable(_, _)
                  | InterpreterCallableWithContext(_, _) => false
                  ExecutorCallable(executable) => executable.is_constructable()
                }
                if !is_constructable ||
                  callable is ConstructorOnlyCallable(_, _) {
                  return Undefined
                }
                // For constructable functions, check if prototype exists
                match data.bag.properties.get("prototype") {
                  Some(p) => return p
                  None => {
                    // Create and memoize default prototype
                    let proto : Value = Object({
                      bag: PropertyBag(),
                      prototype: get_obj_proto(
                        realm_state=Some(self.realm_state),
                      ),
                      callable: None,
                      class_name: "Object",
                      extensible: true,
                      arraybuffer_state: None,
                    })
                    data.bag.properties["prototype"] = proto
                    return proto
                  }
                }
              }
            None => ()
          }
          // Built-in Object methods (only for objects that inherit from Object.prototype)
          if !(data.prototype is Null && data.callable is None) {
            if prop == "hasOwnProperty" {
              return make_method_func(
                name="hasOwnProperty",
                length=1,
                realm_state=Some(self.realm_state),
                fn(this_val, args) {
                  let key_val = if args.length() > 0 {
                    args[0]
                  } else {
                    Undefined
                  }
                  match this_val {
                    Object(d) =>
                      match key_val {
                        Symbol(sym) =>
                          Bool(d.bag.symbol_properties.contains(sym.id))
                        _ =>
                          Bool(d.bag.properties.contains(key_val.to_string()))
                      }
                    Array(ad) =>
                      match key_val {
                        Symbol(sym) =>
                          Bool(get_array_symbol_prop(ad, sym.id) is Some(_))
                        _ => {
                          let k = key_val.to_string()
                          if k == "length" {
                            return Bool(true)
                          }
                          let idx = @string.parse_int(k) catch { _ => -1 }
                          if idx >= 0 &&
                            idx.to_string() == k &&
                            idx < ad.elements.length() {
                            Bool(true)
                          } else {
                            Bool(get_array_named_prop(ad, k) is Some(_))
                          }
                        }
                      }
                    _ => Bool(false)
                  }
                },
              )
            }
            if prop == "toString" {
              return make_native_func(
                name="toString",
                realm_state=Some(self.realm_state),
                fn(_args) { String_("[object Object]") },
              )
            }
          }
          Undefined
        }
      }
    }
    Array(data) =>
      match key {
        Number(n) => {
          // Use Int64 to avoid overflow for indices >= 2^31.
          let i64 = n.to_int64()
          if n >= 0.0 && n == i64.to_double() && i64 <= 0x7FFFFFFFL {
            let i = i64.to_int()
            self.array_get_indexed(obj, data, i, i.to_string(), loc)
          } else {
            // Non-integer or very-large key: not handled by the dense
            // index path. Check own descriptors / named-prop bag first,
            // then fall through to prototype walk so inherited indexed
            // and non-indexed numeric keys are resolved per spec.
            let key_str = to_js_string(Number(n))
            match data.bag.descriptors.get(key_str) {
              Some(desc) =>
                match desc.getter {
                  Some(getter) => return self.call_value(getter, obj, [], loc)
                  None => if desc.setter is Some(_) { return Undefined }
                }
              None => ()
            }
            match get_array_named_prop(data, key_str) {
              Some(v) => v
              None => {
                let proto = get_array_prototype(self.realm_state, data)
                self.get_property_from_prototype(obj, proto, key_str, loc)
              }
            }
          }
        }
        String_("length") =>
          match get_array_length_override(data) {
            Some(n64) => Number(n64.to_double())
            None => Number(data.elements.length().to_double())
          }
        String_(s) => {
          // Canonical numeric index string check: parseDouble(s).toString() === s.
          // Mirror the Number(n) branch's Int-range discipline so large
          // numeric strings like "3000000000" (valid array length but
          // outside Int) flow into the non-index path and walk the
          // prototype, rather than overflowing `n.to_int()`.
          let mut is_index : Bool = false
          let mut idx : Int = -1
          try {
            let n = @string.parse_double(s)
            if n >= 0.0 && n.to_string() == s {
              let i64 = n.to_int64()
              if i64.to_double() == n && i64 <= 0x7FFFFFFFL {
                is_index = true
                idx = i64.to_int()
              }
            }
          } catch {
            _ => () // Not a numeric string
          }
          if is_index {
            self.array_get_indexed(obj, data, idx, s, loc)
          } else {
            // Non-index string key: own descriptor → method dispatch → prototype.
            match data.bag.descriptors.get(s) {
              Some(desc) =>
                match desc.getter {
                  Some(getter) => return self.call_value(getter, obj, [], loc)
                  None => if desc.setter is Some(_) { return Undefined }
                }
              None => ()
            }
            // Check bag-backed named properties, then prototype chain / methods.
            match get_array_named_prop(data, s) {
              Some(v) => v
              None =>
                // Prototype override (explicit Object.setPrototypeOf) must be
                // consulted before the built-in method shortcut so that, e.g.,
                // Object.setPrototypeOf(arr, { map: 42 }); arr["map"] === 42.
                match get_array_prototype_override(data) {
                  Some(proto) =>
                    self.get_property_from_prototype(obj, proto, s, loc)
                  None => {
                    // Mirror the `Interpreter::get_property` pattern: many
                    // built-in methods (map/filter/forEach/…) are delegated
                    // to the live `Array.prototype` and return Undefined
                    // from the shortcut. Fall through to the actual
                    // %Array.prototype% walk so `arr["map"]` resolves the
                    // built-in (and so non-index numeric strings keep
                    // ascending to `Object.prototype`).
                    let result = (self.stdlib_hooks.get_array_method_with_interp)(
                      data,
                      s,
                      self.realm_state,
                    )
                    match result {
                      Undefined =>
                        lookup_builtin_proto(self, obj, "Array", s, loc)
                      _ => result
                    }
                  }
                }
            }
          }
        }
        Symbol(sym) =>
          match get_array_symbol_prop(data, sym.id) {
            Some(v) => v
            None => Undefined
          }
        _ => {
          // Non-numeric, non-string, non-symbol key (e.g., Bool(true)).
          // Convert to string and check if it's a valid numeric index first.
          let key_str = to_js_string(key, interp=Some(self))
          let num_idx = @string.parse_int(key_str) catch { _ => -1 }
          if num_idx >= 0 && num_idx.to_string() == key_str {
            return self.array_get_indexed(obj, data, num_idx, key_str, loc)
          }
          // Check for "length" before doing named-property lookup — coerced
          // keys like ({ toString: () => "length" }) must return the array's
          // own length, not Array.prototype.length.
          if key_str == "length" {
            return match get_array_length_override(data) {
              Some(n64) => Number(n64.to_double())
              None => Number(data.elements.length().to_double())
            }
          }
          // Non-numeric string: do named-property lookup with descriptor check.
          match data.bag.descriptors.get(key_str) {
            Some(desc) =>
              match desc.getter {
                Some(getter) => return self.call_value(getter, obj, [], loc)
                None => if desc.setter is Some(_) { return Undefined }
              }
            None => ()
          }
          match get_array_named_prop(data, key_str) {
            Some(v) => v
            None => {
              let proto = get_array_prototype(self.realm_state, data)
              self.get_property_from_prototype(obj, proto, key_str, loc)
            }
          }
        }
      }
    String_(s) =>
      match key {
        Number(n) => {
          let i = n.to_int()
          let units = string_to_utf16(s)
          if i >= 0 && i.to_double() == n && i < units.length() {
            String_(String::make(1, units[i].unsafe_to_char()))
          } else {
            Undefined
          }
        }
        String_("length") => Number(utf16_length(s).to_double())
        String_(property_name) => {
          let index = @string.parse_int(property_name) catch { _ => -1 }
          let units = string_to_utf16(s)
          if index >= 0 &&
            index.to_string() == property_name &&
            index < units.length() {
            String_(String::make(1, units[index].unsafe_to_char()))
          } else {
            (self.stdlib_hooks.get_string_method)(
              s,
              property_name,
              self.realm_state,
              self.annex_b,
            )
          }
        }
        _ => Undefined
      }
    Number(_) =>
      match key {
        String_(method_name) =>
          (self.stdlib_hooks.get_number_method)(
            obj,
            method_name,
            self.realm_state,
          )
        _ => Undefined
      }
    Map(_) | Set(_) => {
      let prop = to_js_string(key, interp=Some(self))
      self.get_property(obj, prop, loc)
    }
    Promise(_) => {
      let prop = to_js_string(key, interp=Some(self))
      self.get_property(obj, prop, loc)
    }
    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 read properties of null (reading '\{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 read properties of undefined (reading '\{key_str}')",
      )
    }
    _ => Undefined
  }
}