///|
pub fn get_string_method(s : String, prop : String) -> Value {
  let chars = s.to_array()
  match prop {
    "charAt" =>
      make_native_func("charAt", fn(args) {
        let idx = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        if idx >= 0 && idx < chars.length() {
          let buf = StringBuilder::new()
          buf.write_char(chars[idx])
          String_(buf.to_string())
        } else {
          String_("")
        }
      })
    "charCodeAt" =>
      make_native_func("charCodeAt", fn(args) {
        let idx = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        if idx >= 0 && idx < chars.length() {
          Number(chars[idx].to_int().to_double())
        } else {
          Number(0.0 / 0.0)
        }
      })
    "at" =>
      make_native_func("at", fn(args) {
        // String.prototype.at() - access character at index (supports negative)
        let idx = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        let len = chars.length()
        let actual_idx = if idx < 0 { len + idx } else { idx }
        if actual_idx >= 0 && actual_idx < len {
          let buf = StringBuilder::new()
          buf.write_char(chars[actual_idx])
          String_(buf.to_string())
        } else {
          Undefined
        }
      })
    "codePointAt" =>
      make_native_func("codePointAt", fn(args) {
        // String.prototype.codePointAt() - get code point at index
        let idx = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        if idx >= 0 && idx < chars.length() {
          let code = chars[idx].to_int()
          // Check for high surrogate
          if code >= 0xD800 && code <= 0xDBFF && idx + 1 < chars.length() {
            let next = chars[idx + 1].to_int()
            // Check for low surrogate
            if next >= 0xDC00 && next <= 0xDFFF {
              // Combine surrogate pair to get code point
              let cp = (code - 0xD800) * 0x400 + (next - 0xDC00) + 0x10000
              return Number(cp.to_double())
            }
          }
          Number(code.to_double())
        } else {
          Undefined
        }
      })
    "concat" => {
      let str = s
      make_native_func("concat", fn(args) {
        let buf = StringBuilder::new()
        buf.write_string(str)
        for arg in args {
          buf.write_string(arg.to_string())
        }
        String_(buf.to_string())
      })
    }
    "indexOf" => {
      let str = s
      make_native_func("indexOf", fn(args) {
        let search = if args.length() > 0 {
          args[0].to_string()
        } else {
          "undefined"
        }
        let from_idx = if args.length() > 1 {
          to_number(args[1]).to_int()
        } else {
          0
        }
        let start = if from_idx < 0 { 0 } else { from_idx }
        let slen = str.length()
        let search_len = search.length()
        if search_len == 0 {
          let empty_result = if start > slen { slen } else { start }
          return Number(empty_result.to_double())
        }
        let s_chars = str.to_array()
        let search_chars = search.to_array()
        for i = start; i <= slen - search_len; i = i + 1 {
          let mut found = true
          for j = 0; j < search_len; j = j + 1 {
            if s_chars[i + j] != search_chars[j] {
              found = false
              break
            }
          }
          if found {
            return Number(i.to_double())
          }
        }
        Number(-1.0)
      })
    }
    "lastIndexOf" => {
      let str = s
      make_native_func("lastIndexOf", fn(args) {
        let search = if args.length() > 0 {
          args[0].to_string()
        } else {
          "undefined"
        }
        let slen = str.length()
        let search_len = search.length()
        let from_idx = if args.length() > 1 {
          to_number(args[1]).to_int()
        } else {
          slen
        }
        let from = if from_idx < 0 {
          0
        } else if from_idx > slen {
          slen
        } else {
          from_idx
        }
        if search_len == 0 {
          return Number(from.to_double())
        }
        let max_start = slen - search_len
        if max_start < 0 {
          return Number(-1.0)
        }
        let start = if from > max_start { max_start } else { from }
        let s_chars = str.to_array()
        let search_chars = search.to_array()
        for i = start; i >= 0; i = i - 1 {
          let mut found = true
          for j = 0; j < search_len; j = j + 1 {
            if i + j >= slen || s_chars[i + j] != search_chars[j] {
              found = false
              break
            }
          }
          if found {
            return Number(i.to_double())
          }
        }
        Number(-1.0)
      })
    }
    "includes" => {
      let str = s
      make_native_func("includes", fn(args) {
        let search = if args.length() > 0 {
          args[0].to_string()
        } else {
          "undefined"
        }
        let from_idx = if args.length() > 1 {
          to_number(args[1]).to_int()
        } else {
          0
        }
        let start = if from_idx < 0 { 0 } else { from_idx }
        let slen = str.length()
        let search_len = search.length()
        if search_len == 0 {
          return Bool(true)
        }
        let s_chars = str.to_array()
        let search_chars = search.to_array()
        for i = start; i <= slen - search_len; i = i + 1 {
          let mut found = true
          for j = 0; j < search_len; j = j + 1 {
            if s_chars[i + j] != search_chars[j] {
              found = false
              break
            }
          }
          if found {
            return Bool(true)
          }
        }
        Bool(false)
      })
    }
    "slice" => {
      let str = s
      make_native_func("slice", fn(args) {
        let slen = str.length()
        let mut start = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        let mut end = if args.length() > 1 {
          match args[1] {
            Undefined => slen
            _ => to_number(args[1]).to_int()
          }
        } else {
          slen
        }
        if start < 0 {
          start = slen + start
          if start < 0 {
            start = 0
          }
        }
        if end < 0 {
          end = slen + end
          if end < 0 {
            end = 0
          }
        }
        if end > slen {
          end = slen
        }
        if start >= end {
          return String_("")
        }
        let s_chars = str.to_array()
        let buf = StringBuilder::new()
        for i = start; i < end; i = i + 1 {
          buf.write_char(s_chars[i])
        }
        String_(buf.to_string())
      })
    }
    "substring" => {
      let str = s
      make_native_func("substring", fn(args) {
        let slen = str.length()
        let mut start = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        let mut end = if args.length() > 1 {
          match args[1] {
            Undefined => slen
            _ => to_number(args[1]).to_int()
          }
        } else {
          slen
        }
        if start < 0 {
          start = 0
        }
        if end < 0 {
          end = 0
        }
        if start > slen {
          start = slen
        }
        if end > slen {
          end = slen
        }
        if start > end {
          let tmp = start
          start = end
          end = tmp
        }
        let s_chars = str.to_array()
        let buf = StringBuilder::new()
        for i = start; i < end; i = i + 1 {
          buf.write_char(s_chars[i])
        }
        String_(buf.to_string())
      })
    }
    "toLowerCase" => {
      let str = s
      make_native_func("toLowerCase", fn(_args) {
        let s_chars = str.to_array()
        let buf = StringBuilder::new()
        for c in s_chars {
          let code = c.to_int()
          let lower = unicode_to_lower(code)
          buf.write_char(lower.unsafe_to_char())
        }
        String_(buf.to_string())
      })
    }
    "toUpperCase" => {
      let str = s
      make_native_func("toUpperCase", fn(_args) {
        let s_chars = str.to_array()
        let buf = StringBuilder::new()
        for c in s_chars {
          let code = c.to_int()
          let upper = unicode_to_upper(code)
          buf.write_char(upper.unsafe_to_char())
        }
        String_(buf.to_string())
      })
    }
    "toLocaleLowerCase" => {
      // Locale-specific lowercase (currently uses same Unicode mapping)
      let str = s
      make_native_func("toLocaleLowerCase", fn(_args) {
        let s_chars = str.to_array()
        let buf = StringBuilder::new()
        for c in s_chars {
          let code = c.to_int()
          let lower = unicode_to_lower(code)
          buf.write_char(lower.unsafe_to_char())
        }
        String_(buf.to_string())
      })
    }
    "toLocaleUpperCase" => {
      // Locale-specific uppercase (currently uses same Unicode mapping)
      let str = s
      make_native_func("toLocaleUpperCase", fn(_args) {
        let s_chars = str.to_array()
        let buf = StringBuilder::new()
        for c in s_chars {
          let code = c.to_int()
          let upper = unicode_to_upper(code)
          buf.write_char(upper.unsafe_to_char())
        }
        String_(buf.to_string())
      })
    }
    "trim" => {
      let str = s
      make_native_func("trim", fn(_args) { String_(string_trim(str)) })
    }
    "trimStart" => {
      let str = s
      make_native_func("trimStart", fn(_args) { String_(trim_start(str)) })
    }
    "trimEnd" => {
      let str = s
      make_native_func("trimEnd", fn(_args) { String_(string_trim_end(str)) })
    }
    "split" => {
      let str = s
      make_native_func("split", fn(args) {
        // Handle undefined separator - return array with whole string
        if args.length() == 0 {
          return Array({ elements: [String_(str)] })
        }
        match args[0] {
          Undefined => return Array({ elements: [String_(str)] })
          _ => ()
        }
        // Get limit parameter (default: 2^32 - 1 per ES spec)
        let limit : Int64 = if args.length() > 1 {
          match args[1] {
            Undefined => 4294967295L // 2^32 - 1
            _ => {
              let lim = to_number(args[1]).to_int64()
              if lim < 0L {
                0L
              } else {
                lim
              }
            }
          }
        } else {
          4294967295L
        }
        // If limit is 0, return empty array
        if limit == 0L {
          return Array({ elements: [] })
        }
        let sep = args[0].to_string()
        // Empty separator splits into individual characters
        if sep.length() == 0 {
          let result : Array[Value] = []
          let chars = str.to_array()
          let chars_len = chars.length().to_int64()
          let count = if chars_len < limit {
            chars.length()
          } else {
            limit.to_int()
          }
          for i = 0; i < count; i = i + 1 {
            let buf = StringBuilder::new()
            buf.write_char(chars[i])
            result.push(String_(buf.to_string()))
          }
          return Array({ elements: result })
        }
        let result : Array[Value] = []
        let s_chars = str.to_array()
        let sep_chars = sep.to_array()
        let slen = str.length()
        let sep_len = sep.length()
        let mut last = 0
        let mut i = 0
        while i <= slen - sep_len {
          let mut matched = true
          for j = 0; j < sep_len; j = j + 1 {
            if s_chars[i + j] != sep_chars[j] {
              matched = false
              break
            }
          }
          if matched {
            let buf = StringBuilder::new()
            for k = last; k < i; k = k + 1 {
              buf.write_char(s_chars[k])
            }
            result.push(String_(buf.to_string()))
            // Per spec: if we've reached the limit, return immediately
            if result.length().to_int64() >= limit {
              return Array({ elements: result })
            }
            last = i + sep_len
            i = i + sep_len
          } else {
            i = i + 1
          }
        }
        // Add remaining part if we haven't hit the limit
        if result.length().to_int64() < limit {
          let buf = StringBuilder::new()
          for k = last; k < slen; k = k + 1 {
            buf.write_char(s_chars[k])
          }
          result.push(String_(buf.to_string()))
        }
        Array({ elements: result })
      })
    }
    "replace" => {
      let str = s
      make_native_func("replace", fn(args) {
        if args.length() == 0 {
          return String_(str)
        }
        // Check if first arg is RegExp
        match args[0] {
          Object(data) =>
            if data.class_name == "RegExp" {
              let pattern = match data.properties.get("source") {
                Some(String_(s)) => s
                _ => ""
              }
              let flags = match data.properties.get("flags") {
                Some(String_(s)) => s
                _ => ""
              }
              let replacement = if args.length() > 1 {
                args[1].to_string()
              } else {
                "undefined"
              }
              return string_replace_regex(str, pattern, flags, replacement)
            }
          _ => ()
        }
        let search = args[0].to_string()
        let replacement = if args.length() > 1 {
          args[1].to_string()
        } else {
          "undefined"
        }
        let slen = str.length()
        let search_len = search.length()
        if search_len == 0 {
          return String_(replacement + str)
        }
        let s_chars = str.to_array()
        let search_chars = search.to_array()
        // Find first occurrence
        for i = 0; i <= slen - search_len; i = i + 1 {
          let mut matched = true
          for j = 0; j < search_len; j = j + 1 {
            if s_chars[i + j] != search_chars[j] {
              matched = false
              break
            }
          }
          if matched {
            let buf = StringBuilder::new()
            for k = 0; k < i; k = k + 1 {
              buf.write_char(s_chars[k])
            }
            buf.write_string(replacement)
            for k = i + search_len; k < slen; k = k + 1 {
              buf.write_char(s_chars[k])
            }
            return String_(buf.to_string())
          }
        }
        String_(str)
      })
    }
    "match" => {
      let str = s
      make_native_func("match", fn(args) {
        if args.length() == 0 {
          return Null
        }
        match args[0] {
          Object(data) =>
            if data.class_name == "RegExp" {
              let pattern = match data.properties.get("source") {
                Some(String_(s)) => s
                _ => ""
              }
              let flags = match data.properties.get("flags") {
                Some(String_(s)) => s
                _ => ""
              }
              return string_match_regex(str, pattern, flags)
            }
          _ => ()
        }
        // If not a RegExp, treat as string pattern
        let pattern = args[0].to_string()
        string_match_regex(str, pattern, "")
      })
    }
    "search" => {
      let str = s
      make_native_func("search", fn(args) {
        if args.length() == 0 {
          return Number(0.0)
        }
        match args[0] {
          Object(data) =>
            if data.class_name == "RegExp" {
              let pattern = match data.properties.get("source") {
                Some(String_(s)) => s
                _ => ""
              }
              let flags = match data.properties.get("flags") {
                Some(String_(s)) => s
                _ => ""
              }
              return string_search_regex(str, pattern, flags)
            }
          _ => ()
        }
        let pattern = args[0].to_string()
        string_search_regex(str, pattern, "")
      })
    }
    "startsWith" => {
      let str = s
      make_native_func("startsWith", fn(args) {
        let search = if args.length() > 0 {
          args[0].to_string()
        } else {
          "undefined"
        }
        let raw_pos = if args.length() > 1 {
          to_number(args[1]).to_int()
        } else {
          0
        }
        let slen = str.length()
        let pos = if raw_pos < 0 {
          0
        } else if raw_pos > slen {
          slen
        } else {
          raw_pos
        }
        let search_chars = search.to_array()
        let s_chars = str.to_array()
        if pos + search_chars.length() > s_chars.length() {
          return Bool(false)
        }
        for i = 0; i < search_chars.length(); i = i + 1 {
          if s_chars[pos + i] != search_chars[i] {
            return Bool(false)
          }
        }
        Bool(true)
      })
    }
    "endsWith" => {
      let str = s
      make_native_func("endsWith", fn(args) {
        let search = if args.length() > 0 {
          args[0].to_string()
        } else {
          "undefined"
        }
        let slen = str.length()
        let raw_end = if args.length() > 1 {
          to_number(args[1]).to_int()
        } else {
          slen
        }
        let end_pos = if raw_end < 0 {
          0
        } else if raw_end > slen {
          slen
        } else {
          raw_end
        }
        let search_len = search.length()
        let start = end_pos - search_len
        if start < 0 {
          return Bool(false)
        }
        let s_chars = str.to_array()
        let search_chars = search.to_array()
        for i = 0; i < search_len; i = i + 1 {
          if s_chars[start + i] != search_chars[i] {
            return Bool(false)
          }
        }
        Bool(true)
      })
    }
    "repeat" => {
      let str = s
      make_native_func("repeat", fn(args) {
        let count_num = if args.length() > 0 {
          to_number(args[0])
        } else {
          0.0
        }
        // Check for negative or infinite count
        if count_num < 0.0 || count_num.is_inf() {
          raise @errors.RangeError(message="Invalid repeat count")
        }
        let count = count_num.to_int()
        if count == 0 {
          return String_("")
        }
        let buf = StringBuilder::new()
        for _i = 0; _i < count; _i = _i + 1 {
          buf.write_string(str)
        }
        String_(buf.to_string())
      })
    }
    "padStart" => {
      let str = s
      make_native_func("padStart", fn(args) {
        let target_len = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        let pad_str = if args.length() > 1 { args[1].to_string() } else { " " }
        let slen = str.length()
        if slen >= target_len || pad_str.length() == 0 {
          return String_(str)
        }
        let buf = StringBuilder::new()
        let pad_chars = pad_str.to_array()
        let mut filled = 0
        let needed = target_len - slen
        while filled < needed {
          buf.write_char(pad_chars[filled % pad_chars.length()])
          filled = filled + 1
        }
        buf.write_string(str)
        String_(buf.to_string())
      })
    }
    "padEnd" => {
      let str = s
      make_native_func("padEnd", fn(args) {
        let target_len = if args.length() > 0 {
          to_number(args[0]).to_int()
        } else {
          0
        }
        let pad_str = if args.length() > 1 { args[1].to_string() } else { " " }
        let slen = str.length()
        if slen >= target_len || pad_str.length() == 0 {
          return String_(str)
        }
        let buf = StringBuilder::new()
        buf.write_string(str)
        let pad_chars = pad_str.to_array()
        let mut filled = 0
        let needed = target_len - slen
        while filled < needed {
          buf.write_char(pad_chars[filled % pad_chars.length()])
          filled = filled + 1
        }
        String_(buf.to_string())
      })
    }
    "toString" | "valueOf" => {
      let str = s
      make_native_func(prop, fn(_args) { String_(str) })
    }
    "replaceAll" => {
      let str = s
      make_native_func("replaceAll", fn(args) {
        if args.length() == 0 {
          return String_(str)
        }
        // Check if first arg is RegExp
        match args[0] {
          Object(data) =>
            if data.class_name == "RegExp" {
              // RegExp must have global flag for replaceAll
              let flags = match data.properties.get("flags") {
                Some(String_(f)) => f
                _ => ""
              }
              if not(flags.to_array().contains('g')) {
                raise @errors.TypeError(
                  message="replaceAll must be called with a global RegExp",
                )
              }
              let pattern = match data.properties.get("source") {
                Some(String_(s)) => s
                _ => ""
              }
              let replacement = if args.length() > 1 {
                args[1].to_string()
              } else {
                "undefined"
              }
              return string_replace_regex(str, pattern, flags, replacement)
            }
          _ => ()
        }
        let search = args[0].to_string()
        let replacement = if args.length() > 1 {
          args[1].to_string()
        } else {
          "undefined"
        }
        let slen = str.length()
        let search_len = search.length()
        if search_len == 0 {
          // Empty string: insert replacement before each char and at end
          let buf = StringBuilder::new()
          let s_chars = str.to_array()
          for c in s_chars {
            buf.write_string(replacement)
            buf.write_char(c)
          }
          buf.write_string(replacement)
          return String_(buf.to_string())
        }
        let s_chars = str.to_array()
        let search_chars = search.to_array()
        let buf = StringBuilder::new()
        let mut i = 0
        while i < slen {
          if i <= slen - search_len {
            let mut matched = true
            for j = 0; j < search_len; j = j + 1 {
              if s_chars[i + j] != search_chars[j] {
                matched = false
                break
              }
            }
            if matched {
              buf.write_string(replacement)
              i = i + search_len
              continue
            }
          }
          buf.write_char(s_chars[i])
          i = i + 1
        }
        String_(buf.to_string())
      })
    }
    "localeCompare" => {
      let str = s
      make_native_func("localeCompare", fn(args) {
        // Basic lexicographic comparison (no locale support)
        let other = if args.length() > 0 {
          args[0].to_string()
        } else {
          "undefined"
        }
        if str < other {
          Number(-1.0)
        } else if str > other {
          Number(1.0)
        } else {
          Number(0.0)
        }
      })
    }
    "normalize" => {
      let str = s
      make_native_func("normalize", fn(_args) {
        // STUB: Unicode normalization (NFC, NFD, NFKC, NFKD) is not implemented.
        // Returns the input string unchanged. Full implementation requires
        // Unicode normalization tables and algorithms.
        String_(str)
      })
    }
    "isWellFormed" => {
      let str = s
      make_native_func("isWellFormed", fn(_args) {
        // Check if string is well-formed UTF-16 (no lone surrogates)
        let c = str.to_array()
        let mut i = 0
        while i < c.length() {
          let code = c[i].to_int()
          if code >= 0xD800 && code <= 0xDBFF {
            // High surrogate - must be followed by low surrogate
            if i + 1 >= c.length() {
              return Bool(false)
            }
            let next = c[i + 1].to_int()
            if next < 0xDC00 || next > 0xDFFF {
              return Bool(false)
            }
            // Valid surrogate pair - skip the low surrogate
            i = i + 2
            continue
          } else if code >= 0xDC00 && code <= 0xDFFF {
            // Lone low surrogate
            return Bool(false)
          }
          i = i + 1
        }
        Bool(true)
      })
    }
    "toWellFormed" => {
      let str = s
      make_native_func("toWellFormed", fn(_args) {
        // Replace lone surrogates with U+FFFD
        let c = str.to_array()
        let buf = StringBuilder::new()
        let mut i = 0
        while i < c.length() {
          let code = c[i].to_int()
          if code >= 0xD800 && code <= 0xDBFF {
            if i + 1 < c.length() {
              let next = c[i + 1].to_int()
              if next >= 0xDC00 && next <= 0xDFFF {
                // Valid surrogate pair - write both and skip ahead
                buf.write_char(c[i])
                buf.write_char(c[i + 1])
                i = i + 2
                continue
              } else {
                buf.write_char('\u{FFFD}')
              }
            } else {
              buf.write_char('\u{FFFD}')
            }
          } else if code >= 0xDC00 && code <= 0xDFFF {
            buf.write_char('\u{FFFD}')
          } else {
            buf.write_char(c[i])
          }
          i = i + 1
        }
        String_(buf.to_string())
      })
    }
    _ => Undefined
  }
}

