///|
fn string_methods() -> Map[
  String,
  (String, Array[@value.Value], Map[String, @value.Value], @ast.Span) -> @value.Value raise StarlarkError,
] {
  let m : Map[
    String,
    (String, Array[@value.Value], Map[String, @value.Value], @ast.Span) -> @value.Value raise StarlarkError,
  ] = {}
  m["capitalize"] = string_capitalize
  m["count"] = string_count
  m["endswith"] = string_endswith
  m["startswith"] = string_startswith
  m["find"] = string_find
  m["rfind"] = string_rfind
  m["index"] = string_index
  m["rindex"] = string_rindex
  m["format"] = string_format
  m["join"] = string_join
  m["split"] = string_split
  m["rsplit"] = string_rsplit
  m["lower"] = string_lower
  m["upper"] = string_upper
  m["title"] = string_title
  m["strip"] = string_strip
  m["lstrip"] = string_lstrip
  m["rstrip"] = string_rstrip
  m["replace"] = string_replace
  m["removeprefix"] = string_removeprefix
  m["removesuffix"] = string_removesuffix
  m["partition"] = string_partition
  m["rpartition"] = string_rpartition
  m["splitlines"] = string_splitlines
  m["isalpha"] = string_isalpha
  m["isdigit"] = string_isdigit
  m["isalnum"] = string_isalnum
  m["islower"] = string_islower
  m["isupper"] = string_isupper
  m["isspace"] = string_isspace
  m["istitle"] = string_istitle
  m["elems"] = string_elems
  m
}

///|
fn string_capitalize(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="capitalize() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  if s.length() == 0 {
    return @value.Value::String("")
  }
  let buf = StringBuilder::new()
  buf.write_char(code_to_upper(s[0].to_int()).unsafe_to_char())
  for i = 1; i < s.length(); i = i + 1 {
    buf.write_char(code_to_lower(s[i].to_int()).unsafe_to_char())
  }
  @value.Value::String(buf.to_string())
}

