///|
pub fn setup_builtins(env : Environment, output : Array[String]) -> Unit {
  let _ = output
  // Global constants
  env.def_builtin("NaN", Number(0.0 / 0.0))
  env.def_builtin("Infinity", Number(1.0 / 0.0))
  env.def_builtin("undefined", Undefined)

  // isNaN(value)
  env.def_builtin(
    "isNaN",
    make_native_func("isNaN", fn(args) {
      let val = if args.length() > 0 { args[0] } else { Undefined }
      let n = to_number(val)
      Bool(n.is_nan())
    }),
  )

  // isFinite(value)
  env.def_builtin(
    "isFinite",
    make_native_func("isFinite", fn(args) {
      let val = if args.length() > 0 { args[0] } else { Undefined }
      let n = to_number(val)
      Bool(not(n.is_nan()) && not(n.is_inf()))
    }),
  )

  // parseInt(string, radix?)
  env.def_builtin(
    "parseInt",
    make_native_func("parseInt", fn(args) {
      let str_val = if args.length() > 0 { args[0] } else { Undefined }
      let s = match str_val {
        String_(s) => s
        Number(n) => n.to_string()
        Bool(b) => b.to_string()
        _ => return Number(0.0 / 0.0)
      }
      let trimmed = trim_start(s)
      if trimmed.length() == 0 {
        return Number(0.0 / 0.0)
      }
      let chars = trimmed.to_array()
      let mut pos = 0
      let mut sign = 1.0
      if pos < chars.length() && chars[pos] == '+' {
        pos = pos + 1
      } else if pos < chars.length() && chars[pos] == '-' {
        sign = -1.0
        pos = pos + 1
      }
      let mut radix = if args.length() > 1 {
        let r = to_number(args[1]).to_int()
        if r < 2 || r > 36 {
          return Number(0.0 / 0.0)
        }
        r
      } else {
        10
      }
      // Detect 0x prefix for hex
      if radix == 16 || args.length() <= 1 || to_number(args[1]).to_int() == 0 {
        if pos + 1 < chars.length() &&
          chars[pos] == '0' &&
          (chars[pos + 1] == 'x' || chars[pos + 1] == 'X') {
          radix = 16
          pos = pos + 2
        }
      }
      let mut result = 0.0
      let mut found = false
      while pos < chars.length() {
        let c = chars[pos]
        let digit = char_to_digit(c, radix)
        if digit < 0 {
          break
        }
        result = result * radix.to_double() + digit.to_double()
        found = true
        pos = pos + 1
      }
      if not(found) {
        Number(0.0 / 0.0)
      } else {
        Number(sign * result)
      }
    }),
  )

  // parseFloat(string)
  env.def_builtin(
    "parseFloat",
    make_native_func("parseFloat", fn(args) {
      let str_val = if args.length() > 0 { args[0] } else { Undefined }
      let s = match str_val {
        String_(s) => s
        Number(n) => return Number(n)
        Symbol(_) =>
          raise @errors.TypeError(
            message="Cannot convert a Symbol value to a string",
          )
        Bool(_)
        | Null
        | Undefined
        | Object(_)
        | Array(_)
        | Map(_)
        | Set(_)
        | Promise(_) => return Number(0.0 / 0.0)
      }
      let trimmed = trim_start(s)
      if trimmed.length() == 0 {
        return Number(0.0 / 0.0)
      }
      let chars = trimmed.to_array()
      let len = chars.length()
      let mut pos = 0
      // Optional sign
      if pos < len && (chars[pos] == '+' || chars[pos] == '-') {
        pos = pos + 1
      }
      // Check for Infinity
      let sign_end = pos
      if pos + 8 <= len &&
        chars[pos] == 'I' &&
        chars[pos + 1] == 'n' &&
        chars[pos + 2] == 'f' &&
        chars[pos + 3] == 'i' &&
        chars[pos + 4] == 'n' &&
        chars[pos + 5] == 'i' &&
        chars[pos + 6] == 't' &&
        chars[pos + 7] == 'y' {
        let sign : Double = if sign_end > 0 && chars[0] == '-' {
          -1.0
        } else {
          1.0
        }
        return Number(sign * (1.0 / 0.0))
      }
      // Single-pass scan: [+-]? digits? [. digits?] ([eE] [+-]? digits)?
      let mut has_digits = false
      // Integer part digits
      while pos < len && chars[pos] >= '0' && chars[pos] <= '9' {
        has_digits = true
        pos = pos + 1
      }
      // Fractional part
      if pos < len && chars[pos] == '.' {
        pos = pos + 1
        while pos < len && chars[pos] >= '0' && chars[pos] <= '9' {
          has_digits = true
          pos = pos + 1
        }
      }
      if not(has_digits) {
        return Number(0.0 / 0.0)
      }
      // Exponent part
      if pos < len && (chars[pos] == 'e' || chars[pos] == 'E') {
        let saved = pos
        pos = pos + 1
        if pos < len && (chars[pos] == '+' || chars[pos] == '-') {
          pos = pos + 1
        }
        let exp_start = pos
        while pos < len && chars[pos] >= '0' && chars[pos] <= '9' {
          pos = pos + 1
        }
        // If no exponent digits, revert to before 'e'/'E'
        if pos == exp_start {
          pos = saved
        }
      }
      // Build the prefix string and parse once
      let buf = StringBuilder::new()
      for i = 0; i < pos; i = i + 1 {
        buf.write_char(chars[i])
      }
      let result = @strconv.parse_double(buf.to_string()) catch {
        _ => 0.0 / 0.0
      }
      Number(result)
    }),
  )

  // encodeURIComponent(string)
  env.def_builtin(
    "encodeURIComponent",
    make_native_func("encodeURIComponent", fn(args) {
      let str = if args.length() > 0 {
        args[0].to_string()
      } else {
        "undefined"
      }
      String_(uri_encode(str, true))
    }),
  )

  // decodeURIComponent(string) - decodes everything
  env.def_builtin(
    "decodeURIComponent",
    make_native_func("decodeURIComponent", fn(args) {
      let str = if args.length() > 0 {
        args[0].to_string()
      } else {
        "undefined"
      }
      String_(uri_decode(str, false))
    }),
  )

  // encodeURI(string)
  env.def_builtin(
    "encodeURI",
    make_native_func("encodeURI", fn(args) {
      let str = if args.length() > 0 {
        args[0].to_string()
      } else {
        "undefined"
      }
      String_(uri_encode(str, false))
    }),
  )

  // decodeURI(string) - preserves reserved characters (; , / ? : @ & = + $ #)
  env.def_builtin(
    "decodeURI",
    make_native_func("decodeURI", fn(args) {
      let str = if args.length() > 0 {
        args[0].to_string()
      } else {
        "undefined"
      }
      String_(uri_decode(str, true))
    }),
  )

  // String constructor with static methods
  let string_proto : Value = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: None,
    class_name: "String",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  env.def_builtin(
    "String",
    Object({
      properties: {
        "prototype": string_proto,
        "fromCharCode": make_native_func("fromCharCode", fn(args) {
          let buf = StringBuilder::new()
          for arg in args {
            let code = to_number(arg).to_int()
            // fromCharCode uses UTF-16 code units, modulo 2^16
            let code_unit = code & 0xFFFF
            buf.write_char(code_unit.unsafe_to_char())
          }
          String_(buf.to_string())
        }),
        "fromCodePoint": make_native_func("fromCodePoint", fn(args) {
          // String.fromCodePoint() - accepts Unicode code points (not code units)
          let buf = StringBuilder::new()
          for arg in args {
            let num = to_number(arg)
            // Check if it's a valid number
            if num.is_nan() || num.is_inf() {
              raise @errors.RangeError(message="Invalid code point")
            }
            // Check if it's an integer (no fractional part)
            let code = num.to_int()
            if code.to_double() != num {
              raise @errors.RangeError(message="Invalid code point")
            }
            // Valid Unicode code points: 0 to 0x10FFFF
            if code < 0 || code > 0x10FFFF {
              raise @errors.RangeError(
                message="Invalid code point \{code.to_string()}",
              )
            }
            // Convert code point to UTF-16
            if code <= 0xFFFF {
              // BMP (Basic Multilingual Plane) - single code unit
              buf.write_char(code.unsafe_to_char())
            } else {
              // Supplementary planes - surrogate pair
              let code_adj = code - 0x10000
              let high = 0xD800 + (code_adj >> 10)
              let low = 0xDC00 + (code_adj & 0x3FF)
              buf.write_char(high.unsafe_to_char())
              buf.write_char(low.unsafe_to_char())
            }
          }
          String_(buf.to_string())
        }),
        "raw": make_native_func("raw", fn(args) {
          // String.raw(template, ...substitutions)
          // Returns raw string content without processing escape sequences
          if args.length() == 0 {
            return String_("")
          }
          let template = args[0]
          match template {
            Object(data) =>
              match data.properties.get("raw") {
                Some(Array(raw_data)) => {
                  let buf = StringBuilder::new()
                  let raw_strings = raw_data.elements
                  let sub_count = args.length() - 1
                  for i = 0; i < raw_strings.length(); i = i + 1 {
                    // Add raw string segment
                    buf.write_string(raw_strings[i].to_string())
                    // Add substitution if available
                    if i < sub_count {
                      buf.write_string(args[i + 1].to_string())
                    }
                  }
                  String_(buf.to_string())
                }
                Some(Object(raw_obj)) => {
                  // Handle object with numeric indices - use length property per spec
                  let buf = StringBuilder::new()
                  let sub_count = args.length() - 1

                  // Get length property (treat missing/invalid as 0)
                  let length = match raw_obj.properties.get("length") {
                    Some(Number(n)) => n.to_int().max(0)
                    _ => 0
                  }

                  // Iterate from 0 to length-1, treating missing indices as Undefined
                  for i = 0; i < length; i = i + 1 {
                    let str_val = match raw_obj.properties.get(i.to_string()) {
                      Some(v) => v
                      None => Undefined
                    }
                    buf.write_string(str_val.to_string())
                    // Add substitution if available
                    if i < sub_count {
                      buf.write_string(args[i + 1].to_string())
                    }
                  }
                  String_(buf.to_string())
                }
                _ =>
                  raise @errors.TypeError(
                    message="Cannot convert to string template",
                  )
              }
            _ =>
              raise @errors.TypeError(
                message="Cannot convert to string template",
              )
          }
        }),
      },
      symbol_properties: {},
      prototype: Null,
      callable: Some(
        NativeCallable("String", fn(args) {
          let val = if args.length() > 0 { args[0] } else { Undefined }
          String_(val.to_string())
        }),
      ),
      class_name: "Function",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    }),
  )

  // Populate String.prototype with methods for .call() support
  setup_string_prototype(string_proto)
  // Set String.prototype.constructor = String
  match string_proto {
    Object(data) => {
      let ctor = try { env.get("String") } catch { _ => Undefined }
      data.properties["constructor"] = ctor
      data.descriptors["constructor"] = { writable: true, enumerable: false, configurable: true }
    }
    _ => ()
  }

  // Number constructor with static methods and prototype
  setup_number_builtins(env)

  // Boolean constructor with prototype
  let boolean_proto_props : Map[String, Value] = {}
  boolean_proto_props["toString"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("toString", fn(this_val, _args) {
        match this_val {
          Bool(b) => String_(b.to_string())
          _ =>
            raise @errors.TypeError(
              message="Boolean.prototype.toString requires a Boolean",
            )
        }
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  boolean_proto_props["valueOf"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("valueOf", fn(this_val, _args) {
        match this_val {
          Bool(b) => Bool(b)
          _ =>
            raise @errors.TypeError(
              message="Boolean.prototype.valueOf requires a Boolean",
            )
        }
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  let boolean_proto : Value = Object({
    properties: boolean_proto_props,
    symbol_properties: {},
    prototype: Null,
    callable: None,
    class_name: "Boolean",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  env.def_builtin(
    "Boolean",
    Object({
      properties: { "prototype": boolean_proto },
      symbol_properties: {},
      prototype: Null,
      callable: Some(
        NativeCallable("Boolean", fn(args) {
          let val = if args.length() > 0 { args[0] } else { Undefined }
          Bool(is_truthy(val))
        }),
      ),
      class_name: "Function",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    }),
  )

  // Set Boolean.prototype.constructor = Boolean
  match boolean_proto {
    Object(data) => {
      let ctor = try { env.get("Boolean") } catch { _ => Undefined }
      data.properties["constructor"] = ctor
      data.descriptors["constructor"] = { writable: true, enumerable: false, configurable: true }
    }
    _ => ()
  }

  // Function constructor with prototype
  setup_function_builtins(env)

  // Error constructors
  let error_proto = register_error_ctor(env, "Error", Null)
  register_error_ctor(env, "TypeError", error_proto) |> ignore
  register_error_ctor(env, "ReferenceError", error_proto) |> ignore
  register_error_ctor(env, "SyntaxError", error_proto) |> ignore
  register_error_ctor(env, "RangeError", error_proto) |> ignore
  register_error_ctor(env, "URIError", error_proto) |> ignore
  register_error_ctor(env, "EvalError", error_proto) |> ignore

  // Object, Array constructors and built-in methods
  setup_object_builtins(env)

  // Math object
  setup_math_builtins(env)

  // JSON object
  setup_json_builtins(env)

  // RegExp constructor
  env.def_builtin(
    "RegExp",
    Object({
      properties: {},
      symbol_properties: {},
      prototype: Null,
      callable: Some(
        NativeCallable("RegExp", fn(args) {
          let pattern = if args.length() > 0 {
            match args[0] {
              Object(data) =>
                if data.class_name == "RegExp" {
                  match data.properties.get("source") {
                    Some(String_(s)) => s
                    _ => args[0].to_string()
                  }
                } else {
                  args[0].to_string()
                }
              Undefined => ""
              _ => args[0].to_string()
            }
          } else {
            ""
          }
          let flags = if args.length() > 1 {
            match args[1] {
              Undefined => ""
              _ => args[1].to_string()
            }
          } else {
            ""
          }
          make_regexp_object(pattern, flags)
        }),
      ),
      class_name: "Function",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    }),
  )

  // Symbol constructor and well-known symbols
  setup_symbol_builtins(env)

  // Set up iterator protocol on Array and String prototypes
  // Use try/catch since setup_iterator_protocol can raise errors
  setup_iterator_protocol(env) catch {
    _ => () // Ignore errors during initialization
  }

  // Set up Map and Set collections
  setup_map_set_builtins(env)

  // Set up Promise constructor and prototype
  setup_promise(env)

  // Set up timer APIs (setTimeout, clearTimeout, setInterval, clearInterval)
  setup_timers(env)
}

///|
fn setup_symbol_builtins(env : Environment) -> Unit {
  // Create well-known symbols
  let iterator_sym = new_symbol(Some("Symbol.iterator"))
  // Store globally for iterator protocol support
  well_known_iterator_sym.val = Some(iterator_sym)
  let to_primitive_sym = new_symbol(Some("Symbol.toPrimitive"))
  let to_string_tag_sym = new_symbol(Some("Symbol.toStringTag"))
  let has_instance_sym = new_symbol(Some("Symbol.hasInstance"))
  // Store globally for instanceof operator support
  well_known_hasinstance_sym.val = Some(has_instance_sym)
  let is_concat_spreadable_sym = new_symbol(Some("Symbol.isConcatSpreadable"))
  let species_sym = new_symbol(Some("Symbol.species"))
  let match_sym = new_symbol(Some("Symbol.match"))
  let replace_sym = new_symbol(Some("Symbol.replace"))
  let search_sym = new_symbol(Some("Symbol.search"))
  let split_sym = new_symbol(Some("Symbol.split"))
  let unscopables_sym = new_symbol(Some("Symbol.unscopables"))
  let async_iterator_sym = new_symbol(Some("Symbol.asyncIterator"))

  // Symbol prototype
  let symbol_proto_props : Map[String, Value] = {}
  symbol_proto_props["toString"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("toString", fn(this_val, _args) {
        match this_val {
          Symbol(sym) =>
            match sym.description {
              Some(desc) => String_("Symbol(\{desc})")
              None => String_("Symbol()")
            }
          _ =>
            raise @errors.TypeError(
              message="Symbol.prototype.toString requires a Symbol",
            )
        }
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  symbol_proto_props["valueOf"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("valueOf", fn(this_val, _args) {
        match this_val {
          Symbol(_) => this_val
          _ =>
            raise @errors.TypeError(
              message="Symbol.prototype.valueOf requires a Symbol",
            )
        }
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  symbol_proto_props["description"] = Undefined // Getter, simplified
  let symbol_proto : Value = Object({
    properties: symbol_proto_props,
    symbol_properties: {},
    prototype: Null,
    callable: None,
    class_name: "Symbol",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })

  // Symbol constructor properties
  let symbol_props : Map[String, Value] = {}
  symbol_props["prototype"] = symbol_proto

  // Well-known symbols as static properties
  symbol_props["iterator"] = Symbol(iterator_sym)
  symbol_props["toPrimitive"] = Symbol(to_primitive_sym)
  symbol_props["toStringTag"] = Symbol(to_string_tag_sym)
  symbol_props["hasInstance"] = Symbol(has_instance_sym)
  symbol_props["isConcatSpreadable"] = Symbol(is_concat_spreadable_sym)
  symbol_props["species"] = Symbol(species_sym)
  symbol_props["match"] = Symbol(match_sym)
  symbol_props["replace"] = Symbol(replace_sym)
  symbol_props["search"] = Symbol(search_sym)
  symbol_props["split"] = Symbol(split_sym)
  symbol_props["unscopables"] = Symbol(unscopables_sym)
  symbol_props["asyncIterator"] = Symbol(async_iterator_sym)

  // Symbol.for(key) - returns symbol from global registry
  symbol_props["for"] = make_native_func("for", fn(args) {
    let key = if args.length() > 0 { args[0].to_string() } else { "undefined" }
    match global_symbol_registry.get(key) {
      Some(sym) => Symbol(sym)
      None => {
        let sym = new_symbol(Some(key))
        global_symbol_registry[key] = sym
        symbol_registry_reverse[sym.id] = key // O(1) reverse lookup
        Symbol(sym)
      }
    }
  })

  // Symbol.keyFor(sym) - returns key for symbol in global registry (O(1) via reverse lookup)
  symbol_props["keyFor"] = make_native_func("keyFor", fn(args) {
    let sym = if args.length() > 0 { args[0] } else { Undefined }
    match sym {
      Symbol(sym_data) =>
        match symbol_registry_reverse.get(sym_data.id) {
          Some(key) => String_(key)
          None => Undefined
        }
      _ => raise @errors.TypeError(message="Symbol.keyFor requires a symbol")
    }
  })

  // Symbol constructor - cannot be called with new, only as function
  env.def_builtin(
    "Symbol",
    Object({
      properties: symbol_props,
      symbol_properties: {},
      prototype: Null,
      callable: Some(
        NonConstructableCallable("Symbol", fn(args) {
          let description : String? = if args.length() > 0 {
            match args[0] {
              Undefined => None
              _ => Some(args[0].to_string())
            }
          } else {
            None
          }
          Symbol(new_symbol(description))
        }),
      ),
      class_name: "Function",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    }),
  )
  // Set Symbol.prototype.constructor = Symbol
  match symbol_proto {
    Object(data) => {
      let ctor = try { env.get("Symbol") } catch { _ => Undefined }
      data.properties["constructor"] = ctor
      data.descriptors["constructor"] = { writable: true, enumerable: false, configurable: true }
    }
    _ => ()
  }
}

///|
/// Set up iterator protocol on Array and String prototypes
fn setup_iterator_protocol(env : Environment) -> Unit raise Error {
  let iterator_sym = get_iterator_symbol()

  // Helper to create an array iterator object
  // Array iterator returns { value, done } for each element
  fn make_array_iterator(arr : ArrayData) -> Value {
    // Create mutable index state using a Ref
    let index_ref : Ref[Int] = { val: 0 }

    // Create the iterator next method
    let next_method = Object({
      properties: {},
      symbol_properties: {},
      prototype: Null,
      callable: Some(
        NativeCallable("next", fn(_args) {
          let idx = index_ref.val
          if idx < arr.elements.length() {
            let value = arr.elements[idx]
            index_ref.val = idx + 1
            // Return { value: element, done: false }
            let result_props : Map[String, Value] = {}
            result_props["value"] = value
            result_props["done"] = Bool(false)
            Object({
              properties: result_props,
              symbol_properties: {},
              prototype: Null,
              callable: None,
              class_name: "Object",
              descriptors: {},
              symbol_descriptors: {},
              extensible: true,
            })
          } else {
            // Return { value: undefined, done: true }
            let result_props : Map[String, Value] = {}
            result_props["value"] = Undefined
            result_props["done"] = Bool(true)
            Object({
              properties: result_props,
              symbol_properties: {},
              prototype: Null,
              callable: None,
              class_name: "Object",
              descriptors: {},
              symbol_descriptors: {},
              extensible: true,
            })
          }
        }),
      ),
      class_name: "Function",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    })

    // Create the iterator object
    let iter_props : Map[String, Value] = {}
    iter_props["next"] = next_method

    // Iterator should also have Symbol.iterator returning itself
    let iter_symbol_props : Map[Int, Value] = {}
    let iter_obj = Object({
      properties: iter_props,
      symbol_properties: iter_symbol_props,
      prototype: Null,
      callable: None,
      class_name: "Array Iterator",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    })

    // Add [Symbol.iterator] that returns the iterator itself
    match iter_obj {
      Object(data) =>
        data.symbol_properties[iterator_sym.id] = Object({
          properties: {},
          symbol_properties: {},
          prototype: Null,
          callable: Some(
            MethodCallable("[Symbol.iterator]", fn(this_val, _args) { this_val }),
          ),
          class_name: "Function",
          descriptors: {},
          symbol_descriptors: {},
          extensible: true,
        })
      _ => ()
    }
    iter_obj
  }

  // Get Array constructor and add Symbol.iterator to its prototype
  let array_val = env.get("Array")
  match array_val {
    Object(array_ctor) =>
      match array_ctor.properties.get("prototype") {
        Some(Object(proto_data)) => {
          // Add [Symbol.iterator] method that returns an array iterator
          proto_data.symbol_properties[iterator_sym.id] = Object({
            properties: {},
            symbol_properties: {},
            prototype: Null,
            callable: Some(
              MethodCallable("[Symbol.iterator]", fn(this_val, _args) {
                match this_val {
                  Array(arr) => make_array_iterator(arr)
                  _ =>
                    raise @errors.TypeError(
                      message="Array.prototype[Symbol.iterator] called on non-array",
                    )
                }
              }),
            ),
            class_name: "Function",
            descriptors: {},
            symbol_descriptors: {},
            extensible: true,
          })

          // Also add values() method (alias for [Symbol.iterator])
          proto_data.properties["values"] = Object({
            properties: {},
            symbol_properties: {},
            prototype: Null,
            callable: Some(
              MethodCallable("values", fn(this_val, _args) {
                match this_val {
                  Array(arr) => make_array_iterator(arr)
                  _ =>
                    raise @errors.TypeError(
                      message="Array.prototype.values called on non-array",
                    )
                }
              }),
            ),
            class_name: "Function",
            descriptors: {},
            symbol_descriptors: {},
            extensible: true,
          })

          // Add keys() method that returns an iterator over indices
          proto_data.properties["keys"] = Object({
            properties: {},
            symbol_properties: {},
            prototype: Null,
            callable: Some(
              MethodCallable("keys", fn(this_val, _args) {
                match this_val {
                  Array(arr) => {
                    let index_ref : Ref[Int] = { val: 0 }
                    let len = arr.elements.length()
                    let next_method = Object({
                      properties: {},
                      symbol_properties: {},
                      prototype: Null,
                      callable: Some(
                        NativeCallable("next", fn(_args) {
                          let idx = index_ref.val
                          if idx < len {
                            index_ref.val = idx + 1
                            let result_props : Map[String, Value] = {}
                            result_props["value"] = Number(idx.to_double())
                            result_props["done"] = Bool(false)
                            Object({
                              properties: result_props,
                              symbol_properties: {},
                              prototype: Null,
                              callable: None,
                              class_name: "Object",
                              descriptors: {},
                              symbol_descriptors: {},
                              extensible: true,
                            })
                          } else {
                            let result_props : Map[String, Value] = {}
                            result_props["value"] = Undefined
                            result_props["done"] = Bool(true)
                            Object({
                              properties: result_props,
                              symbol_properties: {},
                              prototype: Null,
                              callable: None,
                              class_name: "Object",
                              descriptors: {},
                              symbol_descriptors: {},
                              extensible: true,
                            })
                          }
                        }),
                      ),
                      class_name: "Function",
                      descriptors: {},
                      symbol_descriptors: {},
                      extensible: true,
                    })
                    let iter_props : Map[String, Value] = {}
                    iter_props["next"] = next_method
                    let iter_obj = Object({
                      properties: iter_props,
                      symbol_properties: {},
                      prototype: Null,
                      callable: None,
                      class_name: "Array Iterator",
                      descriptors: {},
                      symbol_descriptors: {},
                      extensible: true,
                    })

                    // Add [Symbol.iterator] to keys iterator
                    match iter_obj {
                      Object(data) =>
                        data.symbol_properties[iterator_sym.id] = Object({
                          properties: {},
                          symbol_properties: {},
                          prototype: Null,
                          callable: Some(
                            MethodCallable("[Symbol.iterator]", fn(
                              this_val,
                              _args,
                            ) {
                              this_val
                            }),
                          ),
                          class_name: "Function",
                          descriptors: {},
                          symbol_descriptors: {},
                          extensible: true,
                        })
                      _ => ()
                    }
                    iter_obj
                  }
                  _ =>
                    raise @errors.TypeError(
                      message="Array.prototype.keys called on non-array",
                    )
                }
              }),
            ),
            class_name: "Function",
            descriptors: {},
            symbol_descriptors: {},
            extensible: true,
          })

          // Add entries() method that returns an iterator over [index, value] pairs
          proto_data.properties["entries"] = Object({
            properties: {},
            symbol_properties: {},
            prototype: Null,
            callable: Some(
              MethodCallable("entries", fn(this_val, _args) {
                match this_val {
                  Array(arr) => {
                    let index_ref : Ref[Int] = { val: 0 }
                    let next_method = Object({
                      properties: {},
                      symbol_properties: {},
                      prototype: Null,
                      callable: Some(
                        NativeCallable("next", fn(_args) {
                          let idx = index_ref.val
                          if idx < arr.elements.length() {
                            let value = arr.elements[idx]
                            index_ref.val = idx + 1
                            let result_props : Map[String, Value] = {}
                            result_props["value"] = Array({
                              elements: [Number(idx.to_double()), value],
                            })
                            result_props["done"] = Bool(false)
                            Object({
                              properties: result_props,
                              symbol_properties: {},
                              prototype: Null,
                              callable: None,
                              class_name: "Object",
                              descriptors: {},
                              symbol_descriptors: {},
                              extensible: true,
                            })
                          } else {
                            let result_props : Map[String, Value] = {}
                            result_props["value"] = Undefined
                            result_props["done"] = Bool(true)
                            Object({
                              properties: result_props,
                              symbol_properties: {},
                              prototype: Null,
                              callable: None,
                              class_name: "Object",
                              descriptors: {},
                              symbol_descriptors: {},
                              extensible: true,
                            })
                          }
                        }),
                      ),
                      class_name: "Function",
                      descriptors: {},
                      symbol_descriptors: {},
                      extensible: true,
                    })
                    let iter_props : Map[String, Value] = {}
                    iter_props["next"] = next_method
                    let iter_obj = Object({
                      properties: iter_props,
                      symbol_properties: {},
                      prototype: Null,
                      callable: None,
                      class_name: "Array Iterator",
                      descriptors: {},
                      symbol_descriptors: {},
                      extensible: true,
                    })

                    // Add [Symbol.iterator] to entries iterator
                    match iter_obj {
                      Object(data) =>
                        data.symbol_properties[iterator_sym.id] = Object({
                          properties: {},
                          symbol_properties: {},
                          prototype: Null,
                          callable: Some(
                            MethodCallable("[Symbol.iterator]", fn(
                              this_val,
                              _args,
                            ) {
                              this_val
                            }),
                          ),
                          class_name: "Function",
                          descriptors: {},
                          symbol_descriptors: {},
                          extensible: true,
                        })
                      _ => ()
                    }
                    iter_obj
                  }
                  _ =>
                    raise @errors.TypeError(
                      message="Array.prototype.entries called on non-array",
                    )
                }
              }),
            ),
            class_name: "Function",
            descriptors: {},
            symbol_descriptors: {},
            extensible: true,
          })
        }
        _ => ()
      }
    _ => ()
  }

  // Add [Symbol.iterator] for String prototype
  // String's [Symbol.iterator] is accessed via get_string_method, so we store a reference
  // to tell the interpreter to use string iterator
  // We'll handle this in the interpreter by checking for Symbol.iterator access on strings
}

///|
fn setup_math_builtins(env : Environment) -> Unit {
  let math_props : Map[String, Value] = {}
  math_props["PI"] = Number(3.141592653589793)
  math_props["E"] = Number(2.718281828459045)
  math_props["LN2"] = Number(0.6931471805599453)
  math_props["LN10"] = Number(2.302585092994046)
  math_props["LOG2E"] = Number(1.4426950408889634)
  math_props["LOG10E"] = Number(0.4342944819032518)
  math_props["SQRT2"] = Number(1.4142135623730951)
  math_props["SQRT1_2"] = Number(0.7071067811865476)
  math_props["abs"] = make_native_func("abs", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(n.abs())
  })
  math_props["floor"] = make_native_func("floor", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(n.floor())
  })
  math_props["ceil"] = make_native_func("ceil", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(n.ceil())
  })
  math_props["round"] = make_native_func("round", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    if n.is_nan() || n.is_inf() || n == 0.0 {
      Number(n)
    } else {
      let rounded = (n + 0.5).floor()
      if rounded == 0.0 && n < 0.0 {
        Number(-0.0)
      } else {
        Number(rounded)
      }
    }
  })
  math_props["trunc"] = make_native_func("trunc", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    if n >= 0.0 {
      Number(n.floor())
    } else {
      Number(-(-n).floor())
    }
  })
  math_props["sqrt"] = make_native_func("sqrt", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(n.sqrt())
  })
  math_props["pow"] = make_native_func("pow", fn(args) {
    let base = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    let exp = if args.length() > 1 { to_number(args[1]) } else { 0.0 / 0.0 }
    Number(@math.pow(base, exp))
  })
  math_props["min"] = make_native_func("min", fn(args) {
    if args.length() == 0 {
      return Number(1.0 / 0.0)
    }
    let mut result = to_number(args[0])
    for i = 1; i < args.length(); i = i + 1 {
      let n = to_number(args[i])
      if n.is_nan() {
        return Number(0.0 / 0.0)
      }
      if n < result {
        result = n
      }
    }
    Number(result)
  })
  math_props["max"] = make_native_func("max", fn(args) {
    if args.length() == 0 {
      return Number(-1.0 / 0.0)
    }
    let mut result = to_number(args[0])
    for i = 1; i < args.length(); i = i + 1 {
      let n = to_number(args[i])
      if n.is_nan() {
        return Number(0.0 / 0.0)
      }
      if n > result {
        result = n
      }
    }
    Number(result)
  })
  let rng_state : Array[Int] = [123456789]
  math_props["random"] = make_native_func("random", fn(_args) {
    let mut s = rng_state[0]
    s = s ^ (s << 13)
    s = s ^ (s.reinterpret_as_uint() >> 17).reinterpret_as_int()
    s = s ^ (s << 5)
    rng_state[0] = s
    let normalized = s.land(0x7FFFFFFF).to_double() / 2147483648.0
    Number(normalized)
  })
  math_props["sign"] = make_native_func("sign", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    if n.is_nan() {
      Number(0.0 / 0.0)
    } else if n > 0.0 {
      Number(1.0)
    } else if n < 0.0 {
      Number(-1.0)
    } else {
      Number(n) // preserves +0/-0
    }
  })
  math_props["log"] = make_native_func("log", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.ln(n))
  })
  math_props["log2"] = make_native_func("log2", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.log2(n))
  })
  math_props["log10"] = make_native_func("log10", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.log10(n))
  })
  math_props["sin"] = make_native_func("sin", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.sin(n))
  })
  math_props["cos"] = make_native_func("cos", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.cos(n))
  })
  math_props["tan"] = make_native_func("tan", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.tan(n))
  })
  math_props["asin"] = make_native_func("asin", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.asin(n))
  })
  math_props["acos"] = make_native_func("acos", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.acos(n))
  })
  math_props["atan"] = make_native_func("atan", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.atan(n))
  })
  math_props["atan2"] = make_native_func("atan2", fn(args) {
    let y = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    let x = if args.length() > 1 { to_number(args[1]) } else { 0.0 / 0.0 }
    Number(@math.atan2(y, x))
  })
  math_props["sinh"] = make_native_func("sinh", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.sinh(n))
  })
  math_props["cosh"] = make_native_func("cosh", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.cosh(n))
  })
  math_props["tanh"] = make_native_func("tanh", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.tanh(n))
  })
  math_props["asinh"] = make_native_func("asinh", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.asinh(n))
  })
  math_props["acosh"] = make_native_func("acosh", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.acosh(n))
  })
  math_props["atanh"] = make_native_func("atanh", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.atanh(n))
  })
  math_props["exp"] = make_native_func("exp", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.exp(n))
  })
  math_props["expm1"] = make_native_func("expm1", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    Number(@math.expm1(n))
  })
  math_props["log1p"] = make_native_func("log1p", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    // log1p(x) = ln(1 + x), more accurate for small x
    if n.is_nan() || n < -1.0 {
      Number(0.0 / 0.0)
    } else if n == -1.0 {
      Number(-1.0 / 0.0)
    } else if n == 0.0 || n.is_inf() {
      Number(n)
    } else {
      Number(@math.ln(1.0 + n))
    }
  })
  math_props["hypot"] = make_native_func("hypot", fn(args) {
    if args.length() == 0 {
      return Number(0.0)
    }
    let mut sum = 0.0
    for arg in args {
      let n = to_number(arg)
      if n.is_inf() {
        return Number(1.0 / 0.0)
      }
      if n.is_nan() {
        return Number(0.0 / 0.0)
      }
      sum = sum + n * n
    }
    Number(sum.sqrt())
  })
  math_props["cbrt"] = make_native_func("cbrt", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    if n.is_nan() || n.is_inf() || n == 0.0 {
      Number(n)
    } else {
      let sign : Double = if n < 0.0 { -1.0 } else { 1.0 }
      Number(sign * @math.pow(n.abs(), 1.0 / 3.0))
    }
  })
  math_props["clz32"] = make_native_func("clz32", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 }
    let int_val = to_int32(n)
    if int_val == 0 {
      return Number(32.0)
    }
    let uint = int_val.reinterpret_as_uint()
    let mut count = 0
    let mut mask : UInt = 2147483648 // 0x80000000
    while mask > 0 && (uint & mask) == 0 {
      count = count + 1
      mask = mask >> 1
    }
    Number(count.to_double())
  })
  math_props["imul"] = make_native_func("imul", fn(args) {
    let a = if args.length() > 0 { to_number(args[0]) } else { 0.0 }
    let b = if args.length() > 1 { to_number(args[1]) } else { 0.0 }
    let int_a = to_int32(a)
    let int_b = to_int32(b)
    // Perform 32-bit multiplication and mask to lower 32 bits as signed
    let product = int_a.to_int64() * int_b.to_int64()
    let masked = product & 0xFFFFFFFFL
    // Convert to signed 32-bit
    let result = if masked > 0x7FFFFFFFL {
      (masked - 0x100000000L).to_int()
    } else {
      masked.to_int()
    }
    Number(result.to_double())
  })
  math_props["fround"] = make_native_func("fround", fn(args) {
    let n = if args.length() > 0 { to_number(args[0]) } else { 0.0 / 0.0 }
    // Round to nearest float32 (basic approximation)
    if n.is_nan() || n.is_inf() || n == 0.0 {
      Number(n)
    } else {
      // Convert to float32 and back (approximation)
      Number(Float::from_double(n).to_double())
    }
  })
  env.def_builtin(
    "Math",
    Object({
      properties: math_props,
      symbol_properties: {},
      prototype: Null,
      callable: None,
      class_name: "Math",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    }),
  )
}

