///|
/// Get a trap function from the proxy handler, or None if the trap is not defined.
/// Throws TypeError if the proxy has been revoked.
/// Uses the engine's ordinary Get path (interp.get_property) so accessor traps
/// and proxy-handler proxies are resolved with full language semantics.
pub fn get_proxy_trap(
  proxy_data : ProxyData,
  trap_name : String,
  interp : Interpreter,
) -> Value? raise Error {
  let handler = match proxy_data.handler {
    Some(h) => h
    None =>
      raise @errors.TypeError(
        message="Cannot perform '\{trap_name}' on a proxy that has been revoked",
      )
  }
  let _ = match proxy_data.target {
    Some(_) => ()
    None =>
      raise @errors.TypeError(
        message="Cannot perform '\{trap_name}' on a proxy that has been revoked",
      )
  }
  // Use engine-level Get (GetMethod semantics per ES §10.5): this traverses
  // the prototype chain, invokes accessor getters, and handles proxy handlers
  // that are themselves proxies.
  let trap = interp.get_property(handler, trap_name, @token.Loc::default())
  match trap {
    Undefined | Null => None
    _ =>
      if is_callable(trap) {
        Some(trap)
      } else {
        raise @errors.TypeError(
          message="'\{trap_name}' on proxy: trap is not a function",
        )
      }
  }
}

///|
/// Get the target of a proxy, throwing TypeError if revoked.
pub fn get_proxy_target(proxy_data : ProxyData) -> Value raise Error {
  match proxy_data.target {
    Some(t) => t
    None =>
      raise @errors.TypeError(
        message="Cannot perform operation on a proxy that has been revoked",
      )
  }
}

///|
/// Get the handler of a proxy, throwing TypeError if revoked.
pub fn get_proxy_handler(proxy_data : ProxyData) -> Value raise Error {
  match proxy_data.handler {
    Some(h) => h
    None =>
      raise @errors.TypeError(
        message="Cannot perform operation on a proxy that has been revoked",
      )
  }
}

///|
/// Revoke a proxy by nullifying its target and handler slots. The cached
/// The cached call/construct classifications are intentionally left untouched:
/// revocation clears the target and handler slots, not the Proxy's internal
/// [[Call]] and [[Construct]] methods.
pub fn revoke_proxy(proxy_data : ProxyData) -> Unit {
  proxy_data.target = None
  proxy_data.handler = None
}

///|
fn typedarray_declared_length(data : ObjectData) -> Int {
  guard is_typedarray_class(data.class_name) else { return 0 }
  match data.bag.internal_slots.get(ArrayLength) {
    Some(Value::Number(n)) => n.to_int()
    _ => 0
  }
}

///|
fn is_typedarray_own_index_key(key : String, length : Int) -> Bool {
  guard length > 0 else { return false }
  let idx = @string.parse_int(key) catch { _ => -1 }
  idx >= 0 && idx < length && idx.to_string() == key
}

///|
/// Implement the Proxy [[IsExtensible]] internal method per ES §10.5.3.
pub fn proxy_is_extensible(
  interp : Interpreter,
  proxy_data : ProxyData,
) -> Bool raise Error {
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "isExtensible", interp)
  match trap {
    None => interp.is_extensible_internal(target)
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      let result = interp.call_value(trap_fn, handler, [target], loc)
      let trap_result = is_truthy(result)
      let target_extensible = interp.is_extensible_internal(target)
      if trap_result != target_extensible {
        raise @errors.TypeError(
          message="'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is '\{target_extensible}')",
        )
      }
      trap_result
    }
  }
}

