///|
/// Formatting and value conversion helpers.

///|
fn stringify_value_with_env(
  value : Value,
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[String, RuntimeError] {
  let _ = globals
  let _ = builtins
  let _ = io
  Ok(value_to_string(value))
}

///|
fn hex_digit(n : Int32) -> Char {
  if n < 10 {
    ('0'.to_int() + n).to_char().unwrap()
  } else {
    ('a'.to_int() + (n - 10)).to_char().unwrap()
  }
}

///|
fn hex_width(codepoint : Int32, width : Int32) -> String {
  let buf = StringBuilder()
  let mut i = 0
  while i < width {
    let shift = 4 * (width - 1 - i)
    let digit = (codepoint >> shift) & 0xF
    buf.write_char(hex_digit(digit))
    i = i + 1
  }
  buf.to_string()
}

///|
fn ascii_escape(text : String) -> String {
  let buf = StringBuilder()
  for ch in text.to_array() {
    let code = ch.to_int()
    if code < 0x80 {
      buf.write_char(ch)
    } else if code <= 0xFFFF {
      buf.write_string("\\u")
      buf.write_string(hex_width(code, 4))
    } else {
      buf.write_string("\\U")
      buf.write_string(hex_width(code, 8))
    }
  }
  buf.to_string()
}

///|
fn fstring_value_to_text(
  value : Value,
  conv : Char?,
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[String, RuntimeError] {
  match conv {
    None => stringify_value_with_env(value, globals, builtins, io)
    Some('s') =>
      match
        eval_builtin_call("str", [value], [], locals, globals, builtins, io) {
        Ok(Some(Value::Str(text))) => Ok(text)
        Ok(Some(other)) => Ok(value_to_string(other))
        Ok(None) => stringify_value_with_env(value, globals, builtins, io)
        Err(err) => Err(err)
      }
    Some('r') =>
      match
        eval_builtin_call("repr", [value], [], locals, globals, builtins, io) {
        Ok(Some(Value::Str(text))) => Ok(text)
        Ok(Some(other)) => Ok(value_to_string(other))
        Ok(None) => stringify_value_with_env(value, globals, builtins, io)
        Err(err) => Err(err)
      }
    Some('a') => {
      let base = match
        eval_builtin_call("repr", [value], [], locals, globals, builtins, io) {
        Ok(Some(Value::Str(text))) => text
        Ok(Some(other)) => value_to_string(other)
        Ok(None) => value_to_string(value)
        Err(err) => return Err(err)
      }
      Ok(ascii_escape(base))
    }
    Some(_) => stringify_value_with_env(value, globals, builtins, io)
  }
}

///|
fn format_string_width(
  text : String,
  spec : String,
  default_align : Char,
) -> String? {
  if spec.length() == 0 {
    return None
  }
  let chars = spec.to_array()
  let mut fill = ' '
  let mut align : Char? = None
  let mut i = 0
  if chars.length() >= 2 &&
    (chars[1] == '<' || chars[1] == '>' || chars[1] == '^') {
    fill = chars[0]
    align = Some(chars[1])
    i = 2
  } else if chars[0] == '<' || chars[0] == '>' || chars[0] == '^' {
    align = Some(chars[0])
    i = 1
  } else if is_digit(chars[0]) {
    align = Some(default_align)
    i = 0
  } else {
    return None
  }
  let start = i
  while i < chars.length() && is_digit(chars[i]) {
    i += 1
  }
  if start == i || i != chars.length() {
    return None
  }
  let width_text = substring(spec, start, i)
  let width = parse_decimal_int(width_text)
  if width <= 0 {
    return Some(text)
  }
  let len = text.length()
  if len >= width {
    return Some(text)
  }
  let pad = width - len
  let left = match align.unwrap_or(default_align) {
    '<' => 0
    '>' => pad
    '^' => pad / 2
    _ => 0
  }
  let right = pad - left
  let buf = StringBuilder()
  for i = 0; i < left; i = i + 1 {
    buf.write_char(fill)
  }
  buf.write_string(text)
  for i = 0; i < right; i = i + 1 {
    buf.write_char(fill)
  }
  Some(buf.to_string())
}

///|
fn parse_decimal_int(text : String) -> Int32 {
  let chars = text.to_array()
  let mut n = 0
  for c in chars {
    if is_digit(c) {
      n = n * 10 + (c.to_int() - 48)
    }
  }
  n
}

///|
fn fstring_int_to_base_string(
  value : @bigint.BigInt,
  base : Int32,
  uppercase : Bool,
) -> String {
  if value == 0N {
    return "0"
  }
  let digits_table : Array[Char] = "0123456789abcdef".to_array()
  let upper_table : Array[Char] = "0123456789ABCDEF".to_array()
  let table = if uppercase { upper_table } else { digits_table }
  let mut n = value
  let b = @bigint.BigInt::from_int(base)
  let digits : Array[Char] = []
  while n > 0N {
    let d = (n % b).to_int()
    digits.push(table[d])
    n = n / b
  }
  let buf = StringBuilder()
  let mut i = digits.length()
  while i > 0 {
    i = i - 1
    buf.write_char(digits[i])
  }
  buf.to_string()
}

///|
fn format_int_with_spec(value : @bigint.BigInt, spec : String) -> String? {
  if spec.length() == 0 {
    return None
  }
  let chars = spec.to_array()
  let mut i = 0
  let mut alternate = false
  if chars[0] == '#' {
    alternate = true
    i = 1
  }
  let mut zero_pad = false
  if i < chars.length() && chars[i] == '0' {
    zero_pad = true
    i += 1
  }
  let width_start = i
  while i < chars.length() && is_digit(chars[i]) {
    i += 1
  }
  let width = if i > width_start {
    parse_decimal_int(substring(spec, width_start, i))
  } else {
    0
  }
  if i >= chars.length() {
    return None
  }
  let mut grouping : Char? = None
  if i < chars.length() - 1 && (chars[i] == '_' || chars[i] == ',') {
    grouping = Some(chars[i])
    i += 1
  }
  if i >= chars.length() {
    return None
  }
  let typ = chars[i]
  if i + 1 != chars.length() {
    return None
  }
  let neg = value < 0N
  let abs_value = if neg { -value } else { value }
  let (prefix, base, uppercase, decimal) = match typ {
    'd' => ("", 10, false, true)
    'x' => ("0x", 16, false, false)
    'X' => ("0X", 16, true, false)
    'o' => ("0o", 8, false, false)
    'b' => ("0b", 2, false, false)
    _ => return None
  }
  fn group_decimal_underscores(text : String) -> String {
    let chars = text.to_array()
    if chars.length() <= 3 {
      return text
    }
    let buf = StringBuilder()
    let mut i = 0
    let rem = chars.length() % 3
    let first = if rem == 0 { 3 } else { rem }
    while i < chars.length() {
      if i > 0 {
        buf.write_char('_')
      }
      let chunk_len = if i == 0 { first } else { 3 }
      for j = 0; j < chunk_len; j = j + 1 {
        buf.write_char(chars[i + j])
      }
      i = i + chunk_len
    }
    buf.to_string()
  }

  let mut digits = if decimal {
    abs_value.to_string()
  } else {
    fstring_int_to_base_string(abs_value, base, uppercase)
  }
  match grouping {
    Some('_') => if decimal { digits = group_decimal_underscores(digits) }
    _ => ()
  }
  let sign = if neg { "-" } else { "" }
  let alt_prefix = if alternate && !decimal { prefix } else { "" }
  let mut core = sign.to_string() + alt_prefix + digits
  if width > 0 && core.length() < width {
    let pad = width - core.length()
    if zero_pad {
      let buf = StringBuilder()
      buf.write_string(sign)
      buf.write_string(alt_prefix)
      for i = 0; i < pad; i = i + 1 {
        buf.write_char('0')
      }
      buf.write_string(digits)
      core = buf.to_string()
    } else {
      let buf = StringBuilder()
      for i = 0; i < pad; i = i + 1 {
        buf.write_char(' ')
      }
      buf.write_string(core)
      core = buf.to_string()
    }
  }
  Some(core)
}

///|
fn eval_fstring_text(
  text : String,
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[String, RuntimeError] {
  let buf = StringBuilder()
  let chars = text.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c == '{' {
      if i + 1 < chars.length() && chars[i + 1] == '{' {
        buf.write_char('{')
        i += 2
        continue
      }
      let after_open = substring(text, i + 1, text.length())
      let close_rel = find_fstring_expr_close(after_open)
      if close_rel is None {
        return Err(
          make_runtime_error(
            RuntimeErrorKind::Runtime,
            "SyntaxError: f-string: expecting '}'".to_string(),
          ),
        )
      }
      let close = i + 1 + close_rel.unwrap()
      let inner = substring(text, i + 1, close)
      if inner.trim(chars=" \t").length() == 0 {
        return Err(
          make_runtime_error(
            RuntimeErrorKind::Runtime,
            "SyntaxError: f-string: empty expression not allowed".to_string(),
          ),
        )
      }
      let (expr_and_conv, format_spec_raw) = match
        find_top_level_format_colon(inner) {
        Some(pos) =>
          (substring(inner, 0, pos), substring(inner, pos + 1, inner.length()))
        None => (inner, "".to_string())
      }
      let (expr_raw, conv) = match
        find_top_level_fstring_conversion_bang(expr_and_conv) {
        Some(pos) =>
          if pos + 1 < expr_and_conv.length() {
            let conv_char = expr_and_conv.to_array()[pos + 1]
            (substring(expr_and_conv, 0, pos), Some(conv_char))
          } else {
            (expr_and_conv, None)
          }
        None => (expr_and_conv, None)
      }
      let debug_eq = find_top_level_fstring_debug_equal(expr_raw)
      let (expr_text, debug_prefix) = match debug_eq {
        Some(pos) => {
          let mut prefix_end = pos + 1
          let expr_chars = expr_raw.to_array()
          while prefix_end < expr_chars.length() {
            let c = expr_chars[prefix_end]
            if c == ' ' || c == '\t' {
              prefix_end += 1
            } else {
              break
            }
          }
          (
            substring(expr_raw, 0, pos).trim(chars=" \t").to_owned(),
            Some(substring(expr_raw, 0, prefix_end)),
          )
        }
        None => (expr_raw.trim(chars=" \t").to_owned(), None)
      }
      let expr = match parse_expr(expr_text) {
        Ok(value) => value
        Err(_) =>
          return Err(
            make_runtime_error(
              RuntimeErrorKind::Runtime,
              "SyntaxError: f-string: invalid syntax".to_string(),
            ),
          )
      }
      let value = match
        eval_expr_with_env(expr, locals, globals, builtins, io) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      let rendered = match
        fstring_value_to_text(value, conv, locals, globals, builtins, io) {
        Ok(v) => v
        Err(err) => return Err(err)
      }
      let format_spec = if format_spec_raw.trim(chars=" \t").length() == 0 {
        "".to_string()
      } else if format_spec_raw.contains("{") || format_spec_raw.contains("}") {
        match
          eval_fstring_text(format_spec_raw, locals, globals, builtins, io) {
          Ok(v) => v
          Err(err) => return Err(err)
        }
      } else {
        format_spec_raw
      }
      let formatted = if format_spec.length() == 0 {
        rendered
      } else {
        match conv {
          Some(_) =>
            match format_string_width(rendered, format_spec, '<') {
              Some(v) => v
              None => rendered
            }
          None =>
            match value {
              Value::Int(n) =>
                match format_int_with_spec(n, format_spec) {
                  Some(v) => v
                  None =>
                    match format_string_width(rendered, format_spec, '>') {
                      Some(v) => v
                      None => rendered
                    }
                }
              _ =>
                match format_string_width(rendered, format_spec, '<') {
                  Some(v) => v
                  None => rendered
                }
            }
        }
      }
      match debug_prefix {
        Some(prefix) => {
          buf.write_string(prefix)
          buf.write_string(formatted)
        }
        None => buf.write_string(formatted)
      }
      i = close + 1
      continue
    }
    if c == '}' {
      if i + 1 < chars.length() && chars[i + 1] == '}' {
        buf.write_char('}')
        i += 2
        continue
      }
      return Err(
        make_runtime_error(
          RuntimeErrorKind::Runtime,
          "SyntaxError: f-string: single '}' is not allowed".to_string(),
        ),
      )
    }
    buf.write_char(c)
    i += 1
  }
  Ok(buf.to_string())
}

///|
fn bytes_subsequence_contains(
  haystack : Array[Int32],
  needle : Array[Int32],
) -> Bool {
  if needle.length() == 0 {
    return true
  }
  if needle.length() > haystack.length() {
    return false
  }
  let limit = haystack.length() - needle.length()
  let mut i = 0
  while i <= limit {
    let mut matched = true
    let mut j = 0
    while j < needle.length() {
      if haystack[i + j] != needle[j] {
        matched = false
        break
      }
      j = j + 1
    }
    if matched {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn bytes_contains(haystack : Array[Int32], needle : Value) -> Bool {
  match needle {
    Value::Int(v) => {
      let idx = match bigint_to_int_checked(v) {
        Ok(value) => value
        Err(_) => return false
      }
      if idx < 0 || idx > 255 {
        return false
      }
      for byte in haystack {
        if byte == idx {
          return true
        }
      }
      false
    }
    Value::Bool(v) => {
      let idx = if v { 1 } else { 0 }
      for byte in haystack {
        if byte == idx {
          return true
        }
      }
      false
    }
    Value::Bytes(values)
    | Value::ByteArray(values)
    | Value::MemoryView(values) => bytes_subsequence_contains(haystack, values)
    _ => false
  }
}

///|
fn to_complex(value : Value) -> Result[(Double, Double), RuntimeError] {
  match value {
    Value::Complex(real, imag) => Ok((real, imag))
    Value::Float(v) => Ok((v, 0.0))
    Value::Int(v) =>
      match bigint_to_double_checked(v) {
        Ok(value) => Ok((value, 0.0))
        Err(err) => Err(err)
      }
    Value::Bool(v) => Ok((if v { 1.0 } else { 0.0 }, 0.0))
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "expected number".to_string(),
        ),
      )
  }
}

///|
fn pad_left(text : String, width : Int32, pad_char : Char) -> String {
  if width <= text.length() {
    text
  } else {
    let buf = StringBuilder()
    let pad_len = width - text.length()
    for _i = 0; _i < pad_len; _i = _i + 1 {
      buf.write_char(pad_char)
    }
    buf.write_string(text)
    buf.to_string()
  }
}

///|
fn pad_right(text : String, width : Int32, pad_char : Char) -> String {
  if width <= text.length() {
    text
  } else {
    let buf = StringBuilder()
    buf.write_string(text)
    let pad_len = width - text.length()
    for _i = 0; _i < pad_len; _i = _i + 1 {
      buf.write_char(pad_char)
    }
    buf.to_string()
  }
}

///|
fn pad_numeric(text : String, width : Int32, zero_pad : Bool) -> String {
  if !zero_pad {
    return pad_left(text, width, ' ')
  }
  if width <= text.length() {
    return text
  }
  match text.get_char(0) {
    Some('-') => {
      let chars = text.to_array()
      let rest_chars : Array[Char] = []
      for idx = 1; idx < chars.length(); idx = idx + 1 {
        rest_chars.push(chars[idx])
      }
      let rest = String::from_array(rest_chars)
      let pad_len = width - rest.length() - 1
      if pad_len <= 0 {
        text
      } else {
        let buf = StringBuilder()
        buf.write_char('-')
        for _i = 0; _i < pad_len; _i = _i + 1 {
          buf.write_char('0')
        }
        buf.write_string(rest)
        buf.to_string()
      }
    }
    _ => pad_left(text, width, '0')
  }
}

///|
fn format_decimal_value(value : Value) -> Result[String, RuntimeError] {
  match value {
    Value::Int(v) => Ok(v.to_string())
    Value::Bool(v) => Ok(if v { "1" } else { "0" })
    Value::Float(v) => Ok(v.to_int64().to_string())
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "expected number".to_string(),
        ),
      )
  }
}

///|
fn format_hex_value(
  value : Value,
  uppercase : Bool,
) -> Result[String, RuntimeError] {
  match value {
    Value::Int(v) => {
      let neg = v < 0N
      let abs_value = if neg { -v } else { v }
      let mut out = abs_value.to_string(radix=16)
      if uppercase {
        out = out.to_upper()
      }
      if neg {
        out = "-" + out
      }
      Ok(out)
    }
    Value::Bool(v) => Ok(if v { "1" } else { "0" })
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "expected number".to_string(),
        ),
      )
  }
}

