///|
pub fn is_truthy(val : Value) -> Bool {
  match val {
    Number(n) => n != 0.0 && !n.is_nan()
    String_(s) => s.length() > 0
    Bool(b) => b
    Null => false
    Undefined => false
    Object(_) => true
    Array(_) => true
    Symbol(_) => true // Symbols are always truthy
    Map(_) => true // Maps are always truthy
    Set(_) => true // Sets are always truthy
    Promise(_) => true // Promises are always truthy
    Proxy(_) => true // Proxies are always truthy
  }
}

///|
/// Returns true for Value variants that are JS object types (not primitives).
/// Used to detect when ToPrimitive returns a non-primitive, which is a TypeError.
/// Update here if new object-like Value variants are added.
fn is_js_object(val : Value) -> Bool {
  match val {
    Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) => true
    _ => false
  }
}

///|
fn resolve_effective_interp(interp : Interpreter?) -> Interpreter? {
  interp
}

///|
/// Call a callable value, dispatching to the interpreter for UserFunc types.
/// Supports all callable types including user-defined functions.
/// Pass `interp` explicitly when user-code dispatch may be required.
pub fn call_callable_direct(
  callable_val : Value,
  this_val : Value,
  args : Array[Value],
  interp? : Interpreter? = None,
) -> Value raise Error {
  match resolve_effective_interp(interp) {
    Some(ip) =>
      ip.call_value(callable_val, this_val, args, @token.Loc::default())
    None =>
      match callable_val {
        Object(obj_data) =>
          match obj_data.callable {
            Some(MethodCallable(_, f)) => f(this_val, args)
            Some(NativeCallable(_, f)) => f(args)
            Some(NativeCallableWithContext(_, f)) => f(Call, args)
            Some(NonConstructableCallable(_, f)) => f(args)
            Some(UserFunc(_))
            | Some(ArrowFunc(_))
            | Some(UserFuncExt(_))
            | Some(ArrowFuncExt(_))
            | Some(BoundFunc(_, _, _))
            | Some(InterpreterCallable(_, _))
            | Some(InterpreterCallableWithContext(_, _))
            | Some(ExecutorCallable(_))
            | Some(NonConstructableInterpreterCallable(_, _))
            | Some(ClassConstructor(_)) =>
              raise @errors.TypeError(
                message="Cannot call user function without interpreter context",
              )
            _ => raise @errors.TypeError(message="Value is not callable")
          }
        _ => raise @errors.TypeError(message="Value is not callable")
      }
  }
}

///|
/// SetFunctionName: set name property on anonymous functions per ES2015+.
/// Only sets name if the current name is empty (anonymous).
pub fn set_function_name(val : Value, name : String) -> Unit {
  match val {
    Object(data) if data.callable is Some(_) =>
      // Only set if current name is empty (anonymous)
      match data.bag.properties.get("name") {
        Some(String_(s)) if s != "" => ()
        Some(String_(_)) | None => data.bag.properties["name"] = String_(name)
        _ => ()
      }
    _ => ()
  }
}

///|
/// Check if a value is callable
pub fn is_callable(val : Value) -> Bool {
  match val {
    Object(obj_data) => obj_data.callable is Some(_)
    Proxy(proxy_data) => proxy_data.is_callable
    _ => false
  }
}

///|
/// Look up a string-keyed property on an object, walking the prototype chain.
/// HasProperty: check if a named property exists on the object or its prototype chain
pub fn has_property(
  val : Value,
  name : String,
  interp? : Interpreter? = None,
) -> Bool {
  match val {
    Object(data) => {
      // Pure slot-presence check per ES [[HasProperty]] — must NOT invoke getters.
      // Route through has_object_property (uses .contains(), no getter calls) when
      // an interpreter is available; fall back to own-slot only when there is none.
      let effective = resolve_effective_interp(interp)
      match effective {
        Some(ip) =>
          ip.has_object_property(data, String_(name)) catch {
            _ => false
          }
        None =>
          data.bag.properties.contains(name) ||
          data.bag.descriptors.contains(name)
      }
    }
    Array(data) => {
      if name == "length" {
        return true
      }
      try {
        let n = @string.parse_double(name)
        let i = n.to_int()
        if i.to_double() == n && i >= 0 && i < data.elements.length() {
          return true
        }
      } catch {
        _ => ()
      }
      if get_array_named_prop(data, name) is Some(_) {
        return true
      }
      // Walk Array.prototype chain for inherited properties
      let effective = resolve_effective_interp(interp)
      match effective {
        Some(ip) => {
          let ctor = ip.global.get("Array") catch { _ => return false }
          match ctor {
            Object(ctor_data) =>
              match ctor_data.bag.properties.get("prototype") {
                Some(Object(proto_data)) =>
                  ip.has_object_property(proto_data, String_(name)) catch {
                    _ => false
                  }
                _ => false
              }
            _ => false
          }
        }
        None => false
      }
    }
    _ => false
  }
}