///|
/// Get the descriptor for a string-keyed own property on the target.
/// Arrays synthesize `length` and indexed element descriptors; other own
/// named props live on `bag`.
fn target_get_own_descriptor(
  interp : Interpreter?,
  target : Value,
  key : String,
) -> PropDescriptor? raise Error {
  // Properties created via ordinary assignment (`o.x = 1`) live in
  // bag.properties without always receiving a bag.descriptors entry.
  // For §10.5.5 / §10.5.6 invariants we must report such keys as
  // existing with the default data-descriptor shape (w/e/c all true)
  // per §10.1.5.1 OrdinaryGetOwnProperty.
  let default_data : PropDescriptor = {
    writable: true,
    enumerable: true,
    configurable: true,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  match target {
    Object(data) =>
      if data.class_name == "Module" {
        match interp {
          Some(ip) =>
            match
              ip.module_namespace_get_own_property_pair(data, String_(key)) {
              Some((desc, _)) => Some(desc)
              None => None
            }
          None =>
            if data.bag.properties.contains(key) {
              Some(module_namespace_export_descriptor())
            } else {
              None
            }
        }
      } else {
        match data.bag.descriptors.get(key) {
          Some(d) => Some(d)
          None =>
            if data.bag.properties.contains(key) {
              Some(default_data)
            } else {
              None
            }
        }
      }
    Map(data) =>
      match data.bag.descriptors.get(key) {
        Some(d) => Some(d)
        None =>
          if data.bag.properties.contains(key) {
            Some(default_data)
          } else {
            None
          }
      }
    Set(data) =>
      match data.bag.descriptors.get(key) {
        Some(d) => Some(d)
        None =>
          if data.bag.properties.contains(key) {
            Some(default_data)
          } else {
            None
          }
      }
    Promise(data) =>
      match data.bag.descriptors.get(key) {
        Some(d) => Some(d)
        None =>
          if data.bag.properties.contains(key) {
            Some(default_data)
          } else {
            None
          }
      }
    Array(arr) => {
      if key == "length" {
        return Some({
          writable: arr.length_writable,
          enumerable: false,
          configurable: false,
          getter: None,
          setter: None,
          is_accessor: false,
        })
      }
      // Indexed element?
      let idx = @string.parse_int(key) catch { _ => -1 }
      if idx >= 0 && idx.to_string() == key && idx < arr.elements.length() {
        return match arr.bag.descriptors.get(key) {
          Some(d) => Some(d)
          None => Some(default_data)
        }
      }
      // Array named props surface through bag descriptors when explicitly
      // registered; assignment-created props synthesize the default shape.
      match arr.bag.descriptors.get(key) {
        Some(d) => Some(d)
        None =>
          if arr.bag.properties.contains(key) {
            Some(default_data)
          } else {
            None
          }
      }
    }
    _ => None
  }
}

///|
/// Read the own string-keyed value from the target, or Undefined if absent.
/// Mirrors the variant coverage of `target_get_own_descriptor`. Used by
/// proxy invariant checks that compare trap results to target values for
/// non-configurable non-writable data descriptors.
fn target_get_own_value(
  interp : Interpreter?,
  target : Value,
  key : String,
) -> Value raise Error {
  match target {
    Object(data) =>
      if data.class_name == "Module" {
        match interp {
          Some(ip) =>
            match
              ip.module_namespace_get_own_property_pair(data, String_(key)) {
              Some((_, value)) => value
              None => Undefined
            }
          None =>
            match data.bag.descriptors.get(key) {
              Some({ getter: Some(getter), .. }) =>
                call_callable_direct(getter, Object(data), [])
              _ => Undefined
            }
        }
      } else {
        match data.bag.properties.get(key) {
          Some(v) => v
          None => Undefined
        }
      }
    Map(data) =>
      match data.bag.properties.get(key) {
        Some(v) => v
        None => Undefined
      }
    Set(data) =>
      match data.bag.properties.get(key) {
        Some(v) => v
        None => Undefined
      }
    Promise(data) =>
      match data.bag.properties.get(key) {
        Some(v) => v
        None => Undefined
      }
    Array(arr) => {
      if key == "length" {
        return Value::Number(arr.elements.length().to_double())
      }
      let idx = @string.parse_int(key) catch { _ => -1 }
      if idx >= 0 && idx.to_string() == key && idx < arr.elements.length() {
        return arr.elements[idx]
      }
      match arr.bag.properties.get(key) {
        Some(v) => v
        None => Undefined
      }
    }
    _ => Undefined
  }
}

///|
/// Resolve the target's canonical [[GetOwnProperty]] descriptor/value pair
/// when an interpreter is available. The interpreter-free fallback exists for
/// the legacy public invariant helper and cannot observe Proxy internals.
fn target_get_own_property_pair_for_invariant(
  interp : Interpreter?,
  target : Value,
  key : Value,
) -> (PropDescriptor, Value)? raise Error {
  match interp {
    Some(ip) => ip.get_own_property(target, key)
    None =>
      match key {
        String_(name) =>
          match target_get_own_descriptor(None, target, name) {
            Some(desc) => Some((desc, target_get_own_value(None, target, name)))
            None => None
          }
        Symbol(_) =>
          match ordinary_get_own_property(target, key) {
            Some(desc) =>
              Some((desc, ordinary_get_own_value_for_descriptor(target, key)))
            None => None
          }
        _ => None
      }
  }
}

///|
/// Apply ES §10.5.8 steps 10-11 to a completed Proxy `get` trap. Both the
/// legacy call path and admitted activation dispatch use this single boundary
/// so the post-trap descriptor rules remain identical.
fn validate_proxy_get_trap_result(
  interp : Interpreter,
  target : Value,
  property_key : Value,
  result : Value,
) -> Unit raise Error {
  match
    target_get_own_property_pair_for_invariant(
      Some(interp),
      target,
      property_key,
    ) {
    Some((desc, target_value)) =>
      if !desc.configurable {
        if !desc.is_accessor &&
          !desc.writable &&
          !same_value(result, target_value) {
          raise @errors.TypeError(
            message="'get' on proxy: trap did not return the actual value of a read-only non-configurable target property",
          )
        }
        if desc.is_accessor &&
          desc.getter is None &&
          !same_value(result, Undefined) {
          raise @errors.TypeError(
            message="'get' on proxy: trap returned a value for a non-configurable target accessor without a getter",
          )
        }
      }
    None => ()
  }
}

///|
/// Implement the Proxy [[PreventExtensions]] internal method per ES §10.5.4.
pub fn proxy_prevent_extensions(
  interp : Interpreter,
  proxy_data : ProxyData,
) -> Bool raise Error {
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "preventExtensions", interp)
  match trap {
    None => interp.prevent_extensions_internal(target)
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      let result = interp.call_value(trap_fn, handler, [target], loc)
      let trap_result = is_truthy(result)
      // ES §10.5.4 step 8: If trap returns true, target must not be extensible
      if trap_result {
        let target_extensible = interp.is_extensible_internal(target)
        if target_extensible {
          raise @errors.TypeError(
            message="'preventExtensions' on proxy: trap returned truish but the proxy target is extensible",
          )
        }
      }
      trap_result
    }
  }
}

