///|
/// A dummy span used for errors raised from built-in functions.
let builtin_span : @ast.Span = {
  start: { file: "", line: 0, column: 0 },
  end: { file: "", line: 0, column: 0 },
}

///|
/// Convert a Value to its Starlark string representation (like str()).
fn value_to_str(v : @value.Value) -> String {
  match v {
    None => "None"
    Bool(b) => if b { "True" } else { "False" }
    Int(n) => n.to_string()
    String(s) => s
    List(items) => {
      let parts : Array[String] = []
      for item in items.val {
        parts.push(value_to_repr(item))
      }
      "[" + parts.join(", ") + "]"
    }
    Tuple(items) => {
      let parts : Array[String] = []
      for i = 0; i < items.length(); i = i + 1 {
        parts.push(value_to_repr(items[i]))
      }
      if items.length() == 1 {
        "(" + parts[0] + ",)"
      } else {
        "(" + parts.join(", ") + ")"
      }
    }
    Dict(map) => {
      let parts : Array[String] = []
      for k, v2 in map.val {
        parts.push(
          value_to_repr(@value.Value::String(k)) + ": " + value_to_repr(v2),
        )
      }
      "{" + parts.join(", ") + "}"
    }
    Function(name, _) => ""
    BuiltinFunction(name, _) => ""
  }
}

///|
/// Convert a Value to its Starlark repr representation (like repr()).
/// Strings get double-quoted with escapes.
fn value_to_repr(v : @value.Value) -> String {
  match v {
    String(s) => {
      let buf = StringBuilder::new()
      buf.write_char('"')
      for i = 0; i < s.length(); i = i + 1 {
        let code = s[i].to_int()
        if code == '"'.to_int() {
          buf.write_string("\\\"")
        } else if code == '\\'.to_int() {
          buf.write_string("\\\\")
        } else if code == '\n'.to_int() {
          buf.write_string("\\n")
        } else if code == '\r'.to_int() {
          buf.write_string("\\r")
        } else if code == '\t'.to_int() {
          buf.write_string("\\t")
        } else {
          buf.write_char(code.unsafe_to_char())
        }
      }
      buf.write_char('"')
      buf.to_string()
    }
    _ => value_to_str(v)
  }
}

///|
/// Compare two values for sorting. Returns -1, 0, or 1.
fn builtin_compare(
  a : @value.Value,
  b : @value.Value,
) -> Int raise StarlarkError {
  match (a, b) {
    (Int(x), Int(y)) => if x < y { -1 } else if x > y { 1 } else { 0 }
    (String(x), String(y)) => x.lexical_compare(y)
    (Bool(x), Bool(y)) => {
      let xi : Int64 = if x { 1L } else { 0L }
      let yi : Int64 = if y { 1L } else { 0L }
      if xi < yi {
        -1
      } else if xi > yi {
        1
      } else {
        0
      }
    }
    (List(xs), List(ys)) => {
      let min_len = if xs.val.length() < ys.val.length() {
        xs.val.length()
      } else {
        ys.val.length()
      }
      for i = 0; i < min_len; i = i + 1 {
        let c = builtin_compare(xs.val[i], ys.val[i])
        if c != 0 {
          return c
        }
      }
      if xs.val.length() < ys.val.length() {
        -1
      } else if xs.val.length() > ys.val.length() {
        1
      } else {
        0
      }
    }
    (Tuple(xs), Tuple(ys)) => {
      let min_len = if xs.length() < ys.length() {
        xs.length()
      } else {
        ys.length()
      }
      for i = 0; i < min_len; i = i + 1 {
        let c = builtin_compare(xs[i], ys[i])
        if c != 0 {
          return c
        }
      }
      if xs.length() < ys.length() {
        -1
      } else if xs.length() > ys.length() {
        1
      } else {
        0
      }
    }
    _ =>
      raise StarlarkError::TypeError(
        message="not supported between instances of '" +
          a.type_name() +
          "' and '" +
          b.type_name() +
          "'",
        span=builtin_span,
      )
  }
}

///|
/// Simple string hash function.
fn string_hash(s : String) -> Int64 {
  let mut h : Int64 = 0L
  for i = 0; i < s.length(); i = i + 1 {
    h = h * 31L + s[i].to_int().to_int64()
  }
  h
}