///|
/// Invoke bag.descriptors[name].getter with obj_val as receiver, if one exists.
/// Returns Some(result) if an accessor getter was found and invoked (or if no
/// interpreter is available, Some(Undefined) signals property existence).
/// Returns None if no accessor descriptor with a getter exists for name.
fn invoke_accessor_getter(
  bag : PropertyBag,
  name : String,
  obj_val : Value,
  effective : Interpreter?,
) -> Value? raise Error {
  match bag.descriptors.get(name) {
    Some(desc) =>
      match desc.getter {
        Some(getter_fn) =>
          match effective {
            Some(_) =>
              Some(
                call_callable_direct(getter_fn, obj_val, [], interp=effective),
              )
            None => Some(Undefined)
          }
        None => None
      }
    None => None
  }
}

///|
/// Look up a string-keyed property on an object, walking the prototype chain.
/// Handles both data properties (bag.properties) and accessor properties
/// (bag.descriptors with getter), invoking getters with obj_val as receiver.
fn lookup_property_chain(
  obj_val : Value,
  data : ObjectData,
  name : String,
  interp? : Interpreter? = None,
) -> Value? raise Error {
  let effective = resolve_effective_interp(interp)
  // Descriptor check first: accessors write Undefined into bag.properties as a
  // sentinel (see apply_descriptor_to_bag / eval_expr ObjectLit Get/Set arms),
  // so checking bag.properties first would return that sentinel and skip the
  // getter. Mirror get_property_of_object: descriptor → data.
  match invoke_accessor_getter(data.bag, name, obj_val, effective) {
    Some(v) => return Some(v)
    None => ()
  }
  match data.bag.properties.get(name) {
    Some(v) => return Some(v)
    None => ()
  }
  // Walk prototype chain
  let mut current = data.prototype
  let mut func_fallback_used = false
  while true {
    match current {
      Object(proto_data) => {
        match invoke_accessor_getter(proto_data.bag, name, obj_val, effective) {
          Some(v) => return Some(v)
          None => ()
        }
        match proto_data.bag.properties.get(name) {
          Some(v) => return Some(v)
          None => ()
        }
        current = proto_data.prototype
      }
      Null =>
        // For function objects, fall back to Function.prototype once
        if !func_fallback_used {
          match data.callable {
            Some(_) =>
              match effective {
                Some(ip) => {
                  let fp = ip.global.get("[[FunctionPrototype]]") catch {
                    _ => break
                  }
                  func_fallback_used = true
                  current = fp
                }
                None => break
              }
            None => break
          }
        } else {
          break
        }
      _ => break
    }
  }
  None
}

///|
/// Look up a symbol-keyed property on an object, walking the prototype chain.
/// Also handles getter descriptors - invokes them and returns the result.
pub fn lookup_symbol_property_chain(
  obj_val : Value,
  data : ObjectData,
  sym_id : Int,
  interp? : Interpreter? = None,
) -> Value? raise Error {
  // Check own symbol descriptors for getter first
  match data.bag.symbol_descriptors.get(sym_id) {
    Some(desc) =>
      match desc.getter {
        Some(getter_fn) => {
          let result = call_callable_direct(getter_fn, obj_val, [], interp~)
          return Some(result)
        }
        None => ()
      }
    None => ()
  }
  // Check own symbol properties
  match data.bag.symbol_properties.get(sym_id) {
    Some(v) => return Some(v)
    None => ()
  }
  // Walk prototype chain
  let mut current = data.prototype
  while true {
    match current {
      Object(proto_data) => {
        // Check symbol descriptors for getter
        match proto_data.bag.symbol_descriptors.get(sym_id) {
          Some(desc) =>
            match desc.getter {
              Some(getter_fn) => {
                let result = call_callable_direct(
                  getter_fn,
                  obj_val,
                  [],
                  interp~,
                )
                return Some(result)
              }
              None => ()
            }
          None => ()
        }
        match proto_data.bag.symbol_properties.get(sym_id) {
          Some(v) => return Some(v)
          None => current = proto_data.prototype
        }
      }
      _ => break
    }
  }
  None
}