///|
/// Implement the Proxy [[GetPrototypeOf]] internal method per ES §10.5.1.
pub fn proxy_get_prototype_of(
  interp : Interpreter,
  proxy_data : ProxyData,
) -> Value raise Error {
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "getPrototypeOf", interp)
  match trap {
    None => object_get_prototype_of(interp, target)
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      let result = interp.call_value(trap_fn, handler, [target], loc)
      // Step 7: Result must be Object or Null
      match result {
        Object(_) | Null => ()
        _ =>
          raise @errors.TypeError(
            message="'getPrototypeOf' on proxy: trap returned neither object nor null",
          )
      }
      // Step 9-10: If target is non-extensible, result must match target's prototype
      let extensible = interp.is_extensible_internal(target)
      if !extensible {
        let target_proto = object_get_prototype_of(interp, target)
        if !strict_equal(result, target_proto) {
          raise @errors.TypeError(
            message="'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype",
          )
        }
      }
      result
    }
  }
}

///|
/// Implement the Proxy [[SetPrototypeOf]] internal method per ES §10.5.2.
pub fn proxy_set_prototype_of(
  interp : Interpreter,
  proxy_data : ProxyData,
  proto : Value,
) -> Bool raise Error {
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "setPrototypeOf", interp)
  match trap {
    None =>
      match object_set_prototype_of(interp, target, proto) {
        Bool(result) => result
        _ => false
      }
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      let result = interp.call_value(trap_fn, handler, [target, proto], loc)
      let trap_result = is_truthy(result)
      // Step 10-11: If target non-extensible, proto must match target's prototype
      if trap_result {
        let extensible = interp.is_extensible_internal(target)
        if !extensible {
          let target_proto = object_get_prototype_of(interp, target)
          if !strict_equal(proto, target_proto) {
            raise @errors.TypeError(
              message="'setPrototypeOf' on proxy: trap returned truish for setting a new prototype on a non-extensible proxy target",
            )
          }
        }
      }
      trap_result
    }
  }
}

///|
/// Implement the Proxy [[HasProperty]] invariant checks per ES §10.5.7.
/// Called after the has trap returns a result.
pub fn proxy_has_property(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : String,
) -> Bool raise Error {
  proxy_has_property_key(interp, proxy_data, String_(key))
}

///|
pub fn proxy_has_property_key(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : Value,
) -> Bool raise Error {
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "has", interp)
  let key_display = match key {
    Symbol(sym) => "Symbol(\{sym.description.unwrap_or_default()})"
    _ => to_js_string(key)
  }
  match trap {
    None =>
      match target {
        Value::Proxy(inner) => proxy_has_property_key(interp, inner, key)
        _ => interp.has_property_key(target, key)
      }
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      let result = interp.call_value(trap_fn, handler, [target, key], loc)
      let trap_result = is_truthy(result)
      // ES §10.5.7 step 9-10: If trap returns false, check invariants
      if !trap_result {
        let target_desc = interp.get_own_property(target, key)
        // Cannot report a non-configurable own property as non-existent
        match target_desc {
          Some((desc, _)) =>
            if !desc.configurable {
              raise @errors.TypeError(
                message="'has' on proxy: trap returned falsish for property '\{key_display}' which exists in the proxy target as non-configurable",
              )
            }
          None => ()
        }
        // Cannot report an own property as non-existent on non-extensible target
        if !interp.is_extensible_internal(target) && target_desc is Some(_) {
          raise @errors.TypeError(
            message="'has' on proxy: trap returned falsish for property '\{key_display}' but the proxy target is not extensible",
          )
        }
      }
      trap_result
    }
  }
}

///|
fn proxy_delete_key_display(key : Value) -> String {
  match key {
    Symbol(sym) => "Symbol(\{sym.description.unwrap_or_default()})"
    String_(s) => s
    _ => to_js_string(key) catch { _ => "" }
  }
}

///|
/// Implement the Proxy [[Delete]] invariant checks per ES §10.5.10.
pub fn proxy_delete_property(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : String,
) -> Bool raise Error {
  proxy_delete_property_key(interp, proxy_data, String_(key))
}

///|
fn proxy_delete_property_key(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : Value,
) -> Bool raise Error {
  let prop_key = match key {
    Symbol(_) | String_(_) => key
    _ => String_(to_js_string(key, interp=Some(interp)))
  }
  let key_display = proxy_delete_key_display(prop_key)
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "deleteProperty", interp)
  match trap {
    None =>
      match target {
        Value::Proxy(inner) =>
          proxy_delete_property_key(interp, inner, prop_key)
        _ => interp.delete_property_key(target, prop_key)
      }
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      let result = interp.call_value(trap_fn, handler, [target, prop_key], loc)
      let trap_result = is_truthy(result)
      // ES §10.5.10 step 10-11: Cannot delete non-configurable property
      if trap_result {
        let target_desc = interp.get_own_property(target, prop_key)
        match target_desc {
          Some((desc, _)) =>
            if !desc.configurable {
              raise @errors.TypeError(
                message="'deleteProperty' on proxy: trap returned truish for property '\{key_display}' which is non-configurable in the proxy target",
              )
            }
          None => ()
        }
        // Step 12: Cannot delete own property of non-extensible target
        if !interp.is_extensible_internal(target) && target_desc is Some(_) {
          raise @errors.TypeError(
            message="'deleteProperty' on proxy: trap returned truish for property '\{key_display}' on a non-extensible proxy target",
          )
        }
      }
      trap_result
    }
  }
}