///|
fn register_error_ctor(
  env : Environment,
  name : String,
  parent_proto : Value,
) -> Value {
  let proto_props : Map[String, Value] = {}
  proto_props["name"] = String_(name)
  proto_props["message"] = String_("")
  proto_props["toString"] = make_native_func("toString", fn(_args) {
    String_(name)
  })
  let proto_obj : Value = Object({
    properties: proto_props,
    symbol_properties: {},
    prototype: parent_proto,
    callable: None,
    class_name: name,
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  env.def_builtin(
    name,
    Object({
      properties: { "prototype": proto_obj },
      symbol_properties: {},
      prototype: Null,
      callable: Some(
        NativeCallable(name, fn(args) {
          let message = if args.length() > 0 {
            match args[0] {
              String_(s) => s
              Undefined => ""
              other => other.to_string()
            }
          } else {
            ""
          }
          let err_props : Map[String, Value] = {}
          err_props["message"] = String_(message)
          err_props["name"] = String_(name)
          err_props["stack"] = String_(name + ": " + message)
          Object({
            properties: err_props,
            symbol_properties: {},
            prototype: proto_obj,
            callable: None,
            class_name: name,
            descriptors: {},
            symbol_descriptors: {},
            extensible: true,
          })
        }),
      ),
      class_name: "Function",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    }),
  )
  // Set ErrorType.prototype.constructor = ErrorType
  match proto_obj {
    Object(data) => {
      let ctor = try { env.get(name) } catch { _ => Undefined }
      data.properties["constructor"] = ctor
      data.descriptors["constructor"] = { writable: true, enumerable: false, configurable: true }
    }
    _ => ()
  }
  proto_obj
}

///|
fn trim_start(s : String) -> String {
  let chars = s.to_array()
  let mut start = 0
  while start < chars.length() {
    let c = chars[start]
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      start = start + 1
    } else {
      break
    }
  }
  if start == 0 {
    return s
  }
  let buf = StringBuilder::new()
  for i = start; i < chars.length(); i = i + 1 {
    buf.write_char(chars[i])
  }
  buf.to_string()
}

///|
fn char_to_digit(c : Char, radix : Int) -> Int {
  let code = c.to_int()
  let digit = if code >= 48 && code <= 57 {
    // '0' - '9'
    code - 48
  } else if code >= 65 && code <= 90 {
    // 'A' - 'Z'
    code - 65 + 10
  } else if code >= 97 && code <= 122 {
    // 'a' - 'z'
    code - 97 + 10
  } else {
    -1
  }
  if digit >= 0 && digit < radix {
    digit
  } else {
    -1
  }
}

///|
fn setup_number_builtins(env : Environment) -> Unit {
  let number_proto_props : Map[String, Value] = {}
  number_proto_props["toFixed"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("toFixed", fn(this_val, args) {
        let n : Double = match this_val {
          Number(v) => v
          Object(data) =>
            if data.class_name == "Number" {
              match data.properties.get("[[NumberData]]") {
                Some(Number(v)) => v
                _ =>
                  raise @errors.TypeError(
                    message="Number.prototype.toFixed requires that 'this' be a Number",
                  )
              }
            } else {
              raise @errors.TypeError(
                message="Number.prototype.toFixed requires that 'this' be a Number",
              )
            }
          _ =>
            raise @errors.TypeError(
              message="Number.prototype.toFixed requires that 'this' be a Number",
            )
        }
        let digits = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        if digits < 0 || digits > 100 {
          raise @errors.RangeError(
            message="toFixed() digits argument must be between 0 and 100",
          )
        }
        if n.is_nan() {
          return String_("NaN")
        }
        if n.is_inf() {
          return String_(if n > 0.0 { "Infinity" } else { "-Infinity" })
        }
        // Simple implementation using multiplier approach
        if digits == 0 {
          let rounded = if n >= 0.0 {
            (n + 0.5).floor()
          } else {
            -(-n + 0.5).floor()
          }
          return String_(rounded.to_int().to_string())
        }
        let multiplier = @math.pow(10.0, digits.to_double())
        let shifted = n * multiplier
        let rounded = if shifted >= 0.0 {
          (shifted + 0.5).floor()
        } else {
          -(-shifted + 0.5).floor()
        }
        let rounded_abs = rounded.abs()
        let int_part_abs = (rounded_abs / multiplier).floor()
        let frac_abs = rounded_abs - int_part_abs * multiplier
        let buf = StringBuilder::new()
        if rounded < 0.0 || (n < 0.0 && rounded == 0.0) {
          buf.write_string("-")
        }
        buf.write_string(int_part_abs.to_int().to_string())
        buf.write_string(".")
        let frac_str = frac_abs.to_int().to_string()
        // Pad with leading zeros
        for _i = 0; _i < digits - frac_str.length(); _i = _i + 1 {
          buf.write_string("0")
        }
        buf.write_string(frac_str)
        String_(buf.to_string())
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  number_proto_props["toString"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("toString", fn(this_val, args) {
        // Validate this is a number
        let n : Double = match this_val {
          Number(v) => v
          Object(data) =>
            if data.class_name == "Number" {
              match data.properties.get("[[NumberData]]") {
                Some(Number(v)) => v
                _ =>
                  raise @errors.TypeError(
                    message="Number.prototype.toString requires that 'this' be a Number",
                  )
              }
            } else {
              raise @errors.TypeError(
                message="Number.prototype.toString requires that 'this' be a Number",
              )
            }
          _ =>
            raise @errors.TypeError(
              message="Number.prototype.toString requires that 'this' be a Number",
            )
        }
        let radix = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          10
        }
        if radix < 2 || radix > 36 {
          raise @errors.RangeError(
            message="toString() radix must be between 2 and 36",
          )
        }
        if n.is_nan() {
          return String_("NaN")
        }
        if n.is_inf() {
          return String_(if n > 0.0 { "Infinity" } else { "-Infinity" })
        }
        if radix == 10 {
          return String_(n.to_string())
        }
        // Integer to string in given radix
        let is_neg = n < 0.0
        let mut val = n.abs().floor()
        if val == 0.0 {
          return String_("0")
        }
        let chars : Array[Char] = []
        let digits = "0123456789abcdefghijklmnopqrstuvwxyz"
        let digit_chars = digits.to_array()
        while val > 0.0 {
          let remainder = (val % radix.to_double()).to_int()
          chars.push(digit_chars[remainder])
          val = (val / radix.to_double()).floor()
        }
        chars.rev_in_place()
        let buf = StringBuilder::new()
        if is_neg {
          buf.write_string("-")
        }
        for c in chars {
          buf.write_char(c)
        }
        String_(buf.to_string())
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  number_proto_props["valueOf"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("valueOf", fn(this_val, _args) {
        match this_val {
          Number(_) => this_val
          Object(data) =>
            if data.class_name == "Number" {
              match data.properties.get("[[NumberData]]") {
                Some(Number(v)) => Number(v)
                _ =>
                  raise @errors.TypeError(
                    message="Number.prototype.valueOf requires that 'this' be a Number",
                  )
              }
            } else {
              raise @errors.TypeError(
                message="Number.prototype.valueOf requires that 'this' be a Number",
              )
            }
          _ =>
            raise @errors.TypeError(
              message="Number.prototype.valueOf requires that 'this' be a Number",
            )
        }
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  number_proto_props["toPrecision"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("toPrecision", fn(this_val, args) {
        let n = to_number(this_val)
        // If no argument or undefined, just return toString result
        if args.length() == 0 {
          return String_(n.to_string())
        }
        match args[0] {
          Undefined => return String_(n.to_string())
          _ => ()
        }
        let precision = to_number(args[0]).to_int()
        if precision < 1 || precision > 100 {
          raise @errors.RangeError(
            message="toPrecision() argument must be between 1 and 100",
          )
        }
        if n.is_nan() {
          return String_("NaN")
        }
        if n.is_inf() {
          return String_(if n > 0.0 { "Infinity" } else { "-Infinity" })
        }
        // Use exponential format when needed
        let is_neg = n < 0.0
        let abs_n = n.abs()
        if abs_n == 0.0 {
          let buf = StringBuilder::new()
          if is_neg {
            buf.write_string("-")
          }
          buf.write_char('0')
          if precision > 1 {
            buf.write_char('.')
            for _i = 1; _i < precision; _i = _i + 1 {
              buf.write_char('0')
            }
          }
          return String_(buf.to_string())
        }
        // Calculate exponent
        let log_val = @math.log10(abs_n)
        let mut exp = log_val.floor().to_int()
        // If exponent >= precision or < -6, use exponential notation
        if exp >= precision || exp < -6 {
          // Use exponential notation
          let mut mantissa = abs_n / @math.pow(10.0, exp.to_double())
          // Round mantissa and check for overflow
          let mult = @math.pow(10.0, (precision - 1).to_double())
          let rounded_mantissa = (mantissa * mult + 0.5).floor() / mult
          // If rounding caused overflow (mantissa >= 10), adjust
          if rounded_mantissa >= 10.0 {
            mantissa = rounded_mantissa / 10.0
            exp = exp + 1
          } else {
            mantissa = rounded_mantissa
          }
          let buf = StringBuilder::new()
          if is_neg {
            buf.write_string("-")
          }
          // Format mantissa with precision-1 decimal places
          let mantissa_str = format_number_precision(mantissa, precision)
          buf.write_string(mantissa_str)
          buf.write_char('e')
          if exp >= 0 {
            buf.write_char('+')
          }
          buf.write_string(exp.to_string())
          return String_(buf.to_string())
        }
        // Use fixed notation
        let decimal_places = precision - exp - 1
        let buf = StringBuilder::new()
        if is_neg {
          buf.write_string("-")
        }
        if decimal_places >= 0 {
          let multiplier = @math.pow(10.0, decimal_places.to_double())
          let shifted = abs_n * multiplier
          let rounded = (shifted + 0.5).floor()
          let int_val = rounded.to_int64()
          let int_str = int_val.to_string()
          let int_len = int_str.length()
          if int_len <= decimal_places {
            buf.write_string("0.")
            for _i = 0; _i < decimal_places - int_len; _i = _i + 1 {
              buf.write_char('0')
            }
            buf.write_string(int_str)
          } else {
            let int_part_len = int_len - decimal_places
            let chars = int_str.to_array()
            for i = 0; i < int_part_len; i = i + 1 {
              buf.write_char(chars[i])
            }
            if decimal_places > 0 {
              buf.write_char('.')
              for i = int_part_len; i < int_len; i = i + 1 {
                buf.write_char(chars[i])
              }
            }
          }
        } else {
          let multiplier = @math.pow(10.0, (-decimal_places).to_double())
          let shifted = abs_n / multiplier
          let rounded = (shifted + 0.5).floor()
          buf.write_string((rounded * multiplier).to_int().to_string())
        }
        String_(buf.to_string())
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  number_proto_props["toExponential"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("toExponential", fn(this_val, args) {
        let n = to_number(this_val)
        if n.is_nan() {
          return String_("NaN")
        }
        if n.is_inf() {
          return String_(if n > 0.0 { "Infinity" } else { "-Infinity" })
        }
        let is_neg = n < 0.0
        let abs_n = n.abs()
        // Handle zero specially
        if abs_n == 0.0 {
          let buf = StringBuilder::new()
          if is_neg {
            buf.write_string("-")
          }
          buf.write_char('0')
          if args.length() > 0 {
            match args[0] {
              Undefined => ()
              _ => {
                let frac_digits = to_number(args[0]).to_int()
                if frac_digits < 0 || frac_digits > 100 {
                  raise @errors.RangeError(
                    message="toExponential() argument must be between 0 and 100",
                  )
                }
                if frac_digits > 0 {
                  buf.write_char('.')
                  for _i = 0; _i < frac_digits; _i = _i + 1 {
                    buf.write_char('0')
                  }
                }
              }
            }
          }
          buf.write_string("e+0")
          return String_(buf.to_string())
        }
        // Calculate exponent
        let log_val = @math.log10(abs_n)
        let mut exp = log_val.floor().to_int()
        let mut mantissa = abs_n / @math.pow(10.0, exp.to_double())
        let buf = StringBuilder::new()
        if is_neg {
          buf.write_string("-")
        }
        // Format mantissa
        if args.length() > 0 {
          match args[0] {
            Undefined => buf.write_string(mantissa.to_string())
            _ => {
              let frac_digits = to_number(args[0]).to_int()
              if frac_digits < 0 || frac_digits > 100 {
                raise @errors.RangeError(
                  message="toExponential() argument must be between 0 and 100",
                )
              }
              // Round mantissa and check for overflow
              let precision = frac_digits + 1
              let mult = @math.pow(10.0, (precision - 1).to_double())
              let rounded_mantissa = (mantissa * mult + 0.5).floor() / mult
              // If rounding caused overflow (mantissa >= 10), adjust
              if rounded_mantissa >= 10.0 {
                mantissa = rounded_mantissa / 10.0
                exp = exp + 1
              } else {
                mantissa = rounded_mantissa
              }
              let mantissa_str = format_number_precision(mantissa, precision)
              buf.write_string(mantissa_str)
            }
          }
        } else {
          buf.write_string(mantissa.to_string())
        }
        buf.write_char('e')
        if exp >= 0 {
          buf.write_char('+')
        }
        buf.write_string(exp.to_string())
        String_(buf.to_string())
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  let number_proto : Value = Object({
    properties: number_proto_props,
    symbol_properties: {},
    prototype: Null,
    callable: None,
    class_name: "Number",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  let number_props : Map[String, Value] = {}
  number_props["prototype"] = number_proto
  number_props["MAX_SAFE_INTEGER"] = Number(9007199254740991.0)
  number_props["MIN_SAFE_INTEGER"] = Number(-9007199254740991.0)
  number_props["EPSILON"] = Number(2.220446049250313e-16)
  number_props["MAX_VALUE"] = Number(1.7976931348623157e+308)
  number_props["MIN_VALUE"] = Number(5.0e-324)
  number_props["POSITIVE_INFINITY"] = Number(1.0 / 0.0)
  number_props["NEGATIVE_INFINITY"] = Number(-1.0 / 0.0)
  number_props["NaN"] = Number(0.0 / 0.0)
  number_props["isNaN"] = make_native_func("isNaN", fn(args) {
    let val = if args.length() > 0 { args[0] } else { Undefined }
    match val {
      Number(n) => Bool(n.is_nan())
      _ => Bool(false) // Number.isNaN does NOT coerce
    }
  })
  number_props["isFinite"] = make_native_func("isFinite", fn(args) {
    let val = if args.length() > 0 { args[0] } else { Undefined }
    match val {
      Number(n) => Bool(not(n.is_nan()) && not(n.is_inf()))
      _ => Bool(false) // Number.isFinite does NOT coerce
    }
  })
  number_props["isInteger"] = make_native_func("isInteger", fn(args) {
    let val = if args.length() > 0 { args[0] } else { Undefined }
    match val {
      Number(n) =>
        if n.is_nan() || n.is_inf() {
          Bool(false)
        } else {
          Bool(n.floor() == n)
        }
      _ => Bool(false)
    }
  })
  number_props["isSafeInteger"] = make_native_func("isSafeInteger", fn(args) {
    // Number.isSafeInteger() - check if value is a safe integer
    // Safe integers are integers in range -(2^53-1) to 2^53-1
    let val = if args.length() > 0 { args[0] } else { Undefined }
    match val {
      Number(n) =>
        if n.is_nan() || n.is_inf() {
          Bool(false)
        } else if n.floor() != n {
          Bool(false)
        } else {
          let max_safe = 9007199254740991.0
          Bool(n >= -max_safe && n <= max_safe)
        }
      _ => Bool(false)
    }
  })
  number_props["parseInt"] = env.get("parseInt") catch { _ => Undefined }
  number_props["parseFloat"] = env.get("parseFloat") catch { _ => Undefined }
  env.def_builtin(
    "Number",
    Object({
      properties: number_props,
      symbol_properties: {},
      prototype: Null,
      callable: Some(
        NativeCallable("Number", fn(args) {
          let val = if args.length() > 0 { args[0] } else { Undefined }
          Number(to_number(val))
        }),
      ),
      class_name: "Function",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    }),
  )
  // Set Number.prototype.constructor = Number
  match number_proto {
    Object(data) => {
      let ctor = try { env.get("Number") } catch { _ => Undefined }
      data.properties["constructor"] = ctor
      data.descriptors["constructor"] = { writable: true, enumerable: false, configurable: true }
    }
    _ => ()
  }
}

///|
fn setup_json_builtins(env : Environment) -> Unit {
  let json_props : Map[String, Value] = {}
  json_props["parse"] = make_native_func("parse", fn(args) {
    let str = if args.length() > 0 { args[0].to_string() } else { "" }
    json_parse(str)
  })
  json_props["stringify"] = make_native_func("stringify", fn(args) {
    let val = if args.length() > 0 { args[0] } else { Undefined }
    let indent = if args.length() > 2 {
      match args[2] {
        Number(n) => {
          let spaces = n.to_int()
          if spaces > 0 {
            let buf = StringBuilder::new()
            for _i = 0; _i < spaces; _i = _i + 1 {
              buf.write_char(' ')
            }
            Some(buf.to_string())
          } else {
            None
          }
        }
        String_(s) => if s.length() > 0 { Some(s) } else { None }
        _ => None
      }
    } else {
      None
    }
    let seen : Array[Value] = []
    json_stringify_value(val, seen, indent, 0)
  })
  env.def_builtin(
    "JSON",
    Object({
      properties: json_props,
      symbol_properties: {},
      prototype: Null,
      callable: None,
      class_name: "JSON",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    }),
  )
}

///|
fn json_parse(input : String) -> Value raise Error {
  let chars = input.to_array()
  let len = chars.length()
  let pos : Array[Int] = [0] // mutable position
  fn skip_ws() -> Unit {
    while pos[0] < len {
      let c = chars[pos[0]]
      if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
        pos[0] += 1
      } else {
        break
      }
    }
  }

  letrec parse_value = fn() -> Value raise Error {
    skip_ws()
    if pos[0] >= len {
      raise @errors.SyntaxError(message="Unexpected end of JSON input")
    }
    let c = chars[pos[0]]
    if c == '"' {
      parse_string()
    } else if c == '{' {
      parse_object()
    } else if c == '[' {
      parse_array()
    } else if c == 't' {
      if pos[0] + 4 <= len &&
        chars[pos[0] + 1] == 'r' &&
        chars[pos[0] + 2] == 'u' &&
        chars[pos[0] + 3] == 'e' {
        pos[0] += 4
        Bool(true)
      } else {
        raise @errors.SyntaxError(
          message="Unexpected token in JSON at position \{pos[0]}",
        )
      }
    } else if c == 'f' {
      if pos[0] + 5 <= len &&
        chars[pos[0] + 1] == 'a' &&
        chars[pos[0] + 2] == 'l' &&
        chars[pos[0] + 3] == 's' &&
        chars[pos[0] + 4] == 'e' {
        pos[0] += 5
        Bool(false)
      } else {
        raise @errors.SyntaxError(
          message="Unexpected token in JSON at position \{pos[0]}",
        )
      }
    } else if c == 'n' {
      if pos[0] + 4 <= len &&
        chars[pos[0] + 1] == 'u' &&
        chars[pos[0] + 2] == 'l' &&
        chars[pos[0] + 3] == 'l' {
        pos[0] += 4
        Null
      } else {
        raise @errors.SyntaxError(
          message="Unexpected token in JSON at position \{pos[0]}",
        )
      }
    } else if c == '-' || (c >= '0' && c <= '9') {
      parse_number()
    } else {
      raise @errors.SyntaxError(
        message="Unexpected token in JSON at position \{pos[0]}",
      )
    }
  }
  and parse_string = fn() -> Value raise Error {
    pos[0] += 1 // skip opening "
    let buf = StringBuilder::new()
    while pos[0] < len {
      let ch = chars[pos[0]]
      if ch == '"' {
        pos[0] += 1
        return String_(buf.to_string())
      }
      if ch == '\\' {
        pos[0] += 1
        if pos[0] >= len {
          raise @errors.SyntaxError(message="Unexpected end of JSON input")
        }
        let esc = chars[pos[0]]
        match esc {
          '"' => buf.write_char('"')
          '\\' => buf.write_char('\\')
          '/' => buf.write_char('/')
          'b' => buf.write_char('\u0008')
          'f' => buf.write_char('\u000C')
          'n' => buf.write_char('\n')
          'r' => buf.write_char('\r')
          't' => buf.write_char('\t')
          'u' =>
            if pos[0] + 4 < len {
              let mut code = 0
              for _j = 0; _j < 4; _j = _j + 1 {
                pos[0] += 1
                let hex_c = chars[pos[0]]
                let d = char_to_digit(hex_c, 16)
                if d < 0 {
                  raise @errors.SyntaxError(
                    message="Invalid unicode escape in JSON",
                  )
                }
                code = code * 16 + d
              }
              buf.write_char(code.unsafe_to_char())
            } else {
              raise @errors.SyntaxError(
                message="Invalid unicode escape in JSON",
              )
            }
          _ =>
            raise @errors.SyntaxError(
              message="Invalid escape character in JSON",
            )
        }
      } else {
        if ch.to_int() < 32 {
          raise @errors.SyntaxError(
            message="Unexpected control character in JSON",
          )
        }
        buf.write_char(ch)
      }
      pos[0] += 1
    }
    raise @errors.SyntaxError(message="Unterminated string in JSON")
  }
  and parse_number = fn() -> Value raise Error {
    let start = pos[0]
    if pos[0] < len && chars[pos[0]] == '-' {
      pos[0] += 1
    }
    while pos[0] < len && chars[pos[0]] >= '0' && chars[pos[0]] <= '9' {
      pos[0] += 1
    }
    if pos[0] < len && chars[pos[0]] == '.' {
      pos[0] += 1
      while pos[0] < len && chars[pos[0]] >= '0' && chars[pos[0]] <= '9' {
        pos[0] += 1
      }
    }
    if pos[0] < len && (chars[pos[0]] == 'e' || chars[pos[0]] == 'E') {
      pos[0] += 1
      if pos[0] < len && (chars[pos[0]] == '+' || chars[pos[0]] == '-') {
        pos[0] += 1
      }
      while pos[0] < len && chars[pos[0]] >= '0' && chars[pos[0]] <= '9' {
        pos[0] += 1
      }
    }
    let buf = StringBuilder::new()
    for i = start; i < pos[0]; i = i + 1 {
      buf.write_char(chars[i])
    }
    let n = @strconv.parse_double(buf.to_string()) catch {
      _ => raise @errors.SyntaxError(message="Invalid number in JSON")
    }
    Number(n)
  }
  and parse_object = fn() -> Value raise Error {
    pos[0] += 1 // skip {
    let properties : Map[String, Value] = {}
    skip_ws()
    if pos[0] < len && chars[pos[0]] == '}' {
      pos[0] += 1
      return Object({
        properties,
        symbol_properties: {},
        prototype: Null,
        callable: None,
        class_name: "Object",
        descriptors: {},
        symbol_descriptors: {},
        extensible: true,
      })
    }
    while true {
      skip_ws()
      if pos[0] >= len || chars[pos[0]] != '"' {
        raise @errors.SyntaxError(
          message="Expected property name in JSON at position \{pos[0]}",
        )
      }
      let key = match parse_string() {
        String_(s) => s
        _ => ""
      }
      skip_ws()
      if pos[0] >= len || chars[pos[0]] != ':' {
        raise @errors.SyntaxError(
          message="Expected ':' in JSON at position \{pos[0]}",
        )
      }
      pos[0] += 1
      let value = parse_value()
      properties[key] = value
      skip_ws()
      if pos[0] < len && chars[pos[0]] == ',' {
        pos[0] += 1
      } else {
        break
      }
    }
    skip_ws()
    if pos[0] >= len || chars[pos[0]] != '}' {
      raise @errors.SyntaxError(
        message="Expected '}' in JSON at position \{pos[0]}",
      )
    }
    pos[0] += 1
    Object({
      properties,
      symbol_properties: {},
      prototype: Null,
      callable: None,
      class_name: "Object",
      descriptors: {},
      symbol_descriptors: {},
      extensible: true,
    })
  }
  and parse_array = fn() -> Value raise Error {
    pos[0] += 1 // skip [
    let elements : Array[Value] = []
    skip_ws()
    if pos[0] < len && chars[pos[0]] == ']' {
      pos[0] += 1
      return Array({ elements, })
    }
    while true {
      let val = parse_value()
      elements.push(val)
      skip_ws()
      if pos[0] < len && chars[pos[0]] == ',' {
        pos[0] += 1
      } else {
        break
      }
    }
    skip_ws()
    if pos[0] >= len || chars[pos[0]] != ']' {
      raise @errors.SyntaxError(
        message="Expected ']' in JSON at position \{pos[0]}",
      )
    }
    pos[0] += 1
    Array({ elements, })
  }

  let result = parse_value()
  skip_ws()
  if pos[0] < len {
    raise @errors.SyntaxError(
      message="Unexpected non-whitespace character after JSON at position \{pos[0]}",
    )
  }
  result
}

///|
fn json_stringify_value(
  val : Value,
  seen : Array[Value],
  indent : String?,
  depth : Int,
) -> Value raise Error {
  match val {
    Undefined => Undefined
    Null => String_("null")
    Bool(b) => String_(b.to_string())
    Number(n) =>
      if n.is_nan() || n.is_inf() {
        String_("null")
      } else {
        String_(val.to_string())
      }
    String_(s) => String_(json_escape_string(s))
    Symbol(_) => Undefined // Symbols are omitted from JSON
    Map(_) => String_("{}") // Maps serialize as empty objects
    Set(_) => String_("{}") // Sets serialize as empty objects
    Promise(_) => String_("{}") // Promises serialize as empty objects
    Array(data) => {
      // Cycle detection
      for s in seen {
        match s {
          Array(sd) =>
            if physical_equal(sd, data) {
              raise @errors.TypeError(
                message="Converting circular structure to JSON",
              )
            }
          _ => ()
        }
      }
      seen.push(val)
      let new_depth = depth + 1
      let buf = StringBuilder::new()
      buf.write_char('[')
      let mut first = true
      for el in data.elements {
        let stringified = json_stringify_value(el, seen, indent, new_depth)
        match stringified {
          Undefined => {
            if not(first) {
              buf.write_char(',')
              match indent {
                Some(_) => ()
                None => ()
              }
            }
            first = false
            match indent {
              Some(ind) => {
                buf.write_char('\n')
                for _i = 0; _i < new_depth; _i = _i + 1 {
                  buf.write_string(ind)
                }
              }
              None => ()
            }
            buf.write_string("null")
          }
          String_(s) => {
            if not(first) {
              buf.write_char(',')
            }
            first = false
            match indent {
              Some(ind) => {
                buf.write_char('\n')
                for _i = 0; _i < new_depth; _i = _i + 1 {
                  buf.write_string(ind)
                }
              }
              None => ()
            }
            buf.write_string(s)
          }
          _ => ()
        }
      }
      match indent {
        Some(ind) =>
          if data.elements.length() > 0 {
            buf.write_char('\n')
            for _i = 0; _i < depth; _i = _i + 1 {
              buf.write_string(ind)
            }
          }
        None => ()
      }
      buf.write_char(']')
      let _ = seen.pop()
      String_(buf.to_string())
    }
    Object(data) =>
      match data.callable {
        Some(_) => Undefined // Functions are not serialized
        None => {
          // Cycle detection
          for s in seen {
            match s {
              Object(sd) =>
                if physical_equal(sd, data) {
                  raise @errors.TypeError(
                    message="Converting circular structure to JSON",
                  )
                }
              _ => ()
            }
          }
          seen.push(val)
          let new_depth = depth + 1
          let buf = StringBuilder::new()
          buf.write_char('{')
          let mut first = true
          data.properties.each(fn(k, v) {
            let stringified = json_stringify_value(v, seen, indent, new_depth)
            match stringified {
              Undefined => () // Skip undefined values
              String_(s) => {
                if not(first) {
                  buf.write_char(',')
                }
                first = false
                match indent {
                  Some(ind) => {
                    buf.write_char('\n')
                    for _i = 0; _i < new_depth; _i = _i + 1 {
                      buf.write_string(ind)
                    }
                  }
                  None => ()
                }
                buf.write_string(json_escape_string(k))
                buf.write_char(':')
                match indent {
                  Some(_) => buf.write_char(' ')
                  None => ()
                }
                buf.write_string(s)
              }
              _ => ()
            }
          })
          match indent {
            Some(ind) =>
              if not(first) {
                buf.write_char('\n')
                for _i = 0; _i < depth; _i = _i + 1 {
                  buf.write_string(ind)
                }
              }
            None => ()
          }
          buf.write_char('}')
          let _ = seen.pop()
          String_(buf.to_string())
        }
      }
  }
}

///|

///|
pub fn get_number_method(num_val : Value, prop : String) -> Value {
  match prop {
    "toFixed" =>
      Object({
        properties: {},
        symbol_properties: {},
        prototype: Null,
        callable: Some(
          MethodCallable("toFixed", fn(this_val, args) {
            let n = to_number(this_val)
            let digits = if args.length() > 0 {
              to_number(args[0]).to_int()
            } else {
              0
            }
            if digits < 0 || digits > 100 {
              raise @errors.RangeError(
                message="toFixed() digits argument must be between 0 and 100",
              )
            }
            if n.is_nan() {
              return String_("NaN")
            }
            if n.is_inf() {
              return String_(if n > 0.0 { "Infinity" } else { "-Infinity" })
            }
            if digits == 0 {
              let rounded = if n >= 0.0 {
                (n + 0.5).floor()
              } else {
                -(-n + 0.5).floor()
              }
              return String_(rounded.to_int().to_string())
            }
            let multiplier = @math.pow(10.0, digits.to_double())
            let shifted = n * multiplier
            let rounded = if shifted >= 0.0 {
              (shifted + 0.5).floor()
            } else {
              -(-shifted + 0.5).floor()
            }
            let rounded_abs = rounded.abs()
            let int_part_abs = (rounded_abs / multiplier).floor()
            let frac_abs = rounded_abs - int_part_abs * multiplier
            let buf = StringBuilder::new()
            if rounded < 0.0 || (n < 0.0 && rounded == 0.0) {
              buf.write_string("-")
            }
            buf.write_string(int_part_abs.to_int().to_string())
            buf.write_string(".")
            let frac_str = frac_abs.to_int().to_string()
            for _i = 0; _i < digits - frac_str.length(); _i = _i + 1 {
              buf.write_string("0")
            }
            buf.write_string(frac_str)
            String_(buf.to_string())
          }),
        ),
        class_name: "Function",
        descriptors: {},
        symbol_descriptors: {},
        extensible: true,
      })
    "toString" =>
      Object({
        properties: {},
        symbol_properties: {},
        prototype: Null,
        callable: Some(
          MethodCallable("toString", fn(this_val, args) {
            let n = to_number(this_val)
            let radix = if args.length() > 0 {
              to_number(args[0]).to_int()
            } else {
              10
            }
            if radix < 2 || radix > 36 {
              raise @errors.RangeError(
                message="toString() radix must be between 2 and 36",
              )
            }
            if n.is_nan() {
              return String_("NaN")
            }
            if n.is_inf() {
              return String_(if n > 0.0 { "Infinity" } else { "-Infinity" })
            }
            if radix == 10 {
              return String_(num_val.to_string())
            }
            let is_neg = n < 0.0
            let mut val = n.abs().floor()
            if val == 0.0 {
              return String_("0")
            }
            let digits_arr : Array[Char] = []
            let digit_str = "0123456789abcdefghijklmnopqrstuvwxyz"
            let digit_chars = digit_str.to_array()
            while val > 0.0 {
              let remainder = (val % radix.to_double()).to_int()
              digits_arr.push(digit_chars[remainder])
              val = (val / radix.to_double()).floor()
            }
            digits_arr.rev_in_place()
            let buf = StringBuilder::new()
            if is_neg {
              buf.write_string("-")
            }
            for c in digits_arr {
              buf.write_char(c)
            }
            String_(buf.to_string())
          }),
        ),
        class_name: "Function",
        descriptors: {},
        symbol_descriptors: {},
        extensible: true,
      })
    "valueOf" =>
      Object({
        properties: {},
        symbol_properties: {},
        prototype: Null,
        callable: Some(
          MethodCallable("valueOf", fn(this_val, _args) {
            Number(to_number(this_val))
          }),
        ),
        class_name: "Function",
        descriptors: {},
        symbol_descriptors: {},
        extensible: true,
      })
    "toPrecision" =>
      Object({
        properties: {},
        symbol_properties: {},
        prototype: Null,
        callable: Some(
          MethodCallable("toPrecision", fn(this_val, args) {
            let n = to_number(this_val)
            if args.length() == 0 {
              return String_(n.to_string())
            }
            match args[0] {
              Undefined => return String_(n.to_string())
              _ => ()
            }
            let precision = to_number(args[0]).to_int()
            if precision < 1 || precision > 100 {
              raise @errors.RangeError(
                message="toPrecision() argument must be between 1 and 100",
              )
            }
            if n.is_nan() {
              return String_("NaN")
            }
            if n.is_inf() {
              return String_(if n > 0.0 { "Infinity" } else { "-Infinity" })
            }
            let is_neg = n < 0.0
            let abs_n = n.abs()
            if abs_n == 0.0 {
              let buf = StringBuilder::new()
              if is_neg {
                buf.write_string("-")
              }
              buf.write_char('0')
              if precision > 1 {
                buf.write_char('.')
                for _i = 1; _i < precision; _i = _i + 1 {
                  buf.write_char('0')
                }
              }
              return String_(buf.to_string())
            }
            let log_val = @math.log10(abs_n)
            let mut exp = log_val.floor().to_int()
            if exp >= precision || exp < -6 {
              let mut mantissa = abs_n / @math.pow(10.0, exp.to_double())
              // Round mantissa and check for overflow
              let mult = @math.pow(10.0, (precision - 1).to_double())
              let rounded_mantissa = (mantissa * mult + 0.5).floor() / mult
              if rounded_mantissa >= 10.0 {
                mantissa = rounded_mantissa / 10.0
                exp = exp + 1
              } else {
                mantissa = rounded_mantissa
              }
              let buf = StringBuilder::new()
              if is_neg {
                buf.write_string("-")
              }
              let mantissa_str = format_number_precision(mantissa, precision)
              buf.write_string(mantissa_str)
              buf.write_char('e')
              if exp >= 0 {
                buf.write_char('+')
              }
              buf.write_string(exp.to_string())
              return String_(buf.to_string())
            }
            let decimal_places = precision - exp - 1
            let buf = StringBuilder::new()
            if is_neg {
              buf.write_string("-")
            }
            if decimal_places >= 0 {
              let multiplier = @math.pow(10.0, decimal_places.to_double())
              let shifted = abs_n * multiplier
              let rounded = (shifted + 0.5).floor()
              let int_val = rounded.to_int64()
              let int_str = int_val.to_string()
              let int_len = int_str.length()
              if int_len <= decimal_places {
                buf.write_string("0.")
                for _i = 0; _i < decimal_places - int_len; _i = _i + 1 {
                  buf.write_char('0')
                }
                buf.write_string(int_str)
              } else {
                let int_part_len = int_len - decimal_places
                let chars = int_str.to_array()
                for i = 0; i < int_part_len; i = i + 1 {
                  buf.write_char(chars[i])
                }
                if decimal_places > 0 {
                  buf.write_char('.')
                  for i = int_part_len; i < int_len; i = i + 1 {
                    buf.write_char(chars[i])
                  }
                }
              }
            } else {
              let multiplier = @math.pow(10.0, (-decimal_places).to_double())
              let shifted = abs_n / multiplier
              let rounded = (shifted + 0.5).floor()
              buf.write_string((rounded * multiplier).to_int().to_string())
            }
            String_(buf.to_string())
          }),
        ),
        class_name: "Function",
        descriptors: {},
        symbol_descriptors: {},
        extensible: true,
      })
    "toExponential" =>
      Object({
        properties: {},
        symbol_properties: {},
        prototype: Null,
        callable: Some(
          MethodCallable("toExponential", fn(this_val, args) {
            let n = to_number(this_val)
            if n.is_nan() {
              return String_("NaN")
            }
            if n.is_inf() {
              return String_(if n > 0.0 { "Infinity" } else { "-Infinity" })
            }
            let is_neg = n < 0.0
            let abs_n = n.abs()
            if abs_n == 0.0 {
              let buf = StringBuilder::new()
              if is_neg {
                buf.write_string("-")
              }
              buf.write_char('0')
              if args.length() > 0 {
                match args[0] {
                  Undefined => ()
                  _ => {
                    let frac_digits = to_number(args[0]).to_int()
                    if frac_digits < 0 || frac_digits > 100 {
                      raise @errors.RangeError(
                        message="toExponential() argument must be between 0 and 100",
                      )
                    }
                    if frac_digits > 0 {
                      buf.write_char('.')
                      for _i = 0; _i < frac_digits; _i = _i + 1 {
                        buf.write_char('0')
                      }
                    }
                  }
                }
              }
              buf.write_string("e+0")
              return String_(buf.to_string())
            }
            let log_val = @math.log10(abs_n)
            let mut exp = log_val.floor().to_int()
            let mut mantissa = abs_n / @math.pow(10.0, exp.to_double())
            let buf = StringBuilder::new()
            if is_neg {
              buf.write_string("-")
            }
            if args.length() > 0 {
              match args[0] {
                Undefined => buf.write_string(mantissa.to_string())
                _ => {
                  let frac_digits = to_number(args[0]).to_int()
                  if frac_digits < 0 || frac_digits > 100 {
                    raise @errors.RangeError(
                      message="toExponential() argument must be between 0 and 100",
                    )
                  }
                  // Round mantissa and check for overflow
                  let precision = frac_digits + 1
                  let mult = @math.pow(10.0, (precision - 1).to_double())
                  let rounded_mantissa = (mantissa * mult + 0.5).floor() / mult
                  if rounded_mantissa >= 10.0 {
                    mantissa = rounded_mantissa / 10.0
                    exp = exp + 1
                  } else {
                    mantissa = rounded_mantissa
                  }
                  let mantissa_str = format_number_precision(
                    mantissa, precision,
                  )
                  buf.write_string(mantissa_str)
                }
              }
            } else {
              buf.write_string(mantissa.to_string())
            }
            buf.write_char('e')
            if exp >= 0 {
              buf.write_char('+')
            }
            buf.write_string(exp.to_string())
            String_(buf.to_string())
          }),
        ),
        class_name: "Function",
        descriptors: {},
        symbol_descriptors: {},
        extensible: true,
      })
    _ => Undefined
  }
}

///|
fn format_number_precision(n : Double, precision : Int) -> String {
  // Format a number with exactly `precision` significant digits
  // Used for toPrecision and toExponential
  // Returns mantissa only (e.g., "1.23" for 1.23, NOT including exponent)
  if precision <= 0 {
    return "0"
  }
  let abs_n = n.abs()
  if abs_n == 0.0 {
    let buf = StringBuilder::new()
    buf.write_char('0')
    if precision > 1 {
      buf.write_char('.')
      for _i = 1; _i < precision; _i = _i + 1 {
        buf.write_char('0')
      }
    }
    return buf.to_string()
  }
  // Round to precision significant digits
  let multiplier = @math.pow(10.0, (precision - 1).to_double())
  let shifted = abs_n * multiplier
  let rounded = (shifted + 0.5).floor()
  let int_val = rounded.to_int64()
  let int_str = int_val.to_string()
  let int_chars = int_str.to_array()
  // Handle rounding overflow (e.g., 9.99 with precision 2 rounds to 100)
  // When this happens, int_chars.length() > precision, and we need to truncate
  let actual_chars = if int_chars.length() > precision {
    // Rounding caused a carry, truncate to precision digits
    // The extra digit is always trailing (e.g., "100" -> "10" for precision 2)
    let truncated : Array[Char] = []
    for i = 0; i < precision; i = i + 1 {
      truncated.push(int_chars[i])
    }
    truncated
  } else {
    int_chars
  }
  let buf = StringBuilder::new()
  if actual_chars.length() == 1 {
    buf.write_char(actual_chars[0])
    if precision > 1 {
      buf.write_char('.')
      for _i = 1; _i < precision; _i = _i + 1 {
        buf.write_char('0')
      }
    }
  } else {
    buf.write_char(actual_chars[0])
    if actual_chars.length() > 1 {
      buf.write_char('.')
      for i = 1; i < actual_chars.length(); i = i + 1 {
        buf.write_char(actual_chars[i])
      }
    }
    // Pad with zeros if needed to reach precision
    let digits_so_far = actual_chars.length()
    if digits_so_far < precision {
      for _i = digits_so_far; _i < precision; _i = _i + 1 {
        buf.write_char('0')
      }
    }
  }
  buf.to_string()
}

///|
fn json_escape_string(s : String) -> String {
  let buf = StringBuilder::new()
  buf.write_char('"')
  let chars = s.to_array()
  for c in chars {
    match c {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\t' => buf.write_string("\\t")
      _ =>
        if c.to_int() < 32 {
          buf.write_string("\\u")
          let code = c.to_int()
          let hex_digits = "0123456789abcdef"
          let hd = hex_digits.to_array()
          buf.write_char(hd[(code >> 12) & 0xf])
          buf.write_char(hd[(code >> 8) & 0xf])
          buf.write_char(hd[(code >> 4) & 0xf])
          buf.write_char(hd[code & 0xf])
        } else {
          buf.write_char(c)
        }
    }
  }
  buf.write_char('"')
  buf.to_string()
}

///|
fn setup_function_builtins(env : Environment) -> Unit {
  // Create Function.prototype
  let func_proto_props : Map[String, Value] = {}

  // Function.prototype.toString - returns function source or [native code]
  func_proto_props["toString"] = Object({
    properties: {},
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      MethodCallable("toString", fn(this_val, _args) {
        match this_val {
          Object(data) =>
            match data.callable {
              Some(UserFunc(func_data)) => {
                let name = func_data.name.unwrap_or("")
                String_("function " + name + "() { [native code] }")
              }
              Some(UserFuncExt(func_data)) => {
                let name = func_data.name.unwrap_or("")
                String_("function " + name + "() { [native code] }")
              }
              Some(ArrowFunc(_)) => String_("() => { [native code] }")
              Some(ArrowFuncExt(_)) => String_("() => { [native code] }")
              Some(NativeCallable(name, _)) =>
                String_("function " + name + "() { [native code] }")
              Some(NonConstructableCallable(name, _)) =>
                String_("function " + name + "() { [native code] }")
              Some(BoundFunc(_, _, _)) =>
                String_("function () { [native code] }")
              Some(MethodCallable(name, _)) =>
                String_("function " + name + "() { [native code] }")
              Some(InterpreterCallable(name, _)) =>
                String_("function " + name + "() { [native code] }")
              Some(ConstructorOnlyCallable(name, _)) =>
                String_("function " + name + "() { [native code] }")
              Some(FuncCallMethod(_)) =>
                String_("function call() { [native code] }")
              Some(FuncApplyMethod(_)) =>
                String_("function apply() { [native code] }")
              Some(ClassConstructor(name, _, _, _, _, _)) =>
                String_("class " + name + " { [native code] }")
              None =>
                raise @errors.TypeError(
                  message="Function.prototype.toString requires a function",
                )
            }
          _ =>
            raise @errors.TypeError(
              message="Function.prototype.toString requires a function",
            )
        }
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })

  // Function.prototype.call - placeholder (actual implementation in interpreter)
  func_proto_props["call"] = Object({
    properties: { "length": Number(1.0) },
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      NativeCallable("call", fn(_args) {
        // This is a placeholder - actual call is handled by FuncCallMethod in interpreter
        Undefined
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })

  // Function.prototype.apply - placeholder (actual implementation in interpreter)
  func_proto_props["apply"] = Object({
    properties: { "length": Number(2.0) },
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      NativeCallable("apply", fn(_args) {
        // This is a placeholder - actual apply is handled by FuncApplyMethod in interpreter
        Undefined
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })

  // Function.prototype.bind - placeholder (actual implementation in interpreter)
  func_proto_props["bind"] = Object({
    properties: { "length": Number(1.0) },
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      NativeCallable("bind", fn(_args) {
        // This is a placeholder - actual bind is handled in interpreter
        Undefined
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })
  let func_proto : Value = Object({
    properties: func_proto_props,
    symbol_properties: {},
    prototype: Null,
    callable: None,
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })

  // Function constructor properties
  let func_ctor_props : Map[String, Value] = {}
  func_ctor_props["prototype"] = func_proto
  func_ctor_props["length"] = Number(1.0)

  // Create a single Function constructor object
  let func_ctor : Value = Object({
    properties: func_ctor_props,
    symbol_properties: {},
    prototype: Null,
    callable: Some(
      NativeCallable("Function", fn(args) {
        // Function constructor: creates a new function from string arguments
        // Last arg is body, previous args are parameter names
        // For now, return a no-op function as dynamic code execution is complex
        if args.length() == 0 {
          return Object({
            properties: {},
            symbol_properties: {},
            prototype: func_proto,
            callable: Some(NativeCallable("anonymous", fn(_) { Undefined })),
            class_name: "Function",
            descriptors: {},
            symbol_descriptors: {},
            extensible: true,
          })
        }
        // Create a simple function that returns undefined
        // Full implementation would require parsing the body string
        Object({
          properties: {},
          symbol_properties: {},
          prototype: func_proto,
          callable: Some(NativeCallable("anonymous", fn(_) { Undefined })),
          class_name: "Function",
          descriptors: {},
          symbol_descriptors: {},
          extensible: true,
        })
      }),
    ),
    class_name: "Function",
    descriptors: {},
    symbol_descriptors: {},
    extensible: true,
  })

  // Add constructor back-reference to prototype (same object as global Function)
  match func_proto {
    Object(data) => data.properties["constructor"] = func_ctor
    _ => ()
  }

  // Register the same Function constructor object globally
  env.def_builtin("Function", func_ctor)
}

///|
fn uri_encode(s : String, encode_component : Bool) -> String {
  // Characters that are never encoded
  // For encodeURIComponent: A-Z a-z 0-9 - _ . ! ~ * ' ( )
  // For encodeURI: A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; , / ? : @ & = + $ #
  let buf = StringBuilder::new()
  let chars = s.to_array()
  let hex_digits = "0123456789ABCDEF"
  let hd = hex_digits.to_array()
  for c in chars {
    let code = c.to_int()
    let should_encode = if code >= 65 && code <= 90 {
      // A-Z
      false
    } else if code >= 97 && code <= 122 {
      // a-z
      false
    } else if code >= 48 && code <= 57 {
      // 0-9
      false
    } else if c == '-' ||
      c == '_' ||
      c == '.' ||
      c == '!' ||
      c == '~' ||
      c == '*' ||
      c == '\'' ||
      c == '(' ||
      c == ')' {
      false
    } else if not(encode_component) &&
      (
        c == ';' ||
        c == ',' ||
        c == '/' ||
        c == '?' ||
        c == ':' ||
        c == '@' ||
        c == '&' ||
        c == '=' ||
        c == '+' ||
        c == '$' ||
        c == '#'
      ) {
      false
    } else {
      true
    }
    if should_encode {
      if code <= 0x7F {
        // Single byte
        buf.write_char('%')
        buf.write_char(hd[(code >> 4) & 0xF])
        buf.write_char(hd[code & 0xF])
      } else if code <= 0x7FF {
        // Two bytes
        let b1 = 0xC0 | (code >> 6)
        let b2 = 0x80 | (code & 0x3F)
        buf.write_char('%')
        buf.write_char(hd[(b1 >> 4) & 0xF])
        buf.write_char(hd[b1 & 0xF])
        buf.write_char('%')
        buf.write_char(hd[(b2 >> 4) & 0xF])
        buf.write_char(hd[b2 & 0xF])
      } else if code <= 0xFFFF {
        // Three bytes
        let b1 = 0xE0 | (code >> 12)
        let b2 = 0x80 | ((code >> 6) & 0x3F)
        let b3 = 0x80 | (code & 0x3F)
        buf.write_char('%')
        buf.write_char(hd[(b1 >> 4) & 0xF])
        buf.write_char(hd[b1 & 0xF])
        buf.write_char('%')
        buf.write_char(hd[(b2 >> 4) & 0xF])
        buf.write_char(hd[b2 & 0xF])
        buf.write_char('%')
        buf.write_char(hd[(b3 >> 4) & 0xF])
        buf.write_char(hd[b3 & 0xF])
      } else {
        // Four bytes
        let b1 = 0xF0 | (code >> 18)
        let b2 = 0x80 | ((code >> 12) & 0x3F)
        let b3 = 0x80 | ((code >> 6) & 0x3F)
        let b4 = 0x80 | (code & 0x3F)
        buf.write_char('%')
        buf.write_char(hd[(b1 >> 4) & 0xF])
        buf.write_char(hd[b1 & 0xF])
        buf.write_char('%')
        buf.write_char(hd[(b2 >> 4) & 0xF])
        buf.write_char(hd[b2 & 0xF])
        buf.write_char('%')
        buf.write_char(hd[(b3 >> 4) & 0xF])
        buf.write_char(hd[b3 & 0xF])
        buf.write_char('%')
        buf.write_char(hd[(b4 >> 4) & 0xF])
        buf.write_char(hd[b4 & 0xF])
      }
    } else {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Check if a character is a URI reserved character
fn is_uri_reserved(c : Char) -> Bool {
  // Reserved characters that decodeURI must preserve: ; , / ? : @ & = + $ #
  c == ';' ||
  c == ',' ||
  c == '/' ||
  c == '?' ||
  c == ':' ||
  c == '@' ||
  c == '&' ||
  c == '=' ||
  c == '+' ||
  c == '$' ||
  c == '#'
}

///|
fn uri_decode(s : String, preserve_reserved : Bool) -> String raise Error {
  let buf = StringBuilder::new()
  let chars = s.to_array()
  let len = chars.length()
  let mut i = 0
  while i < len {
    if chars[i] == '%' {
      // Check for valid percent-encoded sequence
      if i + 2 >= len {
        raise @errors.URIError(message="URI malformed")
      }
      let h1 = hex_char_value(chars[i + 1])
      let h2 = hex_char_value(chars[i + 2])
      if h1 < 0 || h2 < 0 {
        raise @errors.URIError(message="URI malformed")
      }
      let byte1 = (h1 << 4) | h2
      // Check for multi-byte UTF-8 sequences
      if byte1 >= 0xF0 {
        // 4-byte sequence
        if i + 11 >= len {
          raise @errors.URIError(message="URI malformed")
        }
        let b2 = decode_percent_byte(chars, i + 3)
        let b3 = decode_percent_byte(chars, i + 6)
        let b4 = decode_percent_byte(chars, i + 9)
        if b2 < 0 || b3 < 0 || b4 < 0 {
          raise @errors.URIError(message="URI malformed")
        }
        // Validate continuation bytes (must be 10xxxxxx)
        if (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80 || (b4 & 0xC0) != 0x80 {
          raise @errors.URIError(message="URI malformed")
        }
        let cp = ((byte1 & 0x07) << 18) |
          ((b2 & 0x3F) << 12) |
          ((b3 & 0x3F) << 6) |
          (b4 & 0x3F)
        buf.write_char(cp.unsafe_to_char())
        i = i + 12
        continue
      } else if byte1 >= 0xE0 {
        // 3-byte sequence
        if i + 8 >= len {
          raise @errors.URIError(message="URI malformed")
        }
        let b2 = decode_percent_byte(chars, i + 3)
        let b3 = decode_percent_byte(chars, i + 6)
        if b2 < 0 || b3 < 0 {
          raise @errors.URIError(message="URI malformed")
        }
        // Validate continuation bytes
        if (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80 {
          raise @errors.URIError(message="URI malformed")
        }
        let cp = ((byte1 & 0x0F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F)
        buf.write_char(cp.unsafe_to_char())
        i = i + 9
        continue
      } else if byte1 >= 0xC0 {
        // 2-byte sequence
        if i + 5 >= len {
          raise @errors.URIError(message="URI malformed")
        }
        let b2 = decode_percent_byte(chars, i + 3)
        if b2 < 0 {
          raise @errors.URIError(message="URI malformed")
        }
        // Validate continuation byte
        if (b2 & 0xC0) != 0x80 {
          raise @errors.URIError(message="URI malformed")
        }
        let cp = ((byte1 & 0x1F) << 6) | (b2 & 0x3F)
        buf.write_char(cp.unsafe_to_char())
        i = i + 6
        continue
      } else if byte1 >= 0x80 {
        // Invalid: continuation byte without lead byte
        raise @errors.URIError(message="URI malformed")
      }
      // Single byte (ASCII)
      let decoded_char = byte1.unsafe_to_char()
      // For decodeURI, preserve reserved characters in encoded form
      if preserve_reserved && is_uri_reserved(decoded_char) {
        buf.write_char(chars[i])
        buf.write_char(chars[i + 1])
        buf.write_char(chars[i + 2])
      } else {
        buf.write_char(decoded_char)
      }
      i = i + 3
      continue
    }
    buf.write_char(chars[i])
    i = i + 1
  }
  buf.to_string()
}

///|
fn hex_char_value(c : Char) -> Int {
  let code = c.to_int()
  if code >= 48 && code <= 57 {
    code - 48
  } else if code >= 65 && code <= 70 {
    code - 55
  } else if code >= 97 && code <= 102 {
    code - 87
  } else {
    -1
  }
}

///|
fn decode_percent_byte(chars : Array[Char], pos : Int) -> Int {
  if chars[pos] == '%' {
    let h1 = hex_char_value(chars[pos + 1])
    let h2 = hex_char_value(chars[pos + 2])
    if h1 >= 0 && h2 >= 0 {
      return (h1 << 4) | h2
    }
  }
  -1
}