///|
/// ES §7.1.1 ToPrimitive step 1: look up @@toPrimitive on the object and call
/// it with the given hint.  Handles GetMethod semantics (undefined/null → fall
/// through) and validates that the result is not an Object.
///
/// Returns Some(primitive) if @@toPrimitive was found and produced a primitive.
/// Returns None if @@toPrimitive is absent — caller proceeds to OrdinaryToPrimitive.
/// Raises TypeError if @@toPrimitive is not callable or returns an object.
fn call_symbol_to_primitive(
  obj_val : Value,
  hint : String,
  interp? : Interpreter? = None,
) -> Value? raise Error {
  let effective = resolve_effective_interp(interp)
  let to_prim_sym = match effective {
    Some(ip) => ip.realm_state.well_known_symbols.to_primitive
    None => return None
  }
  let method_value = match effective {
    Some(ip) =>
      Some(
        ip.get_property_key_with_receiver(
          obj_val,
          Symbol(to_prim_sym),
          obj_val,
          @token.Loc::default(),
        ),
      )
    None =>
      match obj_val {
        Object(data) =>
          lookup_symbol_property_chain(
            obj_val,
            data,
            to_prim_sym.id,
            interp=effective,
          )
        _ => None
      }
  }
  match method_value {
    Some(Undefined) | Some(Null) | None => None
    Some(exotic_to_prim) => {
      if !is_callable(exotic_to_prim) {
        raise @errors.TypeError(message="Symbol.toPrimitive is not a function")
      }
      let result = call_callable_direct(
        exotic_to_prim,
        obj_val,
        [String_(hint)],
        interp=effective,
      )
      if is_js_object(result) {
        raise @errors.TypeError(
          message="Cannot convert object to primitive value",
        )
      }
      Some(result)
    }
  }
}

///|
/// ToPrimitive(input, hint "number") - converts an object to a primitive value.
/// Follows the ECMAScript spec: check @@toPrimitive, then valueOf, then toString.
pub fn to_primitive_number(
  obj_val : Value,
  data : ObjectData,
  interp? : Interpreter? = None,
) -> Value raise Error {
  match call_symbol_to_primitive(obj_val, "number", interp~) {
    Some(result) => return result
    None => ()
  }
  ordinary_to_primitive_number(obj_val, data, interp~)
}

///|
/// Locate a named method on an object for OrdinaryToPrimitive.
/// Mirrors ES Get(O, name):
///   1. Walk the prototype chain (handles descriptors/getters).
///   2. If not found there, fall back to the interpreter's get_property
///      (reaches synthetic props that live on the global, e.g. Function.prototype).
/// Chain-path values are returned as-is; callers check is_callable before invoking.
/// Fallback-path values are filtered: only callable results become Some.
fn lookup_ordinary_method(
  obj_val : Value,
  data : ObjectData,
  method_name : String,
  interp? : Interpreter? = None,
) -> Value? raise Error {
  let effective = resolve_effective_interp(interp)
  match lookup_property_chain(obj_val, data, method_name, interp~) {
    Some(f) => Some(f)
    None =>
      match effective {
        Some(ip) => {
          let v = ip.get_property(obj_val, method_name, @token.Loc::default())
          if is_callable(v) {
            Some(v)
          } else {
            None
          }
        }
        None => None
      }
  }
}

///|
/// OrdinaryToPrimitive(O, "number") - try valueOf first, then toString.
/// Extracted so callers (to_primitive_default, to_primitive_number) can run
/// the ordinary lookup without re-entering the @@toPrimitive check.
fn ordinary_to_primitive_number(
  obj_val : Value,
  data : ObjectData,
  interp? : Interpreter? = None,
) -> Value raise Error {
  match lookup_ordinary_method(obj_val, data, "valueOf", interp~) {
    Some(vo_fn) =>
      if is_callable(vo_fn) {
        let result = call_callable_direct(vo_fn, obj_val, [], interp~)
        if !is_js_object(result) {
          return result
        }
      }
    None => ()
  }
  match lookup_ordinary_method(obj_val, data, "toString", interp~) {
    Some(ts_fn) =>
      if is_callable(ts_fn) {
        let result = call_callable_direct(ts_fn, obj_val, [], interp~)
        if is_js_object(result) {
          raise @errors.TypeError(
            message="Cannot convert object to primitive value",
          )
        }
        return result
      }
    None => ()
  }
  raise @errors.TypeError(message="Cannot convert object to primitive value")
}

///|
fn try_proxy_ordinary_method(
  interp : Interpreter,
  obj_val : Value,
  method_name : String,
) -> Value? raise Error {
  let method_val = interp.get_property(
    obj_val,
    method_name,
    @token.Loc::default(),
  )
  if !is_callable(method_val) {
    return None
  }
  let result = interp.call_value(method_val, obj_val, [], @token.Loc::default())
  if is_js_object(result) {
    None
  } else {
    Some(result)
  }
}