///|
/// Implement Proxy [[Get]] for either kind of PropertyKey. Trap-less proxies
/// delegate to the target's receiver-aware [[Get]] rather than restarting a
/// normal property access with the target as Receiver.
fn proxy_get_key(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : Value,
  receiver : Value,
) -> Value raise Error {
  let prop_key = to_property_key(key, interp=Some(interp))
  // get_proxy_trap checks revocation and includes trap name in error message
  let trap = get_proxy_trap(proxy_data, "get", interp)
  let target = get_proxy_target(proxy_data)
  match trap {
    None =>
      interp.get_property_key_with_receiver(
        target,
        prop_key,
        receiver,
        @token.Loc::default(),
      )
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      let result = interp.call_value(
        trap_fn,
        handler,
        [target, prop_key, receiver],
        loc,
      )
      validate_proxy_get_trap_result(interp, target, prop_key, result)
      result
    }
  }
}

///|
/// String-key compatibility wrapper for existing [[Get]] call sites.
pub fn proxy_get(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : String,
  receiver : Value,
) -> Value raise Error {
  proxy_get_key(interp, proxy_data, String_(key), receiver)
}

///|
/// ES §10.5.9 step 11-12 invariant checks shared by the string-keyed
/// `proxy_set` and the computed (String or Symbol) path in
/// `set_computed_property`. Raises TypeError if the target's own descriptor
/// for `key` is non-configurable and the trap's truish return is a lie.
fn check_proxy_set_trap_invariants_with_interp(
  interp : Interpreter?,
  target : Value,
  key : Value,
  value : Value,
) -> Unit raise Error {
  let key_display = match key {
    String_(name) => name
    Symbol(sym) => "Symbol(\{sym.description.unwrap_or_default()})"
    _ => ""
  }
  let target_entry = target_get_own_property_pair_for_invariant(
    interp, target, key,
  )
  let (desc_opt, target_value) = match target_entry {
    Some((desc, current_value)) => (Some(desc), current_value)
    None => (None, Undefined)
  }
  match desc_opt {
    Some(desc) =>
      if !desc.configurable {
        if !desc.is_accessor && !desc.writable {
          if !same_value(value, target_value) {
            raise @errors.TypeError(
              message="'set' on proxy: trap returned truish for property '\{key_display}' which exists in the proxy target as a non-configurable and non-writable data property with a different value",
            )
          }
        }
        if desc.is_accessor && desc.setter is None {
          raise @errors.TypeError(
            message="'set' on proxy: trap returned truish for property '\{key_display}' which exists in the proxy target as a non-configurable accessor property without a setter",
          )
        }
      }
    None => ()
  }
}

///|
pub fn check_proxy_set_trap_invariants(
  target : Value,
  key : Value,
  value : Value,
) -> Unit raise Error {
  check_proxy_set_trap_invariants_with_interp(None, target, key, value)
}

///|
/// Implement the Proxy [[Set]] invariant checks per ES §10.5.9.
pub fn proxy_set(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : String,
  value : Value,
  receiver : Value,
  strict : Bool,
) -> Value raise Error {
  let trap = get_proxy_trap(proxy_data, "set", interp)
  let target = get_proxy_target(proxy_data)
  match trap {
    None =>
      // ES §10.5.9 step 5: target.[[Set]](P, V, Receiver) — preserve receiver
      interp.set_property(
        target,
        key,
        value,
        @token.Loc::default(),
        strict~,
        receiver~,
      )
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      let result = interp.call_value(
        trap_fn,
        handler,
        [target, String_(key), value, receiver],
        loc,
      )
      let trap_result = is_truthy(result)
      if !trap_result {
        if strict {
          raise @errors.TypeError(
            message="'set' on proxy: trap returned falsish for property '\{key}'",
          )
        }
        return value
      }
      check_proxy_set_trap_invariants_with_interp(
        Some(interp),
        target,
        String_(key),
        value,
      )
      value
    }
  }
}