///| Unicode-aware character to lowercase conversion
fn unicode_to_lower(code : Int) -> Int {
  // ASCII uppercase (A-Z)
  if code >= 65 && code <= 90 {
    return code + 32
  }
  // Latin-1 Supplement (U+00C0-U+00DE, excluding U+00D7 multiplication sign)
  if code >= 0xC0 && code <= 0xDE && code != 0xD7 {
    return code + 32
  }
  // Latin Extended-A (U+0100-U+017F) - even codes are uppercase
  if code >= 0x100 && code <= 0x12E {
    if (code & 1) == 0 {
      return code + 1
    }
  }
  if code >= 0x132 && code <= 0x136 {
    if (code & 1) == 0 {
      return code + 1
    }
  }
  if code >= 0x139 && code <= 0x147 {
    if (code & 1) == 1 {
      return code + 1
    }
  }
  if code >= 0x14A && code <= 0x176 {
    if (code & 1) == 0 {
      return code + 1
    }
  }
  // Greek uppercase (U+0391-U+03A9, U+03AA-U+03AB)
  if code >= 0x391 && code <= 0x3A9 {
    return code + 32
  }
  if code >= 0x3AA && code <= 0x3AB {
    return code + 32
  }
  // Cyrillic uppercase (U+0410-U+042F)
  if code >= 0x410 && code <= 0x42F {
    return code + 32
  }
  // Special cases
  match code {
    0x178 => 0xFF // Ÿ -> ÿ
    0x179 => 0x17A // Ź -> ź
    0x17B => 0x17C // Ż -> ż
    0x17D => 0x17E // Ž -> ž
    0x181 => 0x253 // Ɓ -> ɓ
    0x186 => 0x254 // Ɔ -> ɔ
    0x189 => 0x256 // Ɖ -> ɖ
    0x18A => 0x257 // Ɗ -> ɗ
    0x18E => 0x1DD // Ǝ -> ǝ
    0x18F => 0x259 // Ə -> ə
    0x190 => 0x25B // Ɛ -> ɛ
    0x193 => 0x260 // Ɠ -> ɠ
    0x194 => 0x263 // Ɣ -> ɣ
    0x197 => 0x268 // Ɨ -> ɨ
    0x196 => 0x269 // Ɩ -> ɩ
    0x19C => 0x26F // Ɯ -> ɯ
    0x19D => 0x272 // Ɲ -> ɲ
    0x19F => 0x275 // Ɵ -> ɵ
    _ => code
  }
}