///|
/// Proxy-aware ToPrimitive shell. Observable property access stays behind the
/// interpreter's canonical [[Get]] dispatcher, while the decision order is a
/// deterministic translation of §7.1.1 / OrdinaryToPrimitive.
fn to_primitive_via_dispatch(
  obj_val : Value,
  hint : String,
  interp? : Interpreter? = None,
) -> Value raise Error {
  let effective = match resolve_effective_interp(interp) {
    Some(ip) => ip
    None =>
      return String_(
        match obj_val {
          Proxy(data) if data.is_callable =>
            "function proxy() { [native code] }"
          _ => "[object Object]"
        },
      )
  }
  let to_prim_sym = effective.realm_state.well_known_symbols.to_primitive
  let exotic = effective.get_computed_property(
    obj_val,
    Symbol(to_prim_sym),
    @token.Loc::default(),
  )
  match exotic {
    Undefined | Null => ()
    _ => {
      if !is_callable(exotic) {
        raise @errors.TypeError(message="Symbol.toPrimitive is not a function")
      }
      let result = effective.call_value(
        exotic,
        obj_val,
        [String_(hint)],
        @token.Loc::default(),
      )
      if is_js_object(result) {
        raise @errors.TypeError(
          message="Cannot convert object to primitive value",
        )
      }
      return result
    }
  }
  let first = if hint == "string" { "toString" } else { "valueOf" }
  let second = if hint == "string" { "valueOf" } else { "toString" }
  match try_proxy_ordinary_method(effective, obj_val, first) {
    Some(result) => return result
    None => ()
  }
  match try_proxy_ordinary_method(effective, obj_val, second) {
    Some(result) => result
    None =>
      raise @errors.TypeError(
        message="Cannot convert object to primitive value",
      )
  }
}

///|
/// Canonical ToPrimitive entry point for callers that hold an arbitrary
/// ECMAScript value. Primitive inputs pass through; object families that need
/// observable property access use the interpreter dispatchers.
pub fn Interpreter::to_primitive_value(
  self : Interpreter,
  value : Value,
  hint : String,
) -> Value raise Error {
  match value {
    Object(data) =>
      if hint == "string" {
        to_primitive_string(value, interp=Some(self))
      } else if hint == "number" {
        to_primitive_number(value, data, interp=Some(self))
      } else {
        to_primitive_default(value, data, interp=Some(self))
      }
    Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) =>
      to_primitive_via_dispatch(value, hint, interp=Some(self))
    _ => value
  }
}

///|
/// ToPrimitive(input, hint "default") - for + operator.
/// Same as "number" except passes "default" to @@toPrimitive.
pub fn to_primitive_default(
  obj_val : Value,
  data : ObjectData,
  interp? : Interpreter? = None,
) -> Value raise Error {
  match call_symbol_to_primitive(obj_val, "default", interp~) {
    Some(result) => return result
    None => ()
  }
  // "default" falls through to the same OrdinaryToPrimitive path as "number".
  ordinary_to_primitive_number(obj_val, data, interp~)
}

///|
/// ToPrimitive(input, hint "default") for Array values.
/// Arrays are objects per §7.2.14 steps 10-11 and must be coerced via
/// ToPrimitive before comparison with a primitive operand.
/// Uses the interpreter's symbol/string property lookup so own valueOf and
/// @@toPrimitive hooks are honoured; falls back to to_js_string (join) for
/// plain arrays that have neither hook.
fn to_primitive_default_array(
  arr_val : Value,
  interp? : Interpreter? = None,
) -> Value raise Error {
  let effective = resolve_effective_interp(interp)
  match effective {
    Some(ip) => {
      // Step 1: Check @@toPrimitive via interpreter symbol-keyed lookup.
      let to_prim_sym = ip.realm_state.well_known_symbols.to_primitive
      let exotic_fn = ip.get_computed_property(
        arr_val,
        Symbol(to_prim_sym),
        @token.Loc::default(),
      )
      match exotic_fn {
        Undefined | Null => ()
        _ => {
          if !is_callable(exotic_fn) {
            raise @errors.TypeError(
              message="Symbol.toPrimitive is not a function",
            )
          }
          let result = call_callable_direct(
            exotic_fn,
            arr_val,
            [String_("default")],
            interp=effective,
          )
          if is_js_object(result) {
            raise @errors.TypeError(
              message="Cannot convert object to primitive value",
            )
          }
          return result
        }
      }
      // Step 2: Try valueOf via interpreter (handles own + prototype chain).
      let vo_fn = ip.get_property(arr_val, "valueOf", @token.Loc::default())
      if is_callable(vo_fn) {
        let result = call_callable_direct(vo_fn, arr_val, [], interp=effective)
        if !is_js_object(result) {
          return result
        }
      }
      // Step 3: Try toString (may be user-overridden on the array or its prototype).
      let ts_fn = ip.get_property(arr_val, "toString", @token.Loc::default())
      if is_callable(ts_fn) {
        let result = call_callable_direct(ts_fn, arr_val, [], interp=effective)
        if is_js_object(result) {
          raise @errors.TypeError(
            message="Cannot convert object to primitive value",
          )
        }
        return result
      }
      // Both methods exhausted without a primitive result (§7.1.1.1 step 3).
      raise @errors.TypeError(
        message="Cannot convert object to primitive value",
      )
    }
    None => ()
  }
  // interp=None fallback: join elements directly (no context to dispatch JS methods).
  String_(to_js_string(arr_val, interp~))
}