///|
/// Implement the Proxy [[OwnPropertyKeys]] internal method per ES §10.5.11.
/// Calls the ownKeys trap, validates the result against the target's invariants,
/// and returns the validated list of keys as an Array value.
pub fn proxy_own_property_keys(
  interp : Interpreter,
  proxy_data : ProxyData,
) -> Value raise Error {
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "ownKeys", interp)
  // Step 6: If trap is undefined, return target.[[OwnPropertyKeys]]()
  match trap {
    None => return make_array(interp.own_property_keys(target))
    _ => ()
  }
  let trap_fn = trap.unwrap()
  let handler = get_proxy_handler(proxy_data)
  let loc = @token.Loc::default()
  // Step 7: Call trap(handler, target)
  let trap_result = interp.call_value(trap_fn, handler, [target], loc)
  // Step 8: CreateListFromArrayLike(trapResult, « String, Symbol »)
  guard is_object_type(trap_result) else {
    raise @errors.TypeError(
      message="'ownKeys' on proxy: trap returned non-object",
    )
  }
  let trap_keys : Array[Value] = []
  let trap_result_len = to_array_like_length_interp(trap_result, interp)
  for i = 0L; i < trap_result_len; i = i + 1L {
    let elem = get_array_like_element_interp(interp, trap_result, i)
    match elem {
      Value::String_(_) | Value::Symbol(_) => trap_keys.push(elem)
      _ =>
        raise @errors.TypeError(
          message="'ownKeys' on proxy: trap result must contain only Strings and Symbols",
        )
    }
  }
  // Step 9: If trapResult contains any duplicate entries, throw TypeError
  let seen_strings = @set.Set::default()
  let seen_symbols : Map[Int, Bool] = Map([])
  for key in trap_keys {
    match key {
      Value::String_(s) => {
        if seen_strings.contains(s) {
          raise @errors.TypeError(
            message="'ownKeys' on proxy: trap returned duplicate key '\{s}'",
          )
        }
        seen_strings.add(s)
      }
      Value::Symbol(sym) => {
        if seen_symbols.contains(sym.id) {
          raise @errors.TypeError(
            message="'ownKeys' on proxy: trap returned duplicate Symbol key",
          )
        }
        seen_symbols[sym.id] = true
      }
      _ => ()
    }
  }
  // Step 10: Get extensibleTarget
  let extensible_target = interp.is_extensible_internal(target)
  // Step 11-12: Get target keys, split into non-configurable and configurable
  let target_keys = interp.own_property_keys(target)
  let target_nonconfigurable_keys : Array[Value] = []
  let target_configurable_keys : Array[Value] = []
  for key in target_keys {
    let non_configurable = match interp.get_own_property(target, key) {
      Some((desc, _)) => !desc.configurable
      None => false
    }
    if non_configurable {
      target_nonconfigurable_keys.push(key)
    } else {
      target_configurable_keys.push(key)
    }
  }
  // Step 15: If extensibleTarget is true and targetNonconfigurableKeys is empty, return
  if extensible_target && target_nonconfigurable_keys.is_empty() {
    return make_array(trap_keys)
  }
  // Step 16: Let uncheckedResultKeys be a copy of trapResult
  let unchecked : Map[String, Int] = Map([]) // string key -> count
  let unchecked_sym : Map[Int, Int] = Map([]) // symbol id -> count
  for key in trap_keys {
    match key {
      Value::String_(s) =>
        unchecked[s] = match unchecked.get(s) {
          Some(n) => n + 1
          None => 1
        }
      Value::Symbol(sym) =>
        unchecked_sym[sym.id] = match unchecked_sym.get(sym.id) {
          Some(n) => n + 1
          None => 1
        }
      _ => ()
    }
  }
  // Step 17: For each key in targetNonconfigurableKeys, must be in trapResult
  for key in target_nonconfigurable_keys {
    match key {
      Value::String_(s) =>
        match unchecked.get(s) {
          Some(n) if n > 0 => unchecked[s] = n - 1
          _ =>
            raise @errors.TypeError(
              message="'ownKeys' on proxy: trap result did not include '\{s}'",
            )
        }
      Value::Symbol(sym) =>
        match unchecked_sym.get(sym.id) {
          Some(n) if n > 0 => unchecked_sym[sym.id] = n - 1
          _ =>
            raise @errors.TypeError(
              message="'ownKeys' on proxy: trap result did not include a required Symbol key",
            )
        }
      _ => ()
    }
  }
  // Step 18: If extensibleTarget is true, return trapResult
  if extensible_target {
    return make_array(trap_keys)
  }
  // Step 19: For each key in targetConfigurableKeys, must be in unchecked
  for key in target_configurable_keys {
    match key {
      Value::String_(s) =>
        match unchecked.get(s) {
          Some(n) if n > 0 => unchecked[s] = n - 1
          _ =>
            raise @errors.TypeError(
              message="'ownKeys' on proxy: trap result did not include '\{s}'",
            )
        }
      Value::Symbol(sym) =>
        match unchecked_sym.get(sym.id) {
          Some(n) if n > 0 => unchecked_sym[sym.id] = n - 1
          _ =>
            raise @errors.TypeError(
              message="'ownKeys' on proxy: trap result did not include a required Symbol key",
            )
        }
      _ => ()
    }
  }
  // Step 20: If uncheckedResultKeys is not empty, throw TypeError
  let mut remaining = 0
  unchecked.each(fn(_k, v) { remaining = remaining + v })
  unchecked_sym.each(fn(_k, v) { remaining = remaining + v })
  if remaining > 0 {
    raise @errors.TypeError(
      message="'ownKeys' on proxy: trap returned extra keys for a non-extensible target",
    )
  }
  make_array(trap_keys)
}