///|
fn string_count(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 3 else {
    raise StarlarkError::TypeError(
      message="count() takes at least 1 and at most 3 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let sub = expect_string(pos[0], "count", span)
  let start = if pos.length() >= 2 {
    clamp_index(expect_int(pos[1], "count", span), s.length())
  } else {
    0
  }
  let end = if pos.length() >= 3 {
    clamp_index(expect_int(pos[2], "count", span), s.length())
  } else {
    s.length()
  }
  let count = str_count(s, sub, start, end)
  @value.Value::Int(count.to_int64())
}

///|
fn string_endswith(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="endswith() takes exactly 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let suffix = expect_string(pos[0], "endswith", span)
  @value.Value::Bool(str_endswith(s, suffix))
}

///|
fn string_startswith(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="startswith() takes exactly 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let prefix = expect_string(pos[0], "startswith", span)
  @value.Value::Bool(str_startswith(s, prefix))
}

///|
fn string_find(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 3 else {
    raise StarlarkError::TypeError(
      message="find() takes at least 1 and at most 3 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let sub = expect_string(pos[0], "find", span)
  let start = if pos.length() >= 2 {
    clamp_index(expect_int(pos[1], "find", span), s.length())
  } else {
    0
  }
  let end = if pos.length() >= 3 {
    clamp_index(expect_int(pos[2], "find", span), s.length())
  } else {
    s.length()
  }
  let idx = str_find(s, sub, start, end)
  @value.Value::Int(idx.to_int64())
}

///|
fn string_rfind(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 3 else {
    raise StarlarkError::TypeError(
      message="rfind() takes at least 1 and at most 3 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let sub = expect_string(pos[0], "rfind", span)
  let start = if pos.length() >= 2 {
    clamp_index(expect_int(pos[1], "rfind", span), s.length())
  } else {
    0
  }
  let end = if pos.length() >= 3 {
    clamp_index(expect_int(pos[2], "rfind", span), s.length())
  } else {
    s.length()
  }
  let idx = str_rfind(s, sub, start, end)
  @value.Value::Int(idx.to_int64())
}

///|
fn string_index(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 3 else {
    raise StarlarkError::TypeError(
      message="index() takes at least 1 and at most 3 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let sub = expect_string(pos[0], "index", span)
  let start = if pos.length() >= 2 {
    clamp_index(expect_int(pos[1], "index", span), s.length())
  } else {
    0
  }
  let end = if pos.length() >= 3 {
    clamp_index(expect_int(pos[2], "index", span), s.length())
  } else {
    s.length()
  }
  let idx = str_find(s, sub, start, end)
  if idx < 0 {
    raise StarlarkError::ValueError(message="substring not found", span~)
  }
  @value.Value::Int(idx.to_int64())
}

///|
fn string_rindex(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 3 else {
    raise StarlarkError::TypeError(
      message="rindex() takes at least 1 and at most 3 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let sub = expect_string(pos[0], "rindex", span)
  let start = if pos.length() >= 2 {
    clamp_index(expect_int(pos[1], "rindex", span), s.length())
  } else {
    0
  }
  let end = if pos.length() >= 3 {
    clamp_index(expect_int(pos[2], "rindex", span), s.length())
  } else {
    s.length()
  }
  let idx = str_rfind(s, sub, start, end)
  if idx < 0 {
    raise StarlarkError::ValueError(message="substring not found", span~)
  }
  @value.Value::Int(idx.to_int64())
}

///|
fn string_format(
  s : String,
  pos : Array[@value.Value],
  kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  // format supports {}, {0}, {1}, and {name} placeholders
  let buf = StringBuilder::new()
  let mut auto_idx = 0
  let mut i = 0
  while i < s.length() {
    let code = s[i].to_int()
    if code == '{'.to_int() {
      if i + 1 < s.length() && s[i + 1].to_int() == '{'.to_int() {
        buf.write_char('{')
        i = i + 2
        continue
      }
      // Find matching }
      let start_idx = i + 1
      let mut end_idx = start_idx
      while end_idx < s.length() && s[end_idx].to_int() != '}'.to_int() {
        end_idx = end_idx + 1
      }
      if end_idx >= s.length() {
        raise StarlarkError::ValueError(
          message="Single '{' encountered in format string",
          span~,
        )
      }
      let field = substr(s, start_idx, end_idx)
      if field == "" {
        // Auto-numbering
        if auto_idx >= pos.length() {
          raise StarlarkError::IndexError(
            message="Replacement index \{auto_idx} out of range for positional args tuple",
            span~,
          )
        }
        buf.write_string(value_to_str(pos[auto_idx]))
        auto_idx = auto_idx + 1
      } else if is_all_digits(field) {
        let idx = parse_simple_int(field)
        if idx >= pos.length() {
          raise StarlarkError::IndexError(
            message="Replacement index \{idx} out of range for positional args tuple",
            span~,
          )
        }
        buf.write_string(value_to_str(pos[idx]))
      } else {
        // Named field
        match kw.get(field) {
          Some(v) => buf.write_string(value_to_str(v))
          None => raise StarlarkError::KeyError(key=field, span~)
        }
      }
      i = end_idx + 1
    } else if code == '}'.to_int() {
      if i + 1 < s.length() && s[i + 1].to_int() == '}'.to_int() {
        buf.write_char('}')
        i = i + 2
        continue
      }
      raise StarlarkError::ValueError(
        message="Single '}' encountered in format string",
        span~,
      )
    } else {
      buf.write_char(code.unsafe_to_char())
      i = i + 1
    }
  }
  @value.Value::String(buf.to_string())
}

///|
fn string_join(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="join() takes exactly 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let items = iterable_to_array(pos[0], span)
  let parts : Array[String] = []
  for item in items {
    match item {
      String(str) => parts.push(str)
      _ =>
        raise StarlarkError::TypeError(
          message="sequence item: expected str instance, got '\{item.type_name()}'",
          span~,
        )
    }
  }
  @value.Value::String(parts.join(s))
}

///|
fn string_split(
  s : String,
  pos : Array[@value.Value],
  kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() <= 2 else {
    raise StarlarkError::TypeError(
      message="split() takes at most 2 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let sep : String? = if pos.length() >= 1 {
    match pos[0] {
      None => None
      String(str) => Some(str)
      _ =>
        raise StarlarkError::TypeError(
          message="split() argument must be str or None, not '\{pos[0].type_name()}'",
          span~,
        )
    }
  } else {
    match kw.get("sep") {
      Some(String(str)) => Some(str)
      Some(None) => None
      Some(other) =>
        raise StarlarkError::TypeError(
          message="split() argument must be str or None, not '\{other.type_name()}'",
          span~,
        )
      None => None
    }
  }
  let maxsplit : Int = if pos.length() >= 2 {
    expect_int(pos[1], "split", span)
  } else {
    match kw.get("maxsplit") {
      Some(Int(n)) => n.to_int()
      Some(_) => -1
      None => -1
    }
  }
  let parts = str_split(s, sep, maxsplit)
  let result : Array[@value.Value] = []
  for part in parts {
    result.push(@value.Value::String(part))
  }
  @value.Value::List(Ref::new(result))
}

///|
fn string_rsplit(
  s : String,
  pos : Array[@value.Value],
  kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() <= 2 else {
    raise StarlarkError::TypeError(
      message="rsplit() takes at most 2 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let sep : String? = if pos.length() >= 1 {
    match pos[0] {
      None => None
      String(str) => Some(str)
      _ =>
        raise StarlarkError::TypeError(
          message="rsplit() argument must be str or None, not '\{pos[0].type_name()}'",
          span~,
        )
    }
  } else {
    None
  }
  let maxsplit : Int = if pos.length() >= 2 {
    expect_int(pos[1], "rsplit", span)
  } else {
    match kw.get("maxsplit") {
      Some(Int(n)) => n.to_int()
      Some(_) => -1
      None => -1
    }
  }
  let parts = str_rsplit(s, sep, maxsplit)
  let result : Array[@value.Value] = []
  for part in parts {
    result.push(@value.Value::String(part))
  }
  @value.Value::List(Ref::new(result))
}

///|
fn string_lower(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="lower() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let buf = StringBuilder::new()
  for i = 0; i < s.length(); i = i + 1 {
    buf.write_char(code_to_lower(s[i].to_int()).unsafe_to_char())
  }
  @value.Value::String(buf.to_string())
}

///|
fn string_upper(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="upper() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let buf = StringBuilder::new()
  for i = 0; i < s.length(); i = i + 1 {
    buf.write_char(code_to_upper(s[i].to_int()).unsafe_to_char())
  }
  @value.Value::String(buf.to_string())
}

///|
fn string_title(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="title() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let buf = StringBuilder::new()
  let mut prev_is_letter = false
  for i = 0; i < s.length(); i = i + 1 {
    let code = s[i].to_int()
    if is_alpha_code(code) {
      if not(prev_is_letter) {
        buf.write_char(code_to_upper(code).unsafe_to_char())
      } else {
        buf.write_char(code_to_lower(code).unsafe_to_char())
      }
      prev_is_letter = true
    } else {
      buf.write_char(code.unsafe_to_char())
      prev_is_letter = false
    }
  }
  @value.Value::String(buf.to_string())
}

///|
fn string_strip(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() <= 1 else {
    raise StarlarkError::TypeError(
      message="strip() takes at most 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let chars : String? = if pos.length() == 1 {
    match pos[0] {
      None => None
      String(str) => Some(str)
      _ =>
        raise StarlarkError::TypeError(
          message="strip() argument must be str or None, not '\{pos[0].type_name()}'",
          span~,
        )
    }
  } else {
    None
  }
  @value.Value::String(str_strip(s, chars))
}

///|
fn string_lstrip(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() <= 1 else {
    raise StarlarkError::TypeError(
      message="lstrip() takes at most 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let chars : String? = if pos.length() == 1 {
    match pos[0] {
      None => None
      String(str) => Some(str)
      _ =>
        raise StarlarkError::TypeError(
          message="lstrip() argument must be str or None, not '\{pos[0].type_name()}'",
          span~,
        )
    }
  } else {
    None
  }
  @value.Value::String(str_lstrip(s, chars))
}

///|
fn string_rstrip(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() <= 1 else {
    raise StarlarkError::TypeError(
      message="rstrip() takes at most 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let chars : String? = if pos.length() == 1 {
    match pos[0] {
      None => None
      String(str) => Some(str)
      _ =>
        raise StarlarkError::TypeError(
          message="rstrip() argument must be str or None, not '\{pos[0].type_name()}'",
          span~,
        )
    }
  } else {
    None
  }
  @value.Value::String(str_rstrip(s, chars))
}

///|
fn string_replace(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 2 && pos.length() <= 3 else {
    raise StarlarkError::TypeError(
      message="replace() takes at least 2 and at most 3 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let old = expect_string(pos[0], "replace", span)
  let new_str = expect_string(pos[1], "replace", span)
  let count : Int = if pos.length() >= 3 {
    expect_int(pos[2], "replace", span)
  } else {
    -1
  }
  @value.Value::String(str_replace(s, old, new_str, count))
}

///|
fn string_removeprefix(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="removeprefix() takes exactly 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let prefix = expect_string(pos[0], "removeprefix", span)
  if str_startswith(s, prefix) {
    @value.Value::String(substr(s, prefix.length(), s.length()))
  } else {
    @value.Value::String(s)
  }
}

///|
fn string_removesuffix(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="removesuffix() takes exactly 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let suffix = expect_string(pos[0], "removesuffix", span)
  if suffix.length() > 0 && str_endswith(s, suffix) {
    @value.Value::String(substr(s, 0, s.length() - suffix.length()))
  } else {
    @value.Value::String(s)
  }
}

///|
fn string_partition(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="partition() takes exactly 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let sep = expect_string(pos[0], "partition", span)
  if sep == "" {
    raise StarlarkError::ValueError(message="empty separator", span~)
  }
  let idx = str_find(s, sep, 0, s.length())
  if idx >= 0 {
    @value.Value::Tuple([
      @value.Value::String(substr(s, 0, idx)),
      @value.Value::String(sep),
      @value.Value::String(substr(s, idx + sep.length(), s.length())),
    ])
  } else {
    @value.Value::Tuple([
      @value.Value::String(s),
      @value.Value::String(""),
      @value.Value::String(""),
    ])
  }
}

///|
fn string_rpartition(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="rpartition() takes exactly 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let sep = expect_string(pos[0], "rpartition", span)
  if sep == "" {
    raise StarlarkError::ValueError(message="empty separator", span~)
  }
  let idx = str_rfind(s, sep, 0, s.length())
  if idx >= 0 {
    @value.Value::Tuple([
      @value.Value::String(substr(s, 0, idx)),
      @value.Value::String(sep),
      @value.Value::String(substr(s, idx + sep.length(), s.length())),
    ])
  } else {
    @value.Value::Tuple([
      @value.Value::String(""),
      @value.Value::String(""),
      @value.Value::String(s),
    ])
  }
}

///|
fn string_splitlines(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() <= 1 else {
    raise StarlarkError::TypeError(
      message="splitlines() takes at most 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let keepends = if pos.length() == 1 { pos[0].is_truthy() } else { false }
  let lines = str_splitlines(s, keepends)
  let result : Array[@value.Value] = []
  for line in lines {
    result.push(@value.Value::String(line))
  }
  @value.Value::List(Ref::new(result))
}

///|
fn string_isalpha(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="isalpha() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  @value.Value::Bool(s.length() > 0 && str_all_codes(s, is_alpha_code))
}

///|
fn string_isdigit(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="isdigit() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  @value.Value::Bool(s.length() > 0 && str_all_codes(s, is_digit_code))
}

///|
fn string_isalnum(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="isalnum() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  @value.Value::Bool(s.length() > 0 && str_all_codes(s, is_alnum_code))
}

///|
fn string_islower(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="islower() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let mut has_cased = false
  let mut all_lower = true
  for i = 0; i < s.length(); i = i + 1 {
    let code = s[i].to_int()
    if is_upper_code(code) {
      all_lower = false
      break
    }
    if is_lower_code(code) {
      has_cased = true
    }
  }
  @value.Value::Bool(has_cased && all_lower)
}

///|
fn string_isupper(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="isupper() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let mut has_cased = false
  let mut all_upper = true
  for i = 0; i < s.length(); i = i + 1 {
    let code = s[i].to_int()
    if is_lower_code(code) {
      all_upper = false
      break
    }
    if is_upper_code(code) {
      has_cased = true
    }
  }
  @value.Value::Bool(has_cased && all_upper)
}

///|
fn string_isspace(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="isspace() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  @value.Value::Bool(s.length() > 0 && str_all_codes(s, is_space_code))
}

///|
fn string_istitle(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="istitle() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  @value.Value::Bool(str_is_title(s))
}

///|
fn string_elems(
  s : String,
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="elems() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let result : Array[@value.Value] = []
  for i = 0; i < s.length(); i = i + 1 {
    result.push(@value.Value::String(string_char_at(s, i)))
  }
  @value.Value::List(Ref::new(result))
}

// ===== Char code point helper functions =====
// All work with Int (code point) to avoid UInt16/Char type issues

///|
fn code_to_upper(code : Int) -> Int {
  if code >= 'a'.to_int() && code <= 'z'.to_int() {
    code - 32
  } else {
    code
  }
}

///|
fn code_to_lower(code : Int) -> Int {
  if code >= 'A'.to_int() && code <= 'Z'.to_int() {
    code + 32
  } else {
    code
  }
}

///|
fn is_alpha_code(code : Int) -> Bool {
  (code >= 'a'.to_int() && code <= 'z'.to_int()) ||
  (code >= 'A'.to_int() && code <= 'Z'.to_int())
}

///|
fn is_digit_code(code : Int) -> Bool {
  code >= '0'.to_int() && code <= '9'.to_int()
}

///|
fn is_alnum_code(code : Int) -> Bool {
  is_alpha_code(code) || is_digit_code(code)
}

///|
fn is_upper_code(code : Int) -> Bool {
  code >= 'A'.to_int() && code <= 'Z'.to_int()
}

///|
fn is_lower_code(code : Int) -> Bool {
  code >= 'a'.to_int() && code <= 'z'.to_int()
}

///|
fn is_space_code(code : Int) -> Bool {
  code == ' '.to_int() ||
  code == '\t'.to_int() ||
  code == '\n'.to_int() ||
  code == '\r'.to_int() ||
  code == 0x0b || // vertical tab
  code == 0x0c // form feed
}

///|
fn str_all_codes(s : String, pred : (Int) -> Bool) -> Bool {
  for i = 0; i < s.length(); i = i + 1 {
    if not(pred(s[i].to_int())) {
      return false
    }
  }
  true
}

///|
fn str_is_title(s : String) -> Bool {
  let mut has_cased = false
  let mut prev_is_letter = false
  for i = 0; i < s.length(); i = i + 1 {
    let code = s[i].to_int()
    if is_upper_code(code) {
      if prev_is_letter {
        return false
      }
      has_cased = true
      prev_is_letter = true
    } else if is_lower_code(code) {
      if not(prev_is_letter) {
        return false
      }
      has_cased = true
      prev_is_letter = true
    } else {
      prev_is_letter = false
    }
  }
  has_cased
}

///|
fn substr(s : String, start : Int, end : Int) -> String {
  let buf = StringBuilder::new()
  for i = start; i < end; i = i + 1 {
    buf.write_char(s[i].to_int().unsafe_to_char())
  }
  buf.to_string()
}

///|
fn str_find(s : String, sub : String, start : Int, end : Int) -> Int {
  if sub.length() == 0 {
    return start
  }
  if sub.length() > end - start {
    return -1
  }
  for i = start; i <= end - sub.length(); i = i + 1 {
    let mut found = true
    for j = 0; j < sub.length(); j = j + 1 {
      if s[i + j] != sub[j] {
        found = false
        break
      }
    }
    if found {
      return i
    }
  }
  -1
}

///|
fn str_rfind(s : String, sub : String, start : Int, end : Int) -> Int {
  if sub.length() == 0 {
    return end
  }
  if sub.length() > end - start {
    return -1
  }
  for i = end - sub.length(); i >= start; i = i - 1 {
    let mut found = true
    for j = 0; j < sub.length(); j = j + 1 {
      if s[i + j] != sub[j] {
        found = false
        break
      }
    }
    if found {
      return i
    }
  }
  -1
}

///|
fn str_count(s : String, sub : String, start : Int, end : Int) -> Int {
  if sub.length() == 0 {
    return end - start + 1
  }
  let mut count = 0
  let mut i = start
  while i <= end - sub.length() {
    let idx = str_find(s, sub, i, end)
    if idx < 0 {
      break
    }
    count = count + 1
    i = idx + sub.length()
  }
  count
}

///|
fn str_startswith(s : String, prefix : String) -> Bool {
  if prefix.length() > s.length() {
    return false
  }
  for i = 0; i < prefix.length(); i = i + 1 {
    if s[i] != prefix[i] {
      return false
    }
  }
  true
}

///|
fn str_endswith(s : String, suffix : String) -> Bool {
  if suffix.length() > s.length() {
    return false
  }
  let offset = s.length() - suffix.length()
  for i = 0; i < suffix.length(); i = i + 1 {
    if s[offset + i] != suffix[i] {
      return false
    }
  }
  true
}

///|
fn str_replace(
  s : String,
  old : String,
  new_str : String,
  count : Int,
) -> String {
  if old.length() == 0 {
    let buf = StringBuilder::new()
    let mut replacements = 0
    let max = if count < 0 { s.length() + 1 } else { count }
    for i = 0; i < s.length(); i = i + 1 {
      if replacements < max {
        buf.write_string(new_str)
        replacements = replacements + 1
      }
      buf.write_char(s[i].to_int().unsafe_to_char())
    }
    if replacements < max {
      buf.write_string(new_str)
    }
    return buf.to_string()
  }
  let buf = StringBuilder::new()
  let mut i = 0
  let mut replacements = 0
  while i < s.length() {
    if (count < 0 || replacements < count) && i + old.length() <= s.length() {
      let idx = str_find(s, old, i, s.length())
      if idx == i {
        buf.write_string(new_str)
        i = i + old.length()
        replacements = replacements + 1
        continue
      }
    }
    buf.write_char(s[i].to_int().unsafe_to_char())
    i = i + 1
  }
  buf.to_string()
}

///|
fn str_split(s : String, sep : String?, maxsplit : Int) -> Array[String] {
  match sep {
    None => {
      let result : Array[String] = []
      let mut i = 0
      let mut splits = 0
      while i < s.length() {
        while i < s.length() && is_space_code(s[i].to_int()) {
          i = i + 1
        }
        if i >= s.length() {
          break
        }
        if maxsplit >= 0 && splits >= maxsplit {
          result.push(substr(s, i, s.length()))
          break
        } else {
          let start = i
          while i < s.length() && not(is_space_code(s[i].to_int())) {
            i = i + 1
          }
          result.push(substr(s, start, i))
          splits = splits + 1
        }
      }
      result
    }
    Some(sep_str) => {
      if sep_str.length() == 0 {
        let result : Array[String] = []
        result.push(s)
        return result
      }
      let result : Array[String] = []
      let mut i = 0
      let mut splits = 0
      while i <= s.length() {
        if maxsplit >= 0 && splits >= maxsplit {
          result.push(substr(s, i, s.length()))
          break
        } else {
          let idx = str_find(s, sep_str, i, s.length())
          if idx < 0 {
            result.push(substr(s, i, s.length()))
            break
          } else {
            result.push(substr(s, i, idx))
            i = idx + sep_str.length()
            splits = splits + 1
          }
        }
      }
      result
    }
  }
}

///|
fn str_rsplit(s : String, sep : String?, maxsplit : Int) -> Array[String] {
  match sep {
    None => {
      let parts : Array[String] = []
      let mut i = s.length() - 1
      let mut splits = 0
      while i >= 0 {
        while i >= 0 && is_space_code(s[i].to_int()) {
          i = i - 1
        }
        if i < 0 {
          break
        }
        if maxsplit >= 0 && splits >= maxsplit {
          parts.push(substr(s, 0, i + 1))
          break
        } else {
          let end = i + 1
          while i >= 0 && not(is_space_code(s[i].to_int())) {
            i = i - 1
          }
          parts.push(substr(s, i + 1, end))
          splits = splits + 1
        }
      }
      let result : Array[String] = []
      for j = parts.length() - 1; j >= 0; j = j - 1 {
        result.push(parts[j])
      }
      result
    }
    Some(sep_str) => {
      if sep_str.length() == 0 {
        let result : Array[String] = []
        result.push(s)
        return result
      }
      let parts : Array[String] = []
      let mut i = s.length()
      let mut splits = 0
      while i >= 0 {
        if maxsplit >= 0 && splits >= maxsplit {
          parts.push(substr(s, 0, i))
          break
        } else {
          let idx = str_rfind(s, sep_str, 0, i)
          if idx < 0 {
            parts.push(substr(s, 0, i))
            break
          } else {
            parts.push(substr(s, idx + sep_str.length(), i))
            i = idx
            splits = splits + 1
          }
        }
      }
      let result : Array[String] = []
      for j = parts.length() - 1; j >= 0; j = j - 1 {
        result.push(parts[j])
      }
      result
    }
  }
}

///|
fn str_strip(s : String, chars : String?) -> String {
  str_rstrip(str_lstrip(s, chars), chars)
}

///|
fn str_lstrip(s : String, chars : String?) -> String {
  let mut start = 0
  match chars {
    None =>
      while start < s.length() && is_space_code(s[start].to_int()) {
        start = start + 1
      }
    Some(ch) =>
      while start < s.length() && str_contains_code(ch, s[start].to_int()) {
        start = start + 1
      }
  }
  substr(s, start, s.length())
}

///|
fn str_rstrip(s : String, chars : String?) -> String {
  let mut end = s.length()
  match chars {
    None =>
      while end > 0 && is_space_code(s[end - 1].to_int()) {
        end = end - 1
      }
    Some(ch) =>
      while end > 0 && str_contains_code(ch, s[end - 1].to_int()) {
        end = end - 1
      }
  }
  substr(s, 0, end)
}

///|
fn str_contains_code(s : String, code : Int) -> Bool {
  for i = 0; i < s.length(); i = i + 1 {
    if s[i].to_int() == code {
      return true
    }
  }
  false
}

///|
fn str_splitlines(s : String, keepends : Bool) -> Array[String] {
  let result : Array[String] = []
  let mut i = 0
  while i < s.length() {
    let start = i
    while i < s.length() &&
          s[i].to_int() != '\n'.to_int() &&
          s[i].to_int() != '\r'.to_int() {
      i = i + 1
    }
    if i < s.length() {
      if s[i].to_int() == '\r'.to_int() &&
        i + 1 < s.length() &&
        s[i + 1].to_int() == '\n'.to_int() {
        if keepends {
          result.push(substr(s, start, i + 2))
        } else {
          result.push(substr(s, start, i))
        }
        i = i + 2
      } else {
        if keepends {
          result.push(substr(s, start, i + 1))
        } else {
          result.push(substr(s, start, i))
        }
        i = i + 1
      }
    } else if start < s.length() {
      result.push(substr(s, start, i))
    }
  }
  result
}

///|
fn expect_string(
  v : @value.Value,
  method_name : String,
  span : @ast.Span,
) -> String raise StarlarkError {
  match v {
    String(s) => s
    _ =>
      raise StarlarkError::TypeError(
        message="\{method_name}() argument must be str, not '\{v.type_name()}'",
        span~,
      )
  }
}

///|
fn expect_int(
  v : @value.Value,
  method_name : String,
  span : @ast.Span,
) -> Int raise StarlarkError {
  match v {
    Int(n) => n.to_int()
    _ =>
      raise StarlarkError::TypeError(
        message="\{method_name}() argument must be int, not '\{v.type_name()}'",
        span~,
      )
  }
}

///|
fn clamp_index(idx : Int, len : Int) -> Int {
  if idx < 0 {
    let result = idx + len
    if result < 0 {
      0
    } else {
      result
    }
  } else if idx > len {
    len
  } else {
    idx
  }
}

///|
fn is_all_digits(s : String) -> Bool {
  if s.length() == 0 {
    return false
  }
  for i = 0; i < s.length(); i = i + 1 {
    if not(is_digit_code(s[i].to_int())) {
      return false
    }
  }
  true
}

///|
fn parse_simple_int(s : String) -> Int {
  let mut result = 0
  for i = 0; i < s.length(); i = i + 1 {
    result = result * 10 + (s[i].to_int() - '0'.to_int())
  }
  result
}