///|
pub fn is_es_whitespace_cp(cp : Int) -> Bool {
  cp == 0x09 ||
  cp == 0x0A ||
  cp == 0x0B ||
  cp == 0x0C ||
  cp == 0x0D ||
  cp == 0x20 || // Space
  cp == 0x00A0 || // No-Break Space
  cp == 0xFEFF || // Zero Width No-Break Space (BOM)
  cp == 0x1680 || // OGHAM SPACE MARK
  (cp >= 0x2000 && cp <= 0x200A) || // EN QUAD through HAIR SPACE
  cp == 0x2028 || // Line Separator
  cp == 0x2029 || // Paragraph Separator
  cp == 0x202F || // NARROW NO-BREAK SPACE
  cp == 0x205F || // MEDIUM MATHEMATICAL SPACE
  cp == 0x3000 // IDEOGRAPHIC SPACE
}

///|
fn is_js_whitespace_code(ch : UInt16) -> Bool {
  is_es_whitespace_cp(ch.to_int())
}

///|
fn js_trim_whitespace(s : String) -> String {
  let len = s.length()
  let mut start = 0
  let mut end = len
  while start < end && is_js_whitespace_code(s[start]) {
    start = start + 1
  }
  while end > start && is_js_whitespace_code(s[end - 1]) {
    end = end - 1
  }
  if start == 0 && end == len {
    s
  } else {
    s[start:end].to_owned()
  }
}

///|
fn parse_hex_string(s : String) -> Double {
  guard s.length() > 0 else { return 0.0 / 0.0 }
  let mut result : Double = 0.0
  for ch in s {
    let digit = if ch >= '0' && ch <= '9' {
      ch.to_int() - '0'.to_int()
    } else if ch >= 'a' && ch <= 'f' {
      ch.to_int() - 'a'.to_int() + 10
    } else if ch >= 'A' && ch <= 'F' {
      ch.to_int() - 'A'.to_int() + 10
    } else {
      return 0.0 / 0.0
    }
    result = result * 16.0 + digit.to_double()
  }
  result
}

///|
fn parse_binary_string(s : String) -> Double {
  guard s.length() > 0 else { return 0.0 / 0.0 }
  let mut result : Double = 0.0
  for ch in s {
    if ch == '0' {
      result = result * 2.0
    } else if ch == '1' {
      result = result * 2.0 + 1.0
    } else {
      return 0.0 / 0.0
    }
  }
  result
}

///|
fn parse_octal_string(s : String) -> Double {
  guard s.length() > 0 else { return 0.0 / 0.0 }
  let mut result : Double = 0.0
  for ch in s {
    if ch >= '0' && ch <= '7' {
      result = result * 8.0 + (ch.to_int() - '0'.to_int()).to_double()
    } else {
      return 0.0 / 0.0
    }
  }
  result
}

///|
pub fn to_number(
  val : Value,
  interp? : Interpreter? = None,
) -> Double raise Error {
  match val {
    Number(n) => n
    String_(s) => {
      let trimmed = js_trim_whitespace(s)
      if trimmed.length() == 0 {
        return 0.0
      }
      if trimmed.length() > 2 {
        let prefix = trimmed.view()[:2].to_owned()
        if prefix == "0x" || prefix == "0X" {
          return parse_hex_string(trimmed.view()[2:].to_owned())
        }
        if prefix == "0b" || prefix == "0B" {
          return parse_binary_string(trimmed.view()[2:].to_owned())
        }
        if prefix == "0o" || prefix == "0O" {
          return parse_octal_string(trimmed.view()[2:].to_owned())
        }
      }
      if trimmed == "Infinity" || trimmed == "+Infinity" {
        return 1.0 / 0.0
      }
      if trimmed == "-Infinity" {
        return -1.0 / 0.0
      }
      // Per spec: numeric separators (_) are only valid in source code literals,
      // not in Number() string-to-number conversion
      if trimmed.contains("_") {
        return 0.0 / 0.0
      }
      // parse_double accepts non-spec strings like "INFINITY" and "inf"; validate first
      let valid = trimmed
        .iter()
        .fold(init=true, fn(acc, ch) {
          acc &&
          (
            (ch >= '0' && ch <= '9') ||
            ch == '.' ||
            ch == 'e' ||
            ch == 'E' ||
            ch == '+' ||
            ch == '-'
          )
        })
      if !valid {
        return 0.0 / 0.0
      }
      @string.parse_double(trimmed) catch {
        _ => 0.0 / 0.0
      }
    }
    Bool(b) => if b { 1.0 } else { 0.0 }
    Null => 0.0
    Object(data) => {
      let prim = to_primitive_number(Object(data), data, interp~)
      to_number(prim, interp~)
    }
    Array(arr_data) => {
      let s = arr_data.elements
        .map(fn(v) {
          match v {
            Undefined | Null => ""
            _ => v.to_string()
          }
        })
        .join(",")
      if s.length() == 0 {
        0.0
      } else {
        @string.parse_double(s) catch {
          _ => 0.0 / 0.0
        }
      }
    }
    Proxy(_) =>
      to_number(to_primitive_via_dispatch(val, "number", interp~), interp~)
    Undefined | Map(_) | Set(_) | Promise(_) => 0.0 / 0.0
    Symbol(_) =>
      raise @errors.TypeError(
        message="Cannot convert a Symbol value to a number",
      )
  }
}