// ---------------------------------------------------------------------------
// Stage B.2: [[GetOwnProperty]] + [[DefineOwnProperty]] traps
// ---------------------------------------------------------------------------

///|
/// Convert a PartialDescriptor into a plain JS descriptor object so it can
/// be passed to a Proxy defineProperty trap as the `Descriptor` argument.
/// Per ES §6.2.5.4 FromPropertyDescriptor, only present fields are emitted
/// (absent fields don't appear on the object).
fn partial_desc_to_value(
  partial : PartialDescriptor,
  realm_state : RealmState,
) -> Value {
  let props : Map[String, Value] = Map([])
  match partial.value {
    Some(v) => props["value"] = v
    None => ()
  }
  match partial.writable {
    Some(w) => props["writable"] = Value::Bool(w)
    None => ()
  }
  match partial.enumerable {
    Some(e) => props["enumerable"] = Value::Bool(e)
    None => ()
  }
  match partial.configurable {
    Some(c) => props["configurable"] = Value::Bool(c)
    None => ()
  }
  if partial.has_getter {
    props["get"] = partial.getter.unwrap_or(Undefined)
  }
  if partial.has_setter {
    props["set"] = partial.setter.unwrap_or(Undefined)
  }
  Value::Object({
    bag: {
      properties: props,
      symbol_properties: Map([]),
      descriptors: Map([]),
      symbol_descriptors: Map([]),
      internal_slots: Map([]),
      host_slots: Map([]),
    },
    prototype: realm_state.get_obj_proto(),
    extensible: true,
    arraybuffer_state: None,
    callable: None,
    class_name: "Object",
  })
}

///|
/// Parse a Proxy getOwnPropertyDescriptor trap's result Value into a
/// PropDescriptor. Approximates §6.2.5.5 CompletePropertyDescriptor:
/// fill absent writable/enumerable/configurable with false. Trap is
/// expected to return an object (caller validates non-object rejection).
fn trap_result_to_descriptor(
  trap_result : Value,
  interp : Interpreter,
) -> PropDescriptor raise Error {
  let loc = @token.Loc::default()
  // §6.2.5.5 ToPropertyDescriptor step 4: data and accessor fields are
  // mutually exclusive. A trap result carrying both shapes is invalid.
  let has_get = has_property(trap_result, "get", interp=Some(interp))
  let has_set = has_property(trap_result, "set", interp=Some(interp))
  let has_value_fld = has_property(trap_result, "value", interp=Some(interp))
  let has_writable_fld = has_property(
    trap_result,
    "writable",
    interp=Some(interp),
  )
  if (has_get || has_set) && (has_value_fld || has_writable_fld) {
    raise @errors.TypeError(
      message="Invalid property descriptor. Cannot both specify accessors and a value or writable attribute",
    )
  }
  let writable = if has_property(trap_result, "writable", interp=Some(interp)) {
    is_truthy(interp.get_property(trap_result, "writable", loc))
  } else {
    false
  }
  let enumerable = if has_property(
      trap_result,
      "enumerable",
      interp=Some(interp),
    ) {
    is_truthy(interp.get_property(trap_result, "enumerable", loc))
  } else {
    false
  }
  let configurable = if has_property(
      trap_result,
      "configurable",
      interp=Some(interp),
    ) {
    is_truthy(interp.get_property(trap_result, "configurable", loc))
  } else {
    false
  }
  // §6.2.5.5 step 7 / 9: get/set must be callable when present and non-undefined.
  let getter = if has_property(trap_result, "get", interp=Some(interp)) {
    let g = interp.get_property(trap_result, "get", loc)
    match g {
      Undefined => None
      _ => {
        if !is_callable(g) {
          raise @errors.TypeError(
            message="Getter must be a function: \{g.to_string()}",
          )
        }
        Some(g)
      }
    }
  } else {
    None
  }
  let setter = if has_property(trap_result, "set", interp=Some(interp)) {
    let s = interp.get_property(trap_result, "set", loc)
    match s {
      Undefined => None
      _ => {
        if !is_callable(s) {
          raise @errors.TypeError(
            message="Setter must be a function: \{s.to_string()}",
          )
        }
        Some(s)
      }
    }
  } else {
    None
  }
  {
    writable,
    enumerable,
    configurable,
    getter,
    setter,
    is_accessor: has_get || has_set,
  }
}

///|
/// Extract the value field from a trap result, if present.
fn trap_result_value(
  trap_result : Value,
  interp : Interpreter,
) -> Value? raise Error {
  if has_property(trap_result, "value", interp=Some(interp)) {
    Some(interp.get_property(trap_result, "value", @token.Loc::default()))
  } else {
    None
  }
}

///|
/// Is this value a plain object (can hold a descriptor)? Proxy, Map, Set,
/// Promise, Array qualify per §7.2.5 IsObject (but for GOPD trap result
/// we want an Object-ish shape with readable properties).
fn is_object_type(val : Value) -> Bool {
  match val {
    Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) => true
    _ => false
  }
}