///| Unicode-aware character to uppercase conversion
fn unicode_to_upper(code : Int) -> Int {
  // ASCII lowercase (a-z)
  if code >= 97 && code <= 122 {
    return code - 32
  }
  // Latin-1 Supplement lowercase (U+00E0-U+00FE, excluding U+00F7 division sign)
  if code >= 0xE0 && code <= 0xFE && code != 0xF7 {
    return code - 32
  }
  // Latin Extended-A (U+0100-U+017F) - odd codes are lowercase
  if code >= 0x101 && code <= 0x12F {
    if (code & 1) == 1 {
      return code - 1
    }
  }
  if code >= 0x133 && code <= 0x137 {
    if (code & 1) == 1 {
      return code - 1
    }
  }
  if code >= 0x13A && code <= 0x148 {
    if (code & 1) == 0 {
      return code - 1
    }
  }
  if code >= 0x14B && code <= 0x177 {
    if (code & 1) == 1 {
      return code - 1
    }
  }
  // Greek lowercase (U+03B1-U+03C9, U+03CA-U+03CB)
  if code >= 0x3B1 && code <= 0x3C9 {
    return code - 32
  }
  if code >= 0x3CA && code <= 0x3CB {
    return code - 32
  }
  // Cyrillic lowercase (U+0430-U+044F)
  if code >= 0x430 && code <= 0x44F {
    return code - 32
  }
  // Special cases
  match code {
    0xFF => 0x178 // ÿ -> Ÿ
    0x17A => 0x179 // ź -> Ź
    0x17C => 0x17B // ż -> Ż
    0x17E => 0x17D // ž -> Ž
    0x253 => 0x181 // ɓ -> Ɓ
    0x254 => 0x186 // ɔ -> Ɔ
    0x256 => 0x189 // ɖ -> Ɖ
    0x257 => 0x18A // ɗ -> Ɗ
    0x1DD => 0x18E // ǝ -> Ǝ
    0x259 => 0x18F // ə -> Ə
    0x25B => 0x190 // ɛ -> Ɛ
    0x260 => 0x193 // ɠ -> Ɠ
    0x263 => 0x194 // ɣ -> Ɣ
    0x268 => 0x197 // ɨ -> Ɨ
    0x269 => 0x196 // ɩ -> Ɩ
    0x26F => 0x19C // ɯ -> Ɯ
    0x272 => 0x19D // ɲ -> Ɲ
    0x275 => 0x19F // ɵ -> Ɵ
    _ => code
  }
}