///|
/// Parse an integer string with a given base.
fn parse_int_with_base(s : String, base : Int) -> Int64 raise StarlarkError {
  let mut str = s
  // Trim whitespace
  let mut start = 0
  while start < str.length() &&
        (
          str[start].to_int() == ' '.to_int() ||
          str[start].to_int() == '\t'.to_int()
        ) {
    start = start + 1
  }
  let mut end = str.length()
  while end > start &&
        (
          str[end - 1].to_int() == ' '.to_int() ||
          str[end - 1].to_int() == '\t'.to_int()
        ) {
    end = end - 1
  }
  if start > 0 || end < str.length() {
    let buf = StringBuilder::new()
    for i = start; i < end; i = i + 1 {
      buf.write_char(str[i].to_int().unsafe_to_char())
    }
    str = buf.to_string()
  }
  if str.length() == 0 {
    raise StarlarkError::ValueError(
      message="int() argument must be a string or a number, not empty string",
      span=builtin_span,
    )
  }
  // Handle sign
  let mut negative = false
  let mut idx = 0
  if str[0].to_int() == '-'.to_int() {
    negative = true
    idx = 1
  } else if str[0].to_int() == '+'.to_int() {
    idx = 1
  }
  let actual_base : Int = if base == 0 {
    // Auto-detect
    if idx + 1 < str.length() && str[idx].to_int() == '0'.to_int() {
      let prefix_code = str[idx + 1].to_int()
      if prefix_code == 'x'.to_int() || prefix_code == 'X'.to_int() {
        idx = idx + 2
        16
      } else if prefix_code == 'o'.to_int() || prefix_code == 'O'.to_int() {
        idx = idx + 2
        8
      } else if prefix_code == 'b'.to_int() || prefix_code == 'B'.to_int() {
        idx = idx + 2
        2
      } else {
        10
      }
    } else {
      10
    }
  } else {
    // If explicit base, still skip prefix if present
    if base == 16 &&
      idx + 1 < str.length() &&
      str[idx].to_int() == '0'.to_int() &&
      (
        str[idx + 1].to_int() == 'x'.to_int() ||
        str[idx + 1].to_int() == 'X'.to_int()
      ) {
      idx = idx + 2
    } else if base == 8 &&
      idx + 1 < str.length() &&
      str[idx].to_int() == '0'.to_int() &&
      (
        str[idx + 1].to_int() == 'o'.to_int() ||
        str[idx + 1].to_int() == 'O'.to_int()
      ) {
      idx = idx + 2
    } else if base == 2 &&
      idx + 1 < str.length() &&
      str[idx].to_int() == '0'.to_int() &&
      (
        str[idx + 1].to_int() == 'b'.to_int() ||
        str[idx + 1].to_int() == 'B'.to_int()
      ) {
      idx = idx + 2
    }
    base
  }
  if idx >= str.length() {
    raise StarlarkError::ValueError(
      message="invalid literal for int() with base \{actual_base}: \{value_to_repr(@value.Value::String(s))}",
      span=builtin_span,
    )
  }
  let mut result : Int64 = 0L
  let base64 = actual_base.to_int64()
  while idx < str.length() {
    let code = str[idx].to_int()
    // Allow underscores as digit separators
    if code == '_'.to_int() {
      idx = idx + 1
      continue
    }
    let digit : Int = if code >= '0'.to_int() && code <= '9'.to_int() {
      code - '0'.to_int()
    } else if code >= 'a'.to_int() && code <= 'z'.to_int() {
      code - 'a'.to_int() + 10
    } else if code >= 'A'.to_int() && code <= 'Z'.to_int() {
      code - 'A'.to_int() + 10
    } else {
      raise StarlarkError::ValueError(
        message="invalid literal for int() with base \{actual_base}: \{value_to_repr(@value.Value::String(s))}",
        span=builtin_span,
      )
    }
    if digit >= actual_base {
      raise StarlarkError::ValueError(
        message="invalid literal for int() with base \{actual_base}: \{value_to_repr(@value.Value::String(s))}",
        span=builtin_span,
      )
    }
    result = result * base64 + digit.to_int64()
    idx = idx + 1
  }
  if negative {
    -result
  } else {
    result
  }
}