///|
/// ES §10.5.5 `[[GetOwnProperty]]` (Proxy). Invokes the
/// `getOwnPropertyDescriptor` trap with the canonical invariant set:
/// - trap returns non-object non-undefined -> TypeError
/// - target's own desc is non-configurable AND trap returns undefined -> TypeError
/// - target is non-extensible + no own desc + trap returns object -> TypeError
/// - trap reports non-configurable + non-writable; target is NOT both -> TypeError
/// - target non-configurable accessor: getter/setter identity must match (SameValue)
/// Returns the completed descriptor or None.
pub fn proxy_get_own_property(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : Value,
) -> (PropDescriptor, Value)? raise Error {
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "getOwnPropertyDescriptor", interp)
  match trap {
    None => interp.get_own_property(target, key)
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      // §7.1.19 ToPropertyKey — use to_js_string so object-typed keys
      // with Symbol.toPrimitive/toString hooks coerce per spec, matching
      // the key the validation path will subsequently look up.
      let key_val = match key {
        Symbol(_) => key
        _ => Value::String_(to_js_string(key))
      }
      let trap_result = interp.call_value(
        trap_fn,
        handler,
        [target, key_val],
        loc,
      )
      // §10.5.5 step 8: trap result must be object or undefined.
      match trap_result {
        Undefined => ()
        _ =>
          if !is_object_type(trap_result) {
            raise @errors.TypeError(
              message="'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined",
            )
          }
      }
      let target_entry = interp.get_own_property(target, key_val)
      let target_desc = match target_entry {
        Some((desc, _)) => Some(desc)
        None => None
      }
      // §10.5.5 step 10: trap returns undefined.
      if trap_result is Undefined {
        match target_desc {
          None => return None
          Some(desc) => {
            // Step 10.b: target desc must be configurable.
            if !desc.configurable {
              raise @errors.TypeError(
                message="'getOwnPropertyDescriptor' on proxy: trap returned undefined for property which exists as non-configurable on the proxy target",
              )
            }
            // Step 10.c: target must be extensible.
            if !interp.is_extensible_internal(target) {
              raise @errors.TypeError(
                message="'getOwnPropertyDescriptor' on proxy: trap returned undefined for property which exists on a non-extensible proxy target",
              )
            }
            return None
          }
        }
      }
      // §10.5.5 step 11+: parse trap descriptor; run invariants.
      let trap_desc = trap_result_to_descriptor(trap_result, interp)
      let trap_value_opt = if !trap_desc.is_accessor {
        trap_result_value(trap_result, interp)
      } else {
        None
      }
      let target_extensible = interp.is_extensible_internal(target)
      match target_desc {
        None => {
          // §10.5.5 step 17.c: target has no own desc; target must be
          // extensible AND the trap must not report non-configurable
          // (can't invent a non-configurable key that doesn't exist).
          if !target_extensible {
            raise @errors.TypeError(
              message="'getOwnPropertyDescriptor' on proxy: trap reported non-existent property on a non-extensible target",
            )
          }
          if !trap_desc.configurable {
            raise @errors.TypeError(
              message="'getOwnPropertyDescriptor' on proxy: trap reported non-configurable for a key that does not exist on the proxy target",
            )
          }
        }
        Some(existing) => {
          // Step 17.a: cannot report non-configurable when target is configurable.
          if !trap_desc.configurable && existing.configurable {
            raise @errors.TypeError(
              message="'getOwnPropertyDescriptor' on proxy: trap reported non-configurable for property which is configurable on the proxy target",
            )
          }
          // §10.5.5 step 17.a (bis): enumerable must match when target is
          // non-configurable. Missing from the prior invariant set.
          if !existing.configurable &&
            trap_desc.enumerable != existing.enumerable {
            raise @errors.TypeError(
              message="'getOwnPropertyDescriptor' on proxy: trap reported different enumerable for non-configurable property on proxy target",
            )
          }
          // Step 17.b: cannot report non-configurable + non-writable unless target matches.
          let trap_is_data = !trap_desc.is_accessor
          let existing_is_data = !existing.is_accessor
          if trap_is_data &&
            existing_is_data &&
            !trap_desc.configurable &&
            !trap_desc.writable &&
            existing.writable {
            raise @errors.TypeError(
              message="'getOwnPropertyDescriptor' on proxy: trap reported non-configurable and non-writable for property which is writable on the proxy target",
            )
          }
          // §10.5.5 step 17.b SameValue extension: when target is frozen
          // (nc+nw data) the trap's reported value must equal target's.
          // Compare only when both sides are data and target is nc+nw.
          if trap_is_data &&
            existing_is_data &&
            !existing.configurable &&
            !existing.writable {
            let cur_val = match target_entry {
              Some((_, value)) => value
              None => Undefined
            }
            let trap_val = match trap_value_opt {
              Some(v) => v
              None => Undefined
            }
            if !same_value(trap_val, cur_val) {
              raise @errors.TypeError(
                message="'getOwnPropertyDescriptor' on proxy: trap reported different value for non-configurable non-writable data property on proxy target",
              )
            }
          }
          // §10.5.5 step 17.a (descriptor-kind invariance for non-configurable):
          // a non-configurable property's descriptor kind (data vs accessor)
          // must not change. Reject data→accessor AND accessor→data
          // reports on a non-configurable target.
          if !existing.configurable && existing_is_data != trap_is_data {
            raise @errors.TypeError(
              message="'getOwnPropertyDescriptor' on proxy: trap reported descriptor kind (data/accessor) mismatch for non-configurable property on proxy target",
            )
          }
          // Accessor identity for existing non-configurable accessor.
          if !existing.configurable && !existing_is_data && !trap_is_data {
            let g_ok = match (existing.getter, trap_desc.getter) {
              (None, None) => true
              (Some(a), Some(b)) => strict_equal(a, b)
              _ => false
            }
            let s_ok = match (existing.setter, trap_desc.setter) {
              (None, None) => true
              (Some(a), Some(b)) => strict_equal(a, b)
              _ => false
            }
            if !g_ok || !s_ok {
              raise @errors.TypeError(
                message="'getOwnPropertyDescriptor' on proxy: trap reported mismatched accessor for non-configurable accessor on proxy target",
              )
            }
          }
        }
      }
      let trap_value = match trap_value_opt {
        Some(v) => v
        None => Undefined
      }
      Some((trap_desc, trap_value))
    }
  }
}