///|
/// ToPrimitive(input, hint "string") - converts an object to a primitive value.
/// Follows the ECMAScript spec: check @@toPrimitive, then toString, then valueOf.
fn to_primitive_string(
  obj_val : Value,
  interp? : Interpreter? = None,
) -> Value raise Error {
  match call_symbol_to_primitive(obj_val, "string", interp~) {
    Some(result) => return result
    None => ()
  }
  // Without an interpreter there is no execution context in which to run
  // user code. Preserve the legacy built-in ordinary conversions for exotic
  // values; runtime expression paths always provide an interpreter and take
  // the full Get/Call route below.
  if interp is None {
    match obj_val {
      Array(data) =>
        return String_(
          data.elements
          .map(v => {
            match v {
              Undefined | Null => ""
              _ => v.to_string()
            }
          })
          .join(","),
        )
      Map(_) | Set(_) | Promise(_) => return String_("[object Object]")
      Proxy(data) =>
        return String_(
          if data.is_callable {
            "function proxy() { [native code] }"
          } else {
            "[object Object]"
          },
        )
      _ => ()
    }
  }
  // Step 2: OrdinaryToPrimitive with hint "string" - try toString first, then valueOf
  let data = match obj_val {
    Object(data) => Some(data)
    _ => None
  }
  let lookup_method = fn(method_name : String) -> Value? raise Error {
    match interp {
      Some(ip) =>
        Some(
          ip.get_property_key_with_receiver(
            obj_val,
            String_(method_name),
            obj_val,
            @token.Loc::default(),
          ),
        )
      None =>
        match data {
          Some(object_data) =>
            lookup_ordinary_method(obj_val, object_data, method_name, interp~)
          None => None
        }
    }
  }
  match lookup_method("toString") {
    Some(ts_fn) =>
      if is_callable(ts_fn) {
        let result = call_callable_direct(ts_fn, obj_val, [], interp~)
        if !is_js_object(result) {
          return result
        }
      }
    None => ()
  }
  match lookup_method("valueOf") {
    Some(vo_fn) =>
      if is_callable(vo_fn) {
        let result = call_callable_direct(vo_fn, obj_val, [], interp~)
        if is_js_object(result) {
          raise @errors.TypeError(
            message="Cannot convert object to primitive value",
          )
        }
        return result
      }
    None => ()
  }
  raise @errors.TypeError(message="Cannot convert object to primitive value")
}

///|
/// ECMAScript ToPropertyKey §7.1.19 — canonicalize a Value into a property
/// key. Symbols pass through; anything else is ToPrimitive(hint:"string") +
/// ToString via `to_js_string`, which invokes user-land Symbol.toPrimitive
/// / toString hooks (so the caller observes side-effects in spec order).
pub fn to_property_key(
  val : Value,
  interp? : Interpreter? = None,
) -> Value raise Error {
  match val {
    Symbol(_) => val
    Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) => {
      let primitive = to_primitive_string(val, interp~)
      match primitive {
        Symbol(_) => primitive
        _ => Value::String_(to_js_string(primitive, interp~))
      }
    }
    _ => Value::String_(to_js_string(val, interp~))
  }
}

///|
/// ECMAScript ToString - converts a value to a string following the spec.
/// For objects, calls ToPrimitive(hint: "string") then converts result to string.
pub fn to_js_string(
  val : Value,
  interp? : Interpreter? = None,
) -> String raise Error {
  match val {
    String_(s) => s
    Number(n) => {
      let i = n.to_int()
      if i.to_double() == n && !n.is_inf() && !n.is_nan() {
        i.to_string()
      } else {
        n.to_string()
      }
    }
    Bool(b) => b.to_string()
    Null => "null"
    Undefined => "undefined"
    Symbol(_) =>
      raise @errors.TypeError(
        message="Cannot convert a Symbol value to a string",
      )
    Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) => {
      let prim = to_primitive_string(val, interp~)
      to_js_string(prim, interp~)
    }
  }
}