///|
fn string_trim(s : String) -> String {
  let trimmed_left = trim_start(s)
  string_trim_end(trimmed_left)
}

///|
/// Extract string from a this value (for String.prototype methods called via .call())
pub fn this_to_string(this_val : Value) -> String raise Error {
  match this_val {
    String_(s) => s
    Number(n) => n.to_string()
    Bool(b) => if b { "true" } else { "false" }
    Null => raise @errors.TypeError(message="String.prototype method called on null")
    Undefined =>
      raise @errors.TypeError(
        message="String.prototype method called on undefined",
      )
    Symbol(_) =>
      raise @errors.TypeError(
        message="Cannot convert a Symbol value to a string",
      )
    Object(data) =>
      // Check if it's a String wrapper object
      if data.class_name == "String" {
        match data.properties.get("__primitiveValue__") {
          Some(String_(s)) => s
          _ => this_val.to_string()
        }
      } else {
        this_val.to_string()
      }
    _ => this_val.to_string()
  }
}

///|
/// Populate String.prototype with all string methods
pub fn setup_string_prototype(string_proto : Value) -> Unit {
  match string_proto {
    Object(data) => {
      let methods = [
        "charAt", "charCodeAt", "at", "codePointAt", "indexOf", "lastIndexOf",
        "includes", "startsWith", "endsWith", "slice", "substring", "toUpperCase",
        "toLowerCase", "trim", "trimStart", "trimEnd", "padStart", "padEnd",
        "repeat", "replace", "replaceAll", "split", "search", "match", "concat",
        "normalize", "isWellFormed", "toWellFormed", "localeCompare", "toString",
        "valueOf",
      ]
      for method_name in methods {
        let mname = method_name
        data.properties[mname] = Object({
          properties: {},
          symbol_properties: {},
          prototype: Null,
          callable: Some(
            MethodCallable(mname, fn(this_val, args) {
              let s = this_to_string(this_val)
              let meth_val = get_string_method(s, mname)
              match meth_val {
                Object(fdata) =>
                  match fdata.callable {
                    Some(NativeCallable(_, f)) => f(args)
                    _ => Undefined
                  }
                _ => Undefined
              }
            }),
          ),
          class_name: "Function",
          descriptors: {},
          symbol_descriptors: {},
          extensible: true,
        })
        // Make methods non-enumerable
        data.descriptors[mname] = {
          writable: true,
          enumerable: false,
          configurable: true,
        }
      }
      // Add constructor back-reference placeholder
      data.properties["constructor"] = Undefined
    }
    _ => ()
  }
}

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