///|
/// ES §10.5.6 `[[DefineOwnProperty]]` (Proxy). Invokes the `defineProperty`
/// trap and validates:
/// - trap returns falsy -> return false
/// - trap returns true + target has no own desc + target non-extensible -> TypeError
/// - trap returns true + incoming descriptor is non-configurable + target
///   descriptor doesn't exist / is configurable -> TypeError
/// - trap returns true + existing target desc non-configurable incompatible
///   with incoming -> TypeError
pub fn proxy_define_property(
  interp : Interpreter,
  proxy_data : ProxyData,
  key : Value,
  partial : PartialDescriptor,
) -> Bool raise Error {
  let target = get_proxy_target(proxy_data)
  let trap = get_proxy_trap(proxy_data, "defineProperty", interp)
  match trap {
    None =>
      interp.define_own_property(target, key, partial, @token.Loc::default())
    Some(trap_fn) => {
      let handler = get_proxy_handler(proxy_data)
      let loc = @token.Loc::default()
      // §7.1.19 ToPropertyKey — use to_js_string so object-typed keys
      // with Symbol.toPrimitive/toString hooks coerce per spec, matching
      // the key the validation path will subsequently look up.
      let key_val = match key {
        Symbol(_) => key
        _ => Value::String_(to_js_string(key))
      }
      let desc_val = partial_desc_to_value(partial, interp.realm_state)
      let result = interp.call_value(
        trap_fn,
        handler,
        [target, key_val, desc_val],
        loc,
      )
      let trap_result = is_truthy(result)
      if !trap_result {
        return false
      }
      // Invariant checks: §10.5.6 step 14+.
      let target_entry = interp.get_own_property(target, key_val)
      let target_desc = match target_entry {
        Some((desc, _)) => Some(desc)
        None => None
      }
      let target_extensible = interp.is_extensible_internal(target)
      // is_new: key is absent as an own property. Use the target's real
      // [[GetOwnProperty]] dispatcher so synthetic TypedArray integer-index
      // descriptors count as existing.
      let is_new = target_desc is None
      // Step 16.b: cannot add a new non-configurable property to non-extensible target.
      if is_new {
        if !target_extensible {
          raise @errors.TypeError(
            message="'defineProperty' on proxy: trap returned truish for adding property to non-extensible target",
          )
        }
        if partial.configurable is Some(false) {
          raise @errors.TypeError(
            message="'defineProperty' on proxy: trap returned truish for defining non-configurable property which does not exist on proxy target",
          )
        }
      } else {
        // Bag-only props (ordinary assignment, no explicit descriptor entry)
        // default to {writable: true, enumerable: true, configurable: true}
        // per §10.1.5.1 OrdinaryGetOwnProperty step 3.
        let current : PropDescriptor = match target_desc {
          Some(d) => d
          None =>
            {
              writable: true,
              enumerable: true,
              configurable: true,
              getter: None,
              setter: None,
              is_accessor: false,
            }
        }
        // Step 18.b: if incoming is non-configurable, existing must also be.
        if partial.configurable is Some(false) && current.configurable {
          raise @errors.TypeError(
            message="'defineProperty' on proxy: trap returned truish for defining non-configurable property on proxy target where property is configurable",
          )
        }
        // §10.5.6 step 18.c: non-configurable existing + incoming data
        // reporting writable:false over existing writable:true -> TypeError.
        // (Spec requires target's writable to remain true if the trap succeeds
        //  but didn't report non-writable; conversely, a lying non-writable
        //  over a non-configurable writable target is invariant-busting.)
        let current_is_data = current.getter is None && current.setter is None
        if !current.configurable && current_is_data && current.writable {
          if partial.writable is Some(false) {
            raise @errors.TypeError(
              message="'defineProperty' on proxy: trap returned truish for non-writable redefinition of a non-configurable writable data property on proxy target",
            )
          }
        }
        // Step 18.c full compatibility when existing is non-configurable.
        if !current.configurable {
          let cur_val = match target_entry {
            Some((_, value)) => value
            None => Undefined
          }
          if !is_compatible_with_non_configurable(current, partial, cur_val) {
            raise @errors.TypeError(
              message="'defineProperty' on proxy: trap returned truish for incompatible redefinition of non-configurable property on proxy target",
            )
          }
        }
      }
      true
    }
  }
}