///|
/// Interpreter method: ToNumber with explicit interpreter context.
pub fn Interpreter::to_number(
  self : Interpreter,
  val : Value,
) -> Double raise Error {
  to_number(val, interp=Some(self))
}

///|
/// Interpreter method: ToString with explicit interpreter context.
pub fn Interpreter::to_js_string(
  self : Interpreter,
  val : Value,
) -> String raise Error {
  to_js_string(val, interp=Some(self))
}

///|
/// Interpreter method: HasProperty with explicit interpreter context.
pub fn Interpreter::has_property(
  self : Interpreter,
  val : Value,
  name : String,
) -> Bool raise Error {
  self.has_property_key(val, String_(name))
}

///|
fn map_target_prototype(realm_state : RealmState, data : MapData) -> Value {
  data.prototype.unwrap_or_else(fn() { realm_state.get_map_proto() })
}

///|
fn set_target_prototype(realm_state : RealmState, data : SetData) -> Value {
  data.prototype.unwrap_or_else(fn() { realm_state.get_set_proto() })
}

///|
fn promise_target_prototype(
  realm_state : RealmState,
  data : PromiseData,
) -> Value {
  data.prototype.unwrap_or_else(fn() { realm_state.get_promise_proto() })
}

///|
/// ES §7.3.11 `HasProperty` with a pre-computed property key.
pub fn Interpreter::has_property_key(
  self : Interpreter,
  val : Value,
  key : Value,
) -> Bool raise Error {
  match val {
    Proxy(proxy_data) => proxy_has_property_key(self, proxy_data, key)
    Object(data) => self.has_object_property(data, key)
    Array(data) => self.has_array_property(data, key)
    // Map/Set/Promise share a uniform shape: an expando bag plus a builtin
    // prototype. Implement OrdinaryHasProperty (ES §7.3.11): own bag →
    // proto chain → bool, with no value inspection. Resolve from the target
    // object's own prototype chain, not the active callee realm.
    Map(data) =>
      self.has_bag_with_proto(
        data.bag,
        map_target_prototype(self.realm_state, data),
        key,
      )
    Set(data) =>
      self.has_bag_with_proto(
        data.bag,
        set_target_prototype(self.realm_state, data),
        key,
      )
    Promise(data) =>
      self.has_bag_with_proto(
        data.bag,
        promise_target_prototype(self.realm_state, data),
        key,
      )
    _ => false
  }
}

///|
fn Interpreter::has_object_property(
  self : Interpreter,
  data : ObjectData,
  key : Value,
) -> Bool raise Error {
  if is_typedarray_class(data.class_name) {
    match key {
      String_(s) =>
        match classify_typedarray_string_key(s) {
          Some(-1) => return false
          Some(idx) =>
            return (self.stdlib_hooks.typedarray_is_valid_index)(
              data,
              idx,
              self.realm_state,
            )
          None => ()
        }
      _ => ()
    }
  }
  match key {
    Symbol(sym) =>
      if data.bag.symbol_properties.contains(sym.id) ||
        data.bag.symbol_descriptors.contains(sym.id) {
        return true
      }
    _ => {
      let name = to_js_string(key)
      if data.bag.properties.contains(name) ||
        data.bag.descriptors.contains(name) {
        return true
      }
    }
  }
  if self.has_property_on_proto(data.prototype, key) {
    return true
  }
  match data.callable {
    Some(_) => {
      let func_proto = self.global.get("[[FunctionPrototype]]") catch {
        _ => return false
      }
      let func_proto_data = match func_proto {
        Object(d) => d
        _ => return false
      }
      // Guard 1: we ARE Function.prototype — walking it again causes infinite recursion.
      // Guard 2: we already walked Function.prototype above (data.prototype == func_proto).
      if physical_equal(data, func_proto_data) ||
        physical_equal(data.prototype, func_proto) {
        return false
      }
      self.has_property_on_proto(func_proto, key)
    }
    None => false
  }
}

///|
fn Interpreter::has_array_property(
  self : Interpreter,
  data : ArrayData,
  key : Value,
) -> Bool raise Error {
  match key {
    Symbol(sym) =>
      if get_array_symbol_prop(data, sym.id) is Some(_) ||
        data.bag.symbol_descriptors.contains(sym.id) {
        return true
      }
    _ => {
      let name = to_js_string(key)
      if name == "length" {
        return true
      }
      let idx = @string.parse_int(name) catch { _ => -1 }
      if idx >= 0 && idx.to_string() == name {
        // Route through the central index classifier so accessor
        // descriptors and the Phase-3 hole / stale-marker semantics
        // stay consistent with `array_index_lookup_result` callers.
        match array_index_lookup_result(data, idx) {
          Present(_) | OwnAccessor(_) => return true
          Hole | OutOfRange => ()
        }
      }
      if data.bag.properties.contains(name) ||
        data.bag.descriptors.contains(name) {
        return true
      }
    }
  }
  self.has_property_on_proto(get_array_prototype(self.realm_state, data), key)
}