///|
fn format_octal_value(value : Value) -> Result[String, RuntimeError] {
  match value {
    Value::Int(v) => Ok(bigint_to_base_string(v, 8, ""))
    Value::Bool(v) => Ok(if v { "1" } else { "0" })
    Value::Float(v) =>
      Ok(bigint_to_base_string(@bigint.BigInt::from_int64(v.to_int64()), 8, ""))
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "expected number".to_string(),
        ),
      )
  }
}

///|
fn round_float_to_int64(value : Double) -> Int64 {
  if value >= 0.0 {
    @math.floor(value + 0.5).to_int64()
  } else {
    @math.ceil(value - 0.5).to_int64()
  }
}

///|
fn format_float_fixed(value : Double, precision : Int32) -> String {
  // Minimal fixed-point formatting for `%f` used throughout the CPython stdlib.
  // Only intended for small precisions (e.g. `%.3f`).
  if precision <= 0 {
    let rounded = round_float_to_int64(value)
    return rounded.to_string()
  }
  let neg = value < 0.0
  let abs_value = if neg { -value } else { value }
  let mut scale : Int64 = 1
  let mut i = 0
  while i < precision && scale < 1_000_000_000_000_000_000L {
    scale = scale * 10L
    i = i + 1
  }
  // If precision is too large for Int64 scaling, fall back to default formatting.
  if i != precision {
    return (if neg { "-" } else { "" }) + abs_value.to_string()
  }
  let scaled = round_float_to_int64(abs_value * scale.to_double())
  let int_part = scaled / scale
  let frac_part = scaled % scale
  let frac_text_raw = frac_part.to_string()
  let frac_text = pad_left(frac_text_raw, precision, '0')
  (if neg { "-" } else { "" }) + int_part.to_string() + "." + frac_text
}