///|
/// Returns a map of all predeclared Starlark built-in functions.
pub fn predeclared_builtins() -> Map[String, @value.Value] {
  let builtins : Map[String, @value.Value] = {}

  // abs(x) - absolute value of int
  builtins.set(
    "abs",
    @value.Value::BuiltinFunction("abs", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="abs() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      match pos[0] {
        Int(n) => @value.Value::Int(n.abs())
        _ =>
          raise StarlarkError::TypeError(
            message="bad operand type for abs(): '\{pos[0].type_name()}'",
            span=builtin_span,
          )
      }
    }),
  )

  // any(x) - true if any element of iterable is truthy
  builtins.set(
    "any",
    @value.Value::BuiltinFunction("any", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="any() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let items = iterable_to_array_by_name(pos[0], "any")
      for item in items {
        if item.is_truthy() {
          return @value.Value::Bool(true)
        }
      }
      @value.Value::Bool(false)
    }),
  )

  // all(x) - true if all elements are truthy (true for empty)
  builtins.set(
    "all",
    @value.Value::BuiltinFunction("all", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="all() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let items = iterable_to_array_by_name(pos[0], "all")
      for item in items {
        if not(item.is_truthy()) {
          return @value.Value::Bool(false)
        }
      }
      @value.Value::Bool(true)
    }),
  )

  // bool(x?) - convert to bool; bool() -> false
  builtins.set(
    "bool",
    @value.Value::BuiltinFunction("bool", fn(pos, _kw) raise {
      if pos.length() == 0 {
        return @value.Value::Bool(false)
      }
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="bool() takes at most one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      @value.Value::Bool(pos[0].is_truthy())
    }),
  )

  // dict(pairs?) - create dict from list of (key, value) tuples; dict() -> {}
  builtins.set(
    "dict",
    @value.Value::BuiltinFunction("dict", fn(pos, kw) raise {
      let map : Map[String, @value.Value] = {}
      guard pos.length() <= 1 else {
        raise StarlarkError::TypeError(
          message="dict() takes at most one positional argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      if pos.length() == 1 {
        let items = iterable_to_array_by_name(pos[0], "dict")
        for item in items {
          match item {
            Tuple(pair) =>
              if pair.length() == 2 {
                match pair[0] {
                  String(k) => map.set(k, pair[1])
                  _ =>
                    raise StarlarkError::TypeError(
                      message="dict keys must be strings",
                      span=builtin_span,
                    )
                }
              } else {
                raise StarlarkError::ValueError(
                  message="dict() requires (key, value) pairs",
                  span=builtin_span,
                )
              }
            List(pair) =>
              if pair.val.length() == 2 {
                match pair.val[0] {
                  String(k) => map.set(k, pair.val[1])
                  _ =>
                    raise StarlarkError::TypeError(
                      message="dict keys must be strings",
                      span=builtin_span,
                    )
                }
              } else {
                raise StarlarkError::ValueError(
                  message="dict() requires (key, value) pairs",
                  span=builtin_span,
                )
              }
            _ =>
              raise StarlarkError::TypeError(
                message="cannot convert dict() element to a pair",
                span=builtin_span,
              )
          }
        }
      }
      for k, v in kw {
        map.set(k, v)
      }
      @value.Value::Dict(Ref::new(map))
    }),
  )

  // dir(x) - sorted list of attribute names
  builtins.set(
    "dir",
    @value.Value::BuiltinFunction("dir", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="dir() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let name_strs : Array[String] = []
      match pos[0] {
        String(_) =>
          for name, _ in string_methods() {
            name_strs.push(name)
          }
        List(_) =>
          for name, _ in list_methods() {
            name_strs.push(name)
          }
        Dict(_) =>
          for name, _ in dict_methods() {
            name_strs.push(name)
          }
        _ => ()
      }
      // Insertion sort with lexicographic comparison
      // (MoonBit's built-in String < / > is not lexicographic)
      for i = 1; i < name_strs.length(); i = i + 1 {
        let key = name_strs[i]
        let mut j = i - 1
        while j >= 0 {
          if name_strs[j].lexical_compare(key) > 0 {
            name_strs[j + 1] = name_strs[j]
            j = j - 1
          } else {
            break
          }
        }
        name_strs[j + 1] = key
      }
      let names : Array[@value.Value] = []
      for name in name_strs {
        names.push(@value.Value::String(name))
      }
      @value.Value::List(Ref::new(names))
    }),
  )

  // enumerate(x, start=0) - list of (index, value) tuples
  builtins.set(
    "enumerate",
    @value.Value::BuiltinFunction("enumerate", fn(pos, kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="enumerate() takes exactly one positional argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let start : Int64 = match kw.get("start") {
        Some(Int(n)) => n
        Some(other) =>
          raise StarlarkError::TypeError(
            message="enumerate() start must be an int, not '" +
              other.type_name() +
              "'",
            span=builtin_span,
          )
        None => 0L
      }
      let items = iterable_to_array_by_name(pos[0], "enumerate")
      let result : Array[@value.Value] = []
      for i = 0; i < items.length(); i = i + 1 {
        let idx = start + i.to_int64()
        let tuple_items : FixedArray[@value.Value] = [
          @value.Value::Int(idx),
          items[i],
        ]
        result.push(@value.Value::Tuple(tuple_items))
      }
      @value.Value::List(Ref::new(result))
    }),
  )

  // fail(*args) - raise error with message
  builtins.set(
    "fail",
    @value.Value::BuiltinFunction("fail", fn(pos, _kw) raise {
      let parts : Array[String] = []
      for arg in pos {
        parts.push(value_to_str(arg))
      }
      let msg = if parts.length() > 0 { parts.join(" ") } else { "fail" }
      raise StarlarkError::ValueError(message="fail: " + msg, span=builtin_span)
    }),
  )

  // getattr(x, name, default?) - get attribute (stub)
  builtins.set(
    "getattr",
    @value.Value::BuiltinFunction("getattr", fn(pos, _kw) raise {
      guard pos.length() >= 2 && pos.length() <= 3 else {
        raise StarlarkError::TypeError(
          message="getattr() takes 2 or 3 arguments (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      if pos.length() == 3 {
        return pos[2]
      }
      let name = match pos[1] {
        String(s) => s
        _ =>
          raise StarlarkError::TypeError(
            message="getattr() attribute name must be a string",
            span=builtin_span,
          )
      }
      raise StarlarkError::TypeError(
        message="'\{pos[0].type_name()}' has no attribute '\{name}'",
        span=builtin_span,
      )
    }),
  )

  // hasattr(x, name) - check attribute (stub: always false)
  builtins.set(
    "hasattr",
    @value.Value::BuiltinFunction("hasattr", fn(pos, _kw) raise {
      guard pos.length() == 2 else {
        raise StarlarkError::TypeError(
          message="hasattr() takes exactly 2 arguments (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      @value.Value::Bool(false)
    }),
  )

  // hash(x) - hash of string
  builtins.set(
    "hash",
    @value.Value::BuiltinFunction("hash", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="hash() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      match pos[0] {
        String(s) => @value.Value::Int(string_hash(s))
        _ =>
          raise StarlarkError::TypeError(
            message="unhashable type: '\{pos[0].type_name()}'",
            span=builtin_span,
          )
      }
    }),
  )

  // int(x, base?) - convert to int
  builtins.set(
    "int",
    @value.Value::BuiltinFunction("int", fn(pos, _kw) raise {
      if pos.length() == 0 {
        return @value.Value::Int(0L)
      }
      guard pos.length() <= 2 else {
        raise StarlarkError::TypeError(
          message="int() takes at most 2 arguments (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let base : Int = if pos.length() == 2 {
        match pos[1] {
          Int(n) => n.to_int()
          _ =>
            raise StarlarkError::TypeError(
              message="int() second argument must be an int",
              span=builtin_span,
            )
        }
      } else {
        10
      }
      match pos[0] {
        Int(n) => {
          if pos.length() == 2 {
            raise StarlarkError::TypeError(
              message="int() can't convert non-string with explicit base",
              span=builtin_span,
            )
          }
          @value.Value::Int(n)
        }
        Bool(b) => {
          if pos.length() == 2 {
            raise StarlarkError::TypeError(
              message="int() can't convert non-string with explicit base",
              span=builtin_span,
            )
          }
          @value.Value::Int(if b { 1L } else { 0L })
        }
        String(s) => @value.Value::Int(parse_int_with_base(s, base))
        _ =>
          raise StarlarkError::TypeError(
            message="int() argument must be a string or a number, not '" +
              pos[0].type_name() +
              "'",
            span=builtin_span,
          )
      }
    }),
  )

  // len(x) - length of string/list/tuple/dict
  builtins.set(
    "len",
    @value.Value::BuiltinFunction("len", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="len() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      match pos[0] {
        String(s) => @value.Value::Int(s.length().to_int64())
        List(items) => @value.Value::Int(items.val.length().to_int64())
        Tuple(items) => @value.Value::Int(items.length().to_int64())
        Dict(map) => @value.Value::Int(map.val.length().to_int64())
        _ =>
          raise StarlarkError::TypeError(
            message="object of type '" + pos[0].type_name() + "' has no len()",
            span=builtin_span,
          )
      }
    }),
  )

  // list(x?) - create list from iterable; list() -> []
  builtins.set(
    "list",
    @value.Value::BuiltinFunction("list", fn(pos, _kw) raise {
      if pos.length() == 0 {
        return @value.Value::List(Ref::new([]))
      }
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="list() takes at most one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let items = iterable_to_array_by_name(pos[0], "list")
      @value.Value::List(Ref::new(items))
    }),
  )

  // max(args) - maximum
  builtins.set(
    "max",
    @value.Value::BuiltinFunction("max", fn(pos, _kw) raise {
      guard pos.length() > 0 else {
        raise StarlarkError::TypeError(
          message="max() requires at least one argument",
          span=builtin_span,
        )
      }
      let items : Array[@value.Value] = if pos.length() == 1 {
        iterable_to_array_by_name(pos[0], "max")
      } else {
        pos
      }
      guard items.length() > 0 else {
        raise StarlarkError::ValueError(
          message="max() arg is an empty sequence",
          span=builtin_span,
        )
      }
      let mut best = items[0]
      for i = 1; i < items.length(); i = i + 1 {
        if builtin_compare(items[i], best) > 0 {
          best = items[i]
        }
      }
      best
    }),
  )

  // min(args) - minimum
  builtins.set(
    "min",
    @value.Value::BuiltinFunction("min", fn(pos, _kw) raise {
      guard pos.length() > 0 else {
        raise StarlarkError::TypeError(
          message="min() requires at least one argument",
          span=builtin_span,
        )
      }
      let items : Array[@value.Value] = if pos.length() == 1 {
        iterable_to_array_by_name(pos[0], "min")
      } else {
        pos
      }
      guard items.length() > 0 else {
        raise StarlarkError::ValueError(
          message="min() arg is an empty sequence",
          span=builtin_span,
        )
      }
      let mut best = items[0]
      for i = 1; i < items.length(); i = i + 1 {
        if builtin_compare(items[i], best) < 0 {
          best = items[i]
        }
      }
      best
    }),
  )

  // print(*args, sep=" ") - print to stderr, return None
  builtins.set(
    "print",
    @value.Value::BuiltinFunction("print", fn(pos, kw) raise {
      let sep = match kw.get("sep") {
        Some(String(s)) => s
        Some(_) =>
          raise StarlarkError::TypeError(
            message="print() sep must be a string",
            span=builtin_span,
          )
        None => " "
      }
      let parts : Array[String] = []
      for arg in pos {
        parts.push(value_to_str(arg))
      }
      println(parts.join(sep))
      @value.Value::None
    }),
  )

  // range(stop) / range(start, stop) / range(start, stop, step)
  builtins.set(
    "range",
    @value.Value::BuiltinFunction("range", fn(pos, _kw) raise {
      guard pos.length() >= 1 && pos.length() <= 3 else {
        raise StarlarkError::TypeError(
          message="range() requires 1 to 3 arguments (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let params : (Int64, Int64, Int64) = if pos.length() == 1 {
        match pos[0] {
          Int(n) => (0L, n, 1L)
          _ =>
            raise StarlarkError::TypeError(
              message="range() argument must be an int",
              span=builtin_span,
            )
        }
      } else if pos.length() == 2 {
        match (pos[0], pos[1]) {
          (Int(a), Int(b)) => (a, b, 1L)
          _ =>
            raise StarlarkError::TypeError(
              message="range() arguments must be ints",
              span=builtin_span,
            )
        }
      } else {
        match (pos[0], pos[1], pos[2]) {
          (Int(a), Int(b), Int(c)) => {
            if c == 0L {
              raise StarlarkError::ValueError(
                message="range() step argument must not be zero",
                span=builtin_span,
              )
            }
            (a, b, c)
          }
          _ =>
            raise StarlarkError::TypeError(
              message="range() arguments must be ints",
              span=builtin_span,
            )
        }
      }
      let (start, stop, step) = params
      let result : Array[@value.Value] = []
      if step > 0L {
        let mut i = start
        while i < stop {
          result.push(@value.Value::Int(i))
          i = i + step
        }
      } else {
        let mut i = start
        while i > stop {
          result.push(@value.Value::Int(i))
          i = i + step
        }
      }
      @value.Value::List(Ref::new(result))
    }),
  )

  // repr(x) - string representation (strings get quoted)
  builtins.set(
    "repr",
    @value.Value::BuiltinFunction("repr", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="repr() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      @value.Value::String(value_to_repr(pos[0]))
    }),
  )

  // reversed(x) - new reversed list
  builtins.set(
    "reversed",
    @value.Value::BuiltinFunction("reversed", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="reversed() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let items = iterable_to_array_by_name(pos[0], "reversed")
      let len = items.length()
      for i = 0; i < len / 2; i = i + 1 {
        let tmp = items[i]
        items[i] = items[len - 1 - i]
        items[len - 1 - i] = tmp
      }
      @value.Value::List(Ref::new(items))
    }),
  )

  // sorted(x, reverse=False) - new sorted list (stable sort)
  builtins.set(
    "sorted",
    @value.Value::BuiltinFunction("sorted", fn(pos, kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="sorted() takes exactly one positional argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let reverse = match kw.get("reverse") {
        Some(Bool(b)) => b
        Some(Int(n)) => n != 0L
        Some(_) =>
          raise StarlarkError::TypeError(
            message="sorted() reverse must be a bool",
            span=builtin_span,
          )
        None => false
      }
      let items = iterable_to_array_by_name(pos[0], "sorted")
      // Insertion sort for stability
      for i = 1; i < items.length(); i = i + 1 {
        let key = items[i]
        let mut j = i - 1
        while j >= 0 {
          let cmp = builtin_compare(items[j], key)
          let should_swap = if reverse { cmp < 0 } else { cmp > 0 }
          if should_swap {
            items[j + 1] = items[j]
            j = j - 1
          } else {
            break
          }
        }
        items[j + 1] = key
      }
      @value.Value::List(Ref::new(items))
    }),
  )

  // str(x) - convert to string (no quotes on strings)
  builtins.set(
    "str",
    @value.Value::BuiltinFunction("str", fn(pos, _kw) raise {
      if pos.length() == 0 {
        return @value.Value::String("")
      }
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="str() takes at most one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      @value.Value::String(value_to_str(pos[0]))
    }),
  )

  // tuple(x?) - create tuple from iterable; tuple() -> ()
  builtins.set(
    "tuple",
    @value.Value::BuiltinFunction("tuple", fn(pos, _kw) raise {
      if pos.length() == 0 {
        return @value.Value::Tuple(FixedArray::make(0, @value.Value::None))
      }
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="tuple() takes at most one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      let items = iterable_to_array_by_name(pos[0], "tuple")
      @value.Value::Tuple(FixedArray::from_array(items))
    }),
  )

  // type(x) - type name string
  builtins.set(
    "type",
    @value.Value::BuiltinFunction("type", fn(pos, _kw) raise {
      guard pos.length() == 1 else {
        raise StarlarkError::TypeError(
          message="type() takes exactly one argument (\{pos.length()} given)",
          span=builtin_span,
        )
      }
      @value.Value::String(pos[0].type_name())
    }),
  )

  // zip(*iterables) - list of tuples, length = shortest input
  builtins.set(
    "zip",
    @value.Value::BuiltinFunction("zip", fn(pos, _kw) raise {
      if pos.length() == 0 {
        return @value.Value::List(Ref::new([]))
      }
      let arrays : Array[Array[@value.Value]] = []
      for arg in pos {
        arrays.push(iterable_to_array_by_name(arg, "zip"))
      }
      let mut min_len = arrays[0].length()
      for i = 1; i < arrays.length(); i = i + 1 {
        if arrays[i].length() < min_len {
          min_len = arrays[i].length()
        }
      }
      let result : Array[@value.Value] = []
      for i = 0; i < min_len; i = i + 1 {
        let tuple_items : Array[@value.Value] = []
        for j = 0; j < arrays.length(); j = j + 1 {
          tuple_items.push(arrays[j][i])
        }
        result.push(@value.Value::Tuple(FixedArray::from_array(tuple_items)))
      }
      @value.Value::List(Ref::new(result))
    }),
  )
  builtins
}