///|
fn Interpreter::has_property_on_proto(
  self : Interpreter,
  proto : Value,
  key : Value,
) -> Bool raise Error {
  match proto {
    Null | Undefined => false
    Proxy(proxy_data) => proxy_has_property_key(self, proxy_data, key)
    Object(proto_data) => self.has_object_property(proto_data, key)
    Array(arr_data) => self.has_array_property(arr_data, key)
    Map(data) =>
      self.has_bag_with_proto(
        data.bag,
        map_target_prototype(self.realm_state, data),
        key,
      )
    Set(data) =>
      self.has_bag_with_proto(
        data.bag,
        set_target_prototype(self.realm_state, data),
        key,
      )
    Promise(data) =>
      self.has_bag_with_proto(
        data.bag,
        promise_target_prototype(self.realm_state, data),
        key,
      )
    _ => false
  }
}

///|
fn bag_has_key(bag : PropertyBag, key : Value) -> Bool raise Error {
  match key {
    Symbol(sym) =>
      bag.symbol_properties.contains(sym.id) ||
      bag.symbol_descriptors.contains(sym.id)
    _ => {
      let name = to_js_string(key)
      bag.properties.contains(name) || bag.descriptors.contains(name)
    }
  }
}

///|
/// OrdinaryHasProperty for exotic values whose prototype is cached on
/// RealmState. Checks own-bag descriptors and properties first, then walks
/// the prototype chain from the caller-supplied builtin prototype value.
fn Interpreter::has_bag_with_proto(
  self : Interpreter,
  bag : PropertyBag,
  proto : Value,
  key : Value,
) -> Bool raise Error {
  if bag_has_key(bag, key) {
    return true
  }
  self.has_property_on_proto(proto, key)
}

///|
/// Interpreter method: ToPrimitive (default hint) with explicit interpreter context.
pub fn Interpreter::to_primitive_default(
  self : Interpreter,
  obj_val : Value,
  data : ObjectData,
) -> Value raise Error {
  to_primitive_default(obj_val, data, interp=Some(self))
}

///|
/// ECMAScript ToIndex: converts a value to a non-negative integer index.
/// Throws RangeError for negative values or values >= 2^53.
/// Returns 0 for undefined.
pub fn to_index(val : Value, interp? : Interpreter? = None) -> Int64 raise {
  match val {
    Undefined => 0L
    _ => {
      let n = to_number(val, interp~)
      let integer_index = if n.is_nan() || n == 0.0 {
        0.0
      } else if n.is_inf() {
        n
      } else {
        let sign = if n < 0.0 { -1.0 } else { 1.0 }
        n.abs().floor() * sign
      }
      if integer_index.is_inf() ||
        integer_index < 0.0 ||
        integer_index >= JS_MAX_SAFE_INTEGER_EXCLUSIVE_DOUBLE {
        raise @errors.RangeError(message="Invalid index")
      }
      integer_index.to_int64()
    }
  }
}

///|
/// Interpreter method: ToIndex with explicit interpreter context.
pub fn Interpreter::to_index(
  self : Interpreter,
  val : Value,
) -> Int64 raise Error {
  to_index(val, interp=Some(self))
}

///|
pub fn to_int32(n : Double) -> Int {
  if n.is_nan() || n.is_inf() || n == 0.0 {
    return 0
  }
  // ECMAScript ToInt32: truncate toward zero, modulo 2^32, map to signed range
  let two32 : Double = 4294967296.0
  // Truncate toward zero
  let abs_n = n.abs()
  let sign : Double = if n < 0.0 { -1.0 } else { 1.0 }
  let truncated = abs_n.floor() * sign
  // Modulo 2^32 (always positive)
  let remainder = truncated % two32
  let pos_mod = if remainder < 0.0 { remainder + two32 } else { remainder }
  // Map to signed 32-bit range
  if pos_mod >= 2147483648.0 {
    (pos_mod - two32).to_int()
  } else {
    pos_mod.to_int()
  }
}

///|
pub fn type_of(val : Value) -> String {
  match val {
    Number(_) => "number"
    String_(_) => "string"
    Bool(_) => "boolean"
    Null => "object" // JS quirk
    Undefined => "undefined"
    Object(data) =>
      match data.callable {
        Some(_) => "function"
        None => "object"
      }
    Array(_) => "object"
    Symbol(_) => "symbol"
    Map(_) => "object"
    Set(_) => "object"
    Promise(_) => "object"
    Proxy(proxy_data) =>
      if proxy_data.is_callable {
        "function"
      } else {
        "object"
      }
  }
}