///|
fn format_float_value(
  value : Value,
  precision_opt : Int32?,
) -> Result[String, RuntimeError] {
  let precision = match precision_opt {
    Some(p) => p
    None => 6
  }
  match value {
    Value::Float(v) => Ok(format_float_fixed(v, precision))
    Value::Int(v) =>
      match bigint_to_double_checked(v) {
        Ok(d) => Ok(format_float_fixed(d, precision))
        Err(err) => Err(err)
      }
    Value::Bool(v) =>
      Ok(format_float_fixed(if v { 1.0 } else { 0.0 }, precision))
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "expected number".to_string(),
        ),
      )
  }
}

///|
fn format_percent_string(
  format : String,
  value : Value,
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[String, RuntimeError] {
  touch_env([], globals, builtins, io)
  let mut mapping_pairs : Array[(Value, Value)]? = None
  let mapping_value = value
  let args = match value {
    Value::Tuple(values) => values
    Value::Instance(inst) =>
      match get_named_value(inst.dict, tuple_storage_name) {
        Some(Value::Tuple(values)) => values
        _ => [value]
      }
    Value::Dict(pairs) => {
      mapping_pairs = Some(pairs)
      [value]
    }
    _ => [value]
  }
  let buf = StringBuilder()
  let chars = format.to_array()
  let mut arg_index = 0
  let mut used_positional = false
  let mut i = 0
  while i < chars.length() {
    let ch = chars[i]
    if ch != '%' {
      buf.write_char(ch)
      i = i + 1
      continue
    }
    if i + 1 >= chars.length() {
      return Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "incomplete format".to_string(),
        ),
      )
    }
    if chars[i + 1] == '%' {
      buf.write_char('%')
      i = i + 2
      continue
    }
    let mut j = i + 1
    let mut key_name : String? = None
    if chars[j] == '(' {
      let mut k = j + 1
      while k < chars.length() && chars[k] != ')' {
        k = k + 1
      }
      if k >= chars.length() {
        return Err(
          make_runtime_error(
            RuntimeErrorKind::Type,
            "incomplete format".to_string(),
          ),
        )
      }
      let key_chars : Array[Char] = []
      for idx = j + 1; idx < k; idx = idx + 1 {
        key_chars.push(chars[idx])
      }
      key_name = Some(String::from_array(key_chars))
      j = k + 1
    }
    if j >= chars.length() {
      return Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "incomplete format".to_string(),
        ),
      )
    }
    fn star_int_value(name : String, v : Value) -> Result[Int32, RuntimeError] {
      match v {
        Value::Int(b) => Ok(b.to_int64().to_int())
        Value::Bool(b) => Ok(if b { 1 } else { 0 })
        Value::Float(f) => Ok(f.to_int64().to_int())
        _ =>
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              name + " is not an integer".to_string(),
            ),
          )
      }
    }

    let mut left_adjust = false
    let mut zero_pad = false
    while j < chars.length() {
      if chars[j] == '-' {
        left_adjust = true
        zero_pad = false
        j = j + 1
        continue
      }
      if chars[j] == '0' && !left_adjust {
        zero_pad = true
        j = j + 1
        continue
      }
      break
    }
    let mut width = 0
    if j < chars.length() && chars[j] == '*' {
      if arg_index >= args.length() {
        return Err(
          make_runtime_error(
            RuntimeErrorKind::Type,
            "not enough arguments for format string".to_string(),
          ),
        )
      }
      let w = match star_int_value("width".to_string(), args[arg_index]) {
        Ok(v) => v
        Err(err) => return Err(err)
      }
      arg_index = arg_index + 1
      used_positional = true
      if w < 0 {
        left_adjust = true
        width = -w
      } else {
        width = w
      }
      j = j + 1
    } else {
      while j < chars.length() && chars[j].is_ascii_digit() {
        width = width * 10 + (chars[j].to_int() - '0'.to_int())
        j = j + 1
      }
    }
    // Optional precision: `%.3f`
    let mut precision : Int32? = None
    if j < chars.length() && chars[j] == '.' {
      j = j + 1
      if j < chars.length() && chars[j] == '*' {
        if arg_index >= args.length() {
          return Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "not enough arguments for format string".to_string(),
            ),
          )
        }
        let p = match star_int_value("precision".to_string(), args[arg_index]) {
          Ok(v) => v
          Err(err) => return Err(err)
        }
        arg_index = arg_index + 1
        used_positional = true
        precision = Some(p)
        j = j + 1
      } else {
        let mut p = 0
        while j < chars.length() && chars[j].is_ascii_digit() {
          p = p * 10 + (chars[j].to_int() - '0'.to_int())
          j = j + 1
        }
        precision = Some(p)
      }
    }
    if j >= chars.length() {
      return Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "incomplete format".to_string(),
        ),
      )
    }
    let arg = match key_name {
      Some(key) => {
        let key_value = Value::Str(key)
        match mapping_pairs {
          Some(pairs) => {
            let index = match dict_find_index(pairs, key_value) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
            match index {
              Some(pos) => pairs[pos].1
              None =>
                return Err(
                  make_runtime_error(
                    RuntimeErrorKind::Key,
                    "key not found".to_string(),
                  ),
                )
            }
          }
          None => {
            // Mapping-based formatting should accept any object that implements
            // `__getitem__` (e.g. `instance.__dict__` wrapper used by logging).
            let getitem = match
              get_attr_from_value(
                mapping_value, "__getitem__", globals, builtins, io,
              ) {
              Ok(v) => v
              Err(err) =>
                return match err.kind {
                  RuntimeErrorKind::Attribute =>
                    Err(
                      make_runtime_error(
                        RuntimeErrorKind::Type,
                        "format requires a mapping".to_string(),
                      ),
                    )
                  _ => Err(err)
                }
            }
            match
              call_callable_with_env(
                getitem,
                [key_value],
                [],
                globals,
                builtins,
                io,
              ) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
          }
        }
      }
      None => {
        if arg_index >= args.length() {
          return Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "not enough arguments for format string".to_string(),
            ),
          )
        }
        let arg = args[arg_index]
        arg_index = arg_index + 1
        used_positional = true
        arg
      }
    }
    let spec = chars[j]
    fn repr_text(
      arg : Value,
      globals : Array[(String, Value)],
      builtins : Array[(String, Value)],
      io : MockIO,
    ) -> Result[String, RuntimeError] {
      match eval_builtin_call("repr", [arg], [], [], globals, builtins, io) {
        Ok(Some(Value::Str(text))) => Ok(text)
        Ok(Some(other)) => Ok(value_to_string(other))
        Ok(None) => Ok(value_to_string(arg))
        Err(err) => Err(err)
      }
    }

    let raw = match spec {
      's' => Ok(value_to_string(arg))
      'r' => repr_text(arg, globals, builtins, io)
      'c' =>
        match arg {
          Value::Str(s) =>
            if s.to_array().length() == 1 {
              Ok(s)
            } else {
              Err(
                make_runtime_error(
                  RuntimeErrorKind::Type,
                  "%c requires int or char".to_string(),
                ),
              )
            }
          _ => {
            let code = match arg {
              Value::Int(v) => v
              Value::Bool(v) => if v { 1N } else { 0N }
              _ =>
                return Err(
                  make_runtime_error(
                    RuntimeErrorKind::Type,
                    "%c requires int or char".to_string(),
                  ),
                )
            }
            if code < 0N || code > 0x10FFFFN {
              return Err(
                make_runtime_error(
                  RuntimeErrorKind::Runtime,
                  "OverflowError: %c arg not in range(0x110000)".to_string(),
                ),
              )
            }
            Ok(char_to_string(Int32::unsafe_to_char(code.to_int64().to_int())))
          }
        }
      'd' | 'i' => format_decimal_value(arg)
      'o' => format_octal_value(arg)
      'x' => format_hex_value(arg, false)
      'X' => format_hex_value(arg, true)
      'f' | 'F' =>
        match format_float_value(arg, precision) {
          Ok(v) => Ok(v)
          Err(err) => Err(err)
        }
      _ =>
        Err(
          make_runtime_error(
            RuntimeErrorKind::Type,
            "unsupported format character".to_string(),
          ),
        )
    }
    let formatted = match raw {
      Ok(text) =>
        if left_adjust {
          pad_right(text, width, ' ')
        } else {
          match spec {
            'd' | 'i' | 'o' | 'x' | 'X' => pad_numeric(text, width, zero_pad)
            _ => pad_left(text, width, if zero_pad { '0' } else { ' ' })
          }
        }
      Err(err) => return Err(err)
    }
    buf.write_string(formatted)
    i = j + 1
  }
  if used_positional && arg_index < args.length() {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "not all arguments converted during string formatting".to_string(),
      ),
    )
  }
  Ok(buf.to_string())
}