///|
/// Decodes a single slice bound (start/end/step) to an int32-range `Int`.
/// `None` propagates the omitted-bound default; an out-of-range or non-int
/// value raises with `label` ("slice step" / "start index" / "end index").
fn slice_int_arg(
  ctx : EvalContext,
  v : @value.Value,
  label : String,
) -> Int? raise EvalErr {
  match v {
    @value.Value::None => None
    @value.Value::Int(n) =>
      if n > max_int32 || n < min_int32 {
        raise EvalErr(
          make_eval_error(ctx, "invalid \{label}: \{n} out of range"),
        )
      } else {
        Some(n.to_int())
      }
    _ =>
      raise EvalErr(
        make_eval_error(ctx, "invalid \{label}: got \{v.type_name()}, want int"),
      )
  }
}

///|
/// Sorts attribute names by Starlark string ordering. Used by `dir()` for
/// modules and custom values, whose declaration order is otherwise arbitrary.
fn sort_starlark_names(names : Array[String]) -> Array[String] {
  names.sort_by(fn(a, b) {
    let av = @value.Value::String(@value.StarlarkString::new(a))
    let bv = @value.Value::String(@value.StarlarkString::new(b))
    match @value.compare_values(av, bv) {
      Ok(c) => c
      Err(_) => 0
    }
  })
  names
}

///|
/// The method names available on each built-in type, in `dir()` order.
/// Single source of truth shared by attribute-access validation (`getattr`,
/// `hasattr`) and the `dir()` builtin. Returns `None` for types with no
/// methods.
fn builtin_type_methods(v : @value.Value) -> Array[String]? {
  match v {
    @value.Value::String(_) =>
      Some([
        "capitalize", "codepoint_ords", "codepoints", "count", "elem_ords", "elems",
        "endswith", "find", "format", "index", "isalnum", "isalpha", "isdigit", "islower",
        "isspace", "istitle", "isupper", "join", "lower", "lstrip", "partition",
        "removeprefix", "removesuffix", "replace", "rfind", "rindex", "rpartition",
        "rsplit", "rstrip", "split", "splitlines", "startswith", "strip", "title",
        "upper",
      ])
    @value.Value::List(_) =>
      Some(["append", "clear", "extend", "index", "insert", "pop", "remove"])
    @value.Value::Dict(_) =>
      Some([
        "clear", "get", "items", "keys", "pop", "popitem", "setdefault", "update",
        "values",
      ])
    @value.Value::Set(_) =>
      Some([
        "add", "clear", "difference", "discard", "intersection", "issubset", "issuperset",
        "pop", "remove", "symmetric_difference", "union", "update",
      ])
    @value.Value::Bytes(_) => Some(["elems"])
    _ => None
  }
}

///|
/// Looks up attribute `attr` on `obj`. Built-in types (string, list, dict,
/// set, bytes) return a `BoundMethod` for valid method names; modules consult
/// their attribute map; `ExtVal` delegates to its `get_attr` hook. Provides a
/// did-you-mean hint when the attribute name is misspelled.
fn eval_getattr(
  ctx : EvalContext,
  obj : @value.Value,
  attr : String,
  _pos : @errors.Position,
) -> @value.Value raise EvalErr {
  match obj {
    @value.Value::String(_)
    | @value.Value::List(_)
    | @value.Value::Dict(_)
    | @value.Value::Set(_)
    | @value.Value::Bytes(_) => {
      let methods = (builtin_type_methods(obj) : Array[String]?)
      match methods {
        Some(ms) =>
          if ms.contains(attr) {
            @value.Value::BoundMethod(
              @value.StarlarkBoundMethod::new(obj, attr),
            )
          } else {
            let hint = @utf8util.spell_hint(attr, ms, prefix=".")
            raise EvalErr(
              make_eval_error(
                ctx,
                "\{obj.type_name()} has no .\{attr} field or method\{hint}",
              ),
            )
          }
        None =>
          @value.Value::BoundMethod(@value.StarlarkBoundMethod::new(obj, attr))
      }
    }
    @value.Value::Module(m) =>
      match m.get(attr) {
        Some(v) => v
        None => {
          let candidates = m.attr_names()
          let hint = @utf8util.spell_hint(attr, candidates, prefix=".")
          raise EvalErr(
            make_eval_error(
              ctx,
              "module has no .\{attr} field or method\{hint}",
            ),
          )
        }
      }
    @value.Value::ExtVal(c) =>
      match c.get_attr(attr) {
        Ok(Some(v)) => v
        Ok(None) =>
          raise EvalErr(
            make_eval_error(
              ctx,
              "\{obj.type_name()} has no .\{attr} field or method",
            ),
          )
        Err(e) => raise EvalErr(make_eval_error(ctx, e))
      }
    _ =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{obj.type_name()} has no .\{attr} field or method",
        ),
      )
  }
}

///|
/// Invokes `func_val` with positional `pos_args` and keyword `kw_args`.
/// Compiled functions run on the VM (which must be present during bytecode
/// dispatch); builtins and bound methods dispatch through the shared call
/// mechanism; `ExtVal` delegates to its `do_call` hook.
fn call_value(
  ctx : EvalContext,
  func_val : @value.Value,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
  pos : @errors.Position,
) -> @value.Value raise EvalErr {
  let depth = ctx.thread.call_stack.length()
  if depth > 0 {
    let caller = ctx.thread.call_stack[depth - 1]
    ctx.thread.call_stack[depth - 1] = @errors.CallFrame::new(
      caller.name(),
      pos,
    )
  }
  match func_val {
    // Every function is bytecode-compiled and runs on a VM frame. This path is
    // reached when the VM drives execution and dispatches a call through the
    // value-level entry (a builtin callback such as `sorted(iterable, key)`, or
    // a direct `Call` opcode), so the VM is always present.
    @value.Value::Function(f) =>
      match (f.compiled_funcode(), ctx.vm) {
        (Some(fc), Some(vm)) => vm.call_compiled(f, fc, pos_args, kw_args, pos)
        _ =>
          abort("internal: function is not bytecode-compiled or VM is absent")
      }
    @value.Value::Builtin(f) => {
      let builtin_frame = @errors.CallFrame::new(
        f.name(),
        @errors.Position::new("", 0, 0),
      )
      ctx.thread.call_stack.push(builtin_frame)
      let call_ctx = @value.BuiltinCallCtx::new(
        fn(f2, args, kwargs) {
          Ok(call_value(ctx, f2, args, kwargs, pos)) catch {
            EvalErr(e) => Err(e.msg())
          }
        },
        get_local=fn(k) { ctx.thread.get_local(k) },
      )
      let result : @value.Value = try {
        match f.call_body(call_ctx, pos_args, kw_args) {
          Some(Ok(v)) => v
          Some(Err(msg)) => raise EvalErr(make_eval_error(ctx, msg))
          None => call_builtin(ctx, f.name(), pos_args, kw_args, pos)
        }
      } catch {
        EvalErr(e) => {
          ignore(ctx.thread.call_stack.pop())
          raise EvalErr(e)
        }
      }
      ignore(ctx.thread.call_stack.pop())
      result
    }
    @value.Value::BoundMethod(m) => {
      let builtin_frame = @errors.CallFrame::new(
        m.method_name(),
        @errors.Position::new("", 0, 0),
      )
      ctx.thread.call_stack.push(builtin_frame)
      let result : @value.Value = call_method(
        ctx,
        m.recv(),
        m.method_name(),
        pos_args,
        kw_args,
        pos,
      ) catch {
        EvalErr(e) => {
          ignore(ctx.thread.call_stack.pop())
          raise EvalErr(e)
        }
      }
      ignore(ctx.thread.call_stack.pop())
      result
    }
    @value.Value::ExtVal(c) =>
      match c.do_call(pos_args, kw_args) {
        Some(Ok(v)) => v
        Some(Err(msg)) => raise EvalErr(make_eval_error(ctx, msg))
        None =>
          raise EvalErr(
            make_eval_error(
              ctx,
              "invalid call of non-function (\{c.get_type_name()})",
            ),
          )
      }
    _ =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "invalid call of non-function (\{func_val.type_name()})",
        ),
      )
  }
}

///|
/// Dispatches a call to a predeclared built-in function by `name`. Acts as
/// the single registry for all standard Starlark builtins: `print`, `len`,
/// `str`, `repr`, `type`, `bool`, `int`, `float`, `abs`, `range`, `list`,
/// `tuple`, `dict`, `set`, `bytes`, `min`, `max`, `enumerate`, `sorted`,
/// `reversed`, `zip`, `any`, `all`, `hash`, `dir`, `chr`, `ord`, `fail`,
/// `hasattr`, and `getattr`.
fn call_builtin(
  ctx : EvalContext,
  name : String,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
  _pos : @errors.Position,
) -> @value.Value raise EvalErr {
  match name {
    "print" => {
      let sep_bytes = {
        let mut found : Bytes = b" "
        for kv in kw_args {
          let (k, v) = kv
          match k {
            "sep" =>
              match v {
                @value.Value::String(s) => found = s.to_bytes()
                _ =>
                  raise EvalErr(
                    make_eval_error(
                      ctx,
                      "print: for parameter \"\{k}\": got \{v.type_name()}, want string",
                    ),
                  )
              }
            other => {
              let hint = @utf8util.spell_hint(other, ["sep"])
              raise EvalErr(
                make_eval_error(
                  ctx,
                  "print: unexpected keyword argument \"\{other}\"\{hint}",
                ),
              )
            }
          }
        }
        found
      }
      let buf = @buffer.Buffer::Buffer()
      let mut first = true
      for arg in pos_args {
        if !first {
          buf.write_bytes(sep_bytes[:])
        }
        first = false
        match arg {
          @value.Value::String(s) => buf.write_bytes(s.to_bytes()[:])
          @value.Value::Bytes(b) => buf.write_bytes(b[:])
          _ => buf.write_string_utf8(check(ctx, arg.to_str_checked()))
        }
      }
      (ctx.thread.print_fn)(ctx.thread, buf.contents())
      @value.Value::None
    }
    "len" => {
      check_positional(ctx, "len", pos_args, kw_args, 1, 1)
      let n = check(ctx, @value.length_of(pos_args[0]))
      @value.Value::Int(BigInt::from_int64(n))
    }
    "str" => {
      reject_kwargs(ctx, "str", kw_args)
      if pos_args.length() == 0 {
        raise EvalErr(
          make_eval_error(ctx, "str: got 0 arguments, want exactly 1"),
        )
      } else if pos_args.length() == 1 {
        match pos_args[0] {
          @value.Value::String(s) => @value.Value::String(s)
          @value.Value::Bytes(b) =>
            // starlark-go's str(bytes) replaces invalid encodings with U+FFFD
            // using Go's per-byte decoder, not the WHATWG maximal-subpart rule.
            @value.Value::String(
              @value.StarlarkString::new(@utf8util.decode_utf8_lossy(b)),
            )
          v =>
            @value.Value::String(
              @value.StarlarkString::new(check(ctx, v.to_str_checked())),
            )
        }
      } else {
        raise EvalErr(
          make_eval_error(
            ctx,
            "str: got \{pos_args.length()} arguments, want exactly 1",
          ),
        )
      }
    }
    "repr" => {
      check_positional(ctx, "repr", pos_args, kw_args, 1, 1)
      @value.Value::String(
        @value.StarlarkString::new(check(ctx, pos_args[0].repr_checked())),
      )
    }
    "type" => {
      reject_kwargs(ctx, "type", kw_args)
      if pos_args.length() != 1 {
        raise EvalErr(make_eval_error(ctx, "type() takes exactly one argument"))
      }
      @value.Value::String(@value.StarlarkString::new(pos_args[0].type_name()))
    }
    "bool" => {
      check_positional(ctx, "bool", pos_args, kw_args, 0, 1)
      if pos_args.length() == 0 {
        @value.Value::Bool(false)
      } else {
        @value.Value::Bool(pos_args[0].truth())
      }
    }
    "int" => {
      if pos_args.length() > 2 {
        raise EvalErr(
          make_eval_error(
            ctx,
            "int: got \{pos_args.length()} arguments, want at most 2",
          ),
        )
      }
      let mut x_from_kw : @value.Value? = None
      let mut base_from_kw : @value.Value? = None
      for kv in kw_args {
        match kv.0 {
          "x" => {
            if pos_args.length() >= 1 {
              raise EvalErr(
                make_eval_error(
                  ctx, "int: got multiple values for keyword argument \"x\"",
                ),
              )
            }
            x_from_kw = Some(kv.1)
          }
          "base" => {
            if pos_args.length() >= 2 {
              raise EvalErr(
                make_eval_error(
                  ctx, "int: got multiple values for keyword argument \"base\"",
                ),
              )
            }
            base_from_kw = Some(kv.1)
          }
          name => {
            let hint = @utf8util.spell_hint(name, ["x", "base"])
            raise EvalErr(
              make_eval_error(
                ctx,
                "int: unexpected keyword argument \"\{name}\"\{hint}",
              ),
            )
          }
        }
      }
      let x_val = match x_from_kw {
        Some(v) => v
        None =>
          if pos_args.length() >= 1 {
            pos_args[0]
          } else {
            raise EvalErr(make_eval_error(ctx, "int: missing argument for x"))
          }
      }
      let base_arg : @value.Value? = match base_from_kw {
        Some(v) => Some(v)
        None => if pos_args.length() >= 2 { Some(pos_args[1]) } else { None }
      }
      match x_val {
        @value.Value::String(s) => {
          let base = match base_arg {
            None => 10
            Some(@value.Value::Int(b)) => b.to_int()
            Some(v) =>
              raise EvalErr(
                make_eval_error(
                  ctx,
                  "int: for base, got \{v.type_name()}, want int",
                ),
              )
          }
          if base != 0 && (base < 2 || base > 36) {
            raise EvalErr(
              make_eval_error(ctx, "int: base must be an integer >= 2 && <= 36"),
            )
          }
          let n = match parse_int_str(s.raw(), base) {
            Ok(n) => n
            Err(msg) => raise EvalErr(make_eval_error(ctx, "int: " + msg))
          }
          @value.Value::Int(n)
        }
        other =>
          match base_arg {
            Some(_) =>
              raise EvalErr(
                make_eval_error(
                  ctx, "int: can't convert non-string with explicit base",
                ),
              )
            None =>
              match other {
                @value.Value::Int(n) => @value.Value::Int(n)
                @value.Value::Float(f) =>
                  if f.is_nan() {
                    raise EvalErr(
                      make_eval_error(
                        ctx, "int: cannot convert float NaN to integer",
                      ),
                    )
                  } else if f.is_inf() {
                    raise EvalErr(
                      make_eval_error(
                        ctx, "int: cannot convert float infinity to integer",
                      ),
                    )
                  } else {
                    @value.Value::Int(@numeric.double_to_bigint(f.trunc()))
                  }
                @value.Value::Bool(b) =>
                  @value.Value::Int(if b { 1N } else { 0N })
                v =>
                  raise EvalErr(
                    make_eval_error(
                      ctx,
                      "int: cannot convert \{v.type_name()} to int",
                    ),
                  )
              }
          }
      }
    }
    "float" => {
      if kw_args.length() > 0 {
        raise EvalErr(
          make_eval_error(ctx, "float does not accept keyword arguments"),
        )
      }
      if pos_args.length() > 1 {
        raise EvalErr(
          make_eval_error(
            ctx,
            "float got \{pos_args.length()} arguments, wants 1",
          ),
        )
      }
      if pos_args.length() == 0 {
        @value.Value::Float(0.0)
      } else {
        match pos_args[0] {
          @value.Value::Float(f) => @value.Value::Float(f)
          @value.Value::Int(n) =>
            match @numeric.bigint_to_finite_double(n) {
              Ok(f) => @value.Value::Float(f)
              Err(msg) => raise EvalErr(make_eval_error(ctx, msg))
            }
          @value.Value::Bool(b) =>
            @value.Value::Float(if b { 1.0 } else { 0.0 })
          @value.Value::String(s) => {
            let raw = s.raw()
            if raw.length() == 0 {
              raise EvalErr(make_eval_error(ctx, "float: empty string"))
            }
            let f = match raw.to_lower() {
              "inf" | "+inf" | "infinity" | "+infinity" => @double.infinity
              "-inf" | "-infinity" => @double.neg_infinity
              "nan" | "+nan" | "-nan" => @double.not_a_number
              _ =>
                match parse_hex_float_str(raw) {
                  Some(v) => {
                    if v.is_inf() {
                      raise EvalErr(
                        make_eval_error(ctx, "floating-point number too large"),
                      )
                    }
                    v
                  }
                  None => {
                    let parsed = @string.parse_double(raw) catch {
                      _ => {
                        let msg = if is_float_syntax(raw) {
                          "floating-point number too large"
                        } else {
                          "invalid float literal: \{raw}"
                        }
                        raise EvalErr(make_eval_error(ctx, msg))
                      }
                    }
                    if parsed.is_inf() {
                      raise EvalErr(
                        make_eval_error(ctx, "floating-point number too large"),
                      )
                    }
                    parsed
                  }
                }
            }
            @value.Value::Float(f)
          }
          v =>
            raise EvalErr(
              make_eval_error(
                ctx,
                "float got \{v.type_name()}, want number or string",
              ),
            )
        }
      }
    }
    "abs" => {
      check_positional(ctx, "abs", pos_args, kw_args, 1, 1)
      match pos_args[0] {
        @value.Value::Int(n) => @value.Value::Int(if n < 0N { -n } else { n })
        @value.Value::Float(f) => @value.Value::Float(f.abs())
        v =>
          raise EvalErr(
            make_eval_error(ctx, "got \{v.type_name()}, want int or float"),
          )
      }
    }
    "range" => builtin_range(ctx, pos_args, kw_args)
    "list" => {
      check_positional(ctx, "list", pos_args, kw_args, 0, 1)
      builtin_list(ctx, pos_args)
    }
    "tuple" => {
      check_positional(ctx, "tuple", pos_args, kw_args, 0, 1)
      builtin_tuple(ctx, pos_args)
    }
    "dict" => builtin_dict(ctx, pos_args, kw_args)
    "set" => {
      check_positional(ctx, "set", pos_args, kw_args, 0, 1)
      builtin_set(ctx, pos_args)
    }
    "bytes" => {
      reject_kwargs(ctx, "bytes", kw_args)
      builtin_bytes(ctx, pos_args)
    }
    "min" => builtin_minmax(ctx, pos_args, kw_args, false)
    "max" => builtin_minmax(ctx, pos_args, kw_args, true)
    "enumerate" => builtin_enumerate(ctx, pos_args, kw_args)
    "sorted" => builtin_sorted(ctx, pos_args, kw_args)
    "reversed" => {
      check_positional(ctx, "reversed", pos_args, kw_args, 1, 1)
      builtin_reversed(ctx, pos_args)
    }
    "zip" => {
      reject_kwargs(ctx, "zip", kw_args)
      builtin_zip(ctx, pos_args)
    }
    "any" => {
      check_positional(ctx, "any", pos_args, kw_args, 1, 1)
      builtin_any_all(ctx, pos_args, true)
    }
    "all" => {
      check_positional(ctx, "all", pos_args, kw_args, 1, 1)
      builtin_any_all(ctx, pos_args, false)
    }
    "hash" => {
      check_positional(ctx, "hash", pos_args, kw_args, 1, 1)
      match pos_args[0] {
        @value.Value::String(s) =>
          @value.Value::Int(
            BigInt::from_int64(
              @numeric.java_string_hash(s.to_bytes()).to_int64(),
            ),
          )
        @value.Value::Bytes(b) => {
          let h = check(ctx, @value.Value::Bytes(b).hash())
          @value.Value::Int(BigInt::from_int64(h.to_int64()))
        }
        v =>
          raise EvalErr(
            make_eval_error(
              ctx,
              "hash: got \{v.type_name()}, want string or bytes",
            ),
          )
      }
    }
    "dir" => {
      reject_kwargs(ctx, "dir", kw_args)
      if pos_args.length() != 1 {
        raise EvalErr(
          make_eval_error(
            ctx,
            "dir: got \{pos_args.length()} arguments, want 1",
          ),
        )
      }
      let names : Array[String] = match builtin_type_methods(pos_args[0]) {
        Some(ms) => ms
        None =>
          match pos_args[0] {
            @value.Value::Module(m) => sort_starlark_names(m.attr_names())
            @value.Value::ExtVal(c) =>
              match c.get_attr_names() {
                Some(names) => sort_starlark_names(names)
                None => []
              }
            _ => []
          }
      }
      let items = names.map(fn(n) {
        @value.Value::String(@value.StarlarkString::new(n))
      })
      @value.Value::List(@value.StarlarkList::new(items))
    }
    "chr" => {
      reject_kwargs(ctx, "chr", kw_args)
      if pos_args.length() != 1 {
        raise EvalErr(
          make_eval_error(
            ctx,
            "chr: got \{pos_args.length()} arguments, want 1",
          ),
        )
      }
      match pos_args[0] {
        @value.Value::Int(n) => {
          if n < 0N {
            raise EvalErr(
              make_eval_error(
                ctx,
                "chr: Unicode code point \{n} out of range (<0)",
              ),
            )
          } else if n > unicode_max_cp_bigint {
            raise EvalErr(
              make_eval_error(
                ctx,
                "chr: Unicode code point U+\{int64_to_hex(n, true)} out of range (>0x10FFFF)",
              ),
            )
          }
          let cp = n.to_int()
          let c = if cp >= unicode_surr_first && cp <= unicode_surr_last {
            '\u{FFFD}'
          } else {
            cp.unsafe_to_char()
          }
          let buf = StringBuilder::new()
          buf.write_char(c)
          @value.Value::String(@value.StarlarkString::new(buf.to_string()))
        }
        v =>
          raise EvalErr(
            make_eval_error(ctx, "chr: got \{v.type_name()}, want int"),
          )
      }
    }
    "ord" => {
      reject_kwargs(ctx, "ord", kw_args)
      if pos_args.length() != 1 {
        raise EvalErr(
          make_eval_error(
            ctx,
            "ord: got \{pos_args.length()} arguments, want 1",
          ),
        )
      }
      match pos_args[0] {
        @value.Value::String(s) => {
          let chars = s.raw().to_array()
          if chars.length() != 1 {
            raise EvalErr(
              make_eval_error(
                ctx,
                "ord: string encodes \{chars.length()} Unicode code points, want 1",
              ),
            )
          }
          @value.Value::Int(BigInt::from_int(chars[0].to_int()))
        }
        @value.Value::Bytes(b) => {
          if b.length() != 1 {
            raise EvalErr(
              make_eval_error(
                ctx,
                "ord: bytes has length \{b.length()}, want 1",
              ),
            )
          }
          @value.Value::Int(BigInt::from_int(b[0].to_int()))
        }
        v =>
          raise EvalErr(
            make_eval_error(
              ctx,
              "ord: got \{v.type_name()}, want string or bytes",
            ),
          )
      }
    }
    "fail" => {
      let sep = {
        let mut s = " "
        for kv in kw_args {
          let (k, v) = kv
          match k {
            "sep" =>
              match v {
                @value.Value::String(sv) => s = sv.raw()
                _ =>
                  raise EvalErr(
                    make_eval_error(
                      ctx,
                      "fail: for parameter \"sep\": got \{v.type_name()}, want string",
                    ),
                  )
              }
            other => {
              let hint = @utf8util.spell_hint(other, ["sep"])
              raise EvalErr(
                make_eval_error(
                  ctx,
                  "fail: unexpected keyword argument \"\{other}\"\{hint}",
                ),
              )
            }
          }
        }
        s
      }
      let body = if pos_args.length() > 0 {
        let parts : Array[String] = []
        for v in pos_args {
          parts.push(check(ctx, v.to_str_checked()))
        }
        parts.join(sep)
      } else {
        ""
      }
      raise EvalErr(make_eval_error(ctx, "fail: \{body}"))
    }
    "hasattr" => {
      check_positional(ctx, "hasattr", pos_args, kw_args, 2, 2)
      let s = arg_as_string(ctx, "hasattr", pos_args, 1)
      let dummy_pos = @errors.Position::new("", 0, 0)
      let found = try {
        ignore(eval_getattr(ctx, pos_args[0], s.raw(), dummy_pos))
        true
      } catch {
        EvalErr(_) => false
      }
      @value.Value::Bool(found)
    }
    "getattr" => {
      check_positional(ctx, "getattr", pos_args, kw_args, 2, 3)
      let s = arg_as_string(ctx, "getattr", pos_args, 1)
      let dummy_pos = @errors.Position::new("", 0, 0)
      eval_getattr(ctx, pos_args[0], s.raw(), dummy_pos) catch {
        EvalErr(_) =>
          if pos_args.length() >= 3 {
            pos_args[2]
          } else {
            raise EvalErr(
              make_eval_error(
                ctx,
                "getattr: \{pos_args[0].type_name()} has no .\{s.raw()} field or method",
              ),
            )
          }
      }
    }
    _ =>
      raise EvalErr(make_eval_error(ctx, "unknown built-in function '\{name}'"))
  }
}

///|
/// Raises an out-of-range `EvalError` when `b` does not fit in a signed 64-bit
/// integer; `msg` carries the full caller-specific diagnostic.
fn check_int64_range(
  ctx : EvalContext,
  b : BigInt,
  msg : String,
) -> Unit raise EvalErr {
  if b.compare_int64(@int64.MAX_VALUE) > 0 ||
    b.compare_int64(@int64.MIN_VALUE) < 0 {
    raise EvalErr(make_eval_error(ctx, msg))
  }
}

///|
/// Validates that `b` fits in a signed 64-bit integer and returns it,
/// enforcing the int64-range constraint on `range` arguments.
fn range_arg_to_int64(ctx : EvalContext, b : BigInt) -> Int64 raise EvalErr {
  check_int64_range(
    ctx,
    b,
    "\{b} out of range (want value in signed 64-bit range)",
  )
  b.to_int64()
}

///|
/// Implements `range(stop)`, `range(start, stop)`, and
/// `range(start, stop, step)`. Validates that each argument is an in-range
/// integer and that `step != 0`.
fn builtin_range(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.Value raise EvalErr {
  check_positional(ctx, "range", pos_args, kw_args, 1, 3)
  let param = fn(i : Int) -> Int64 raise EvalErr {
    match pos_args[i] {
      @value.Value::Int(n) => range_arg_to_int64(ctx, n)
      v =>
        raise EvalErr(
          make_eval_error(
            ctx,
            "range: for parameter \{i + 1}: got \{v.type_name()}, want int",
          ),
        )
    }
  }
  let (start, stop, step) = match pos_args.length() {
    1 => (0L, param(0), 1L)
    2 => (param(0), param(1), 1L)
    _ => (param(0), param(1), param(2))
  }
  if step == 0L {
    raise EvalErr(make_eval_error(ctx, "range: step argument must not be zero"))
  }
  @value.Value::Range(@value.StarlarkRange::new(start, stop, step))
}

///|
/// Implements `list()`. With zero arguments returns an empty list; with one
/// argument, copies a list shallow or materializes any other iterable into a
/// new list.
fn builtin_list(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
) -> @value.Value raise EvalErr {
  if pos_args.length() == 0 {
    return @value.Value::List(@value.StarlarkList::new([]))
  }
  match pos_args[0] {
    @value.Value::List(l) => {
      let items : Array[@value.Value] = []
      for v in l.iter() {
        items.push(v)
      }
      @value.Value::List(@value.StarlarkList::new(items))
    }
    v => {
      guard_range_alloc(ctx, "list", v)
      let it = require_seq(ctx, "list", v)
      @value.Value::List(@value.StarlarkList::new(it.collect()))
    }
  }
}

///|
/// Implements `tuple()`. With zero arguments returns an empty tuple; with one
/// argument materializes any iterable into a tuple.
fn builtin_tuple(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
) -> @value.Value raise EvalErr {
  if pos_args.length() == 0 {
    return @value.Value::Tuple([])
  }
  guard_range_alloc(ctx, "tuple", pos_args[0])
  let it = require_seq(ctx, "tuple", pos_args[0])
  @value.Value::Tuple(it.collect())
}

///|
/// Common implementation of the `dict()` builtin and the `dict.update()`
/// method. Applies at most one positional argument (a dict or iterable of
/// 2-element pairs) then applies keyword arguments in order. `name` prefixes
/// every error message. Precondition: `pos_args.length() <= 1`.
fn update_dict(
  ctx : EvalContext,
  name : String,
  d : @value.StarlarkDict,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> Unit raise EvalErr {
  if pos_args.length() == 1 {
    match pos_args[0] {
      @value.Value::Dict(src) => {
        let pairs : Array[(@value.Value, @value.Value)] = []
        src.each(fn(k, v) { pairs.push((k, v)) })
        for pair in pairs {
          let (k, v) = pair
          check_pfx(ctx, name, d.set(k, v))
        }
      }
      v => {
        guard_range_alloc(ctx, name, v)
        let it = match @value.iterate(v) {
          Ok(i) => i
          Err(_) =>
            raise EvalErr(
              make_eval_error(
                ctx,
                "\{name}: got \{v.type_name()}, want iterable",
              ),
            )
        }
        let mut i = 0
        while true {
          match it.next() {
            None => {
              it.done()
              break
            }
            Some(pair) => {
              let pair_it = match @value.iterate(pair) {
                Ok(pi) => pi
                Err(_) => {
                  it.done()
                  raise EvalErr(
                    make_eval_error(
                      ctx,
                      "\{name}: dictionary update sequence element #\{i} is not iterable (\{pair.type_name()})",
                    ),
                  )
                }
              }
              let elems : Array[@value.Value] = []
              while true {
                match pair_it.next() {
                  None => {
                    pair_it.done()
                    break
                  }
                  Some(e) => elems.push(e)
                }
              }
              if elems.length() != 2 {
                it.done()
                raise EvalErr(
                  make_eval_error(
                    ctx,
                    "\{name}: dictionary update sequence element #\{i} has length \{elems.length()}, want 2",
                  ),
                )
              }
              check_pfx(ctx, name, d.set(elems[0], elems[1]))
              i = i + 1
            }
          }
        }
      }
    }
  }
  let kw_seen : Array[String] = []
  for kw_pair in kw_args {
    let (kw_name, kw_val) = kw_pair
    if kw_seen.contains(kw_name) {
      raise EvalErr(
        make_eval_error(ctx, "\{name}: duplicate keyword arg: \"\{kw_name}\""),
      )
    }
    kw_seen.push(kw_name)
    let key = @value.Value::String(@value.StarlarkString::new(kw_name))
    check_pfx(ctx, name, d.set(key, kw_val))
  }
}

///|
/// Implements `dict()`: enforces at most one positional argument, then
/// delegates to `update_dict` to apply positional and keyword arguments.
fn builtin_dict(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.Value raise EvalErr {
  if pos_args.length() > 1 {
    raise EvalErr(
      make_eval_error(
        ctx,
        "dict: got \{pos_args.length()} arguments, want at most 1",
      ),
    )
  }
  let d = @value.StarlarkDict::new()
  update_dict(ctx, "dict", d, pos_args, kw_args)
  @value.Value::Dict(d)
}

///|
/// Implements `set()`. With zero arguments returns an empty set; with one
/// argument iterates the argument and adds each element.
fn builtin_set(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
) -> @value.Value raise EvalErr {
  let s = @value.StarlarkSet::new()
  if pos_args.length() == 0 {
    return @value.Value::Set(s)
  }
  let iterable = pos_args[0]
  guard_range_alloc(ctx, "set", iterable)
  let it = match @value.iterate(iterable) {
    Err(_) =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "set: for parameter 1: got \{iterable.type_name()}, want iterable",
        ),
      )
    Ok(it) => it
  }
  while true {
    match it.next() {
      None => {
        it.done()
        break
      }
      Some(v) => check_pfx(ctx, "set", s.add(v))
    }
  }
  @value.Value::Set(s)
}

///|
/// Shared implementation of `min` and `max`. Accepts either a single iterable
/// or multiple positional arguments. The optional `key` callable is applied
/// before comparisons; ties keep the first encountered value.
/// `want_max=true` selects the maximum, `false` the minimum.
fn builtin_minmax(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
  want_max : Bool,
) -> @value.Value raise EvalErr {
  let fn_name = if want_max { "max" } else { "min" }
  if pos_args.length() == 0 {
    raise EvalErr(
      make_eval_error(
        ctx,
        "\{fn_name} requires at least one positional argument",
      ),
    )
  }
  let key_fn : @value.Value? = {
    let mut found : @value.Value? = None
    for kv in kw_args {
      match kv {
        ("key", f) => {
          found = Some(f)
          break
        }
        _ => ()
      }
    }
    found
  }
  for kv in kw_args {
    let (kw_name, _) = kv
    if kw_name != "key" {
      let hint = @utf8util.spell_hint(kw_name, ["key"])
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{fn_name}: unexpected keyword argument \"\{kw_name}\"\{hint}",
        ),
      )
    }
  }
  match key_fn {
    Some(f) =>
      match f {
        @value.Value::Function(_)
        | @value.Value::Builtin(_)
        | @value.Value::BoundMethod(_)
        | @value.Value::ExtVal(_) => ()
        v =>
          raise EvalErr(
            make_eval_error(
              ctx,
              "\{fn_name}: for parameter \"key\": got \{v.type_name()}, want callable",
            ),
          )
      }
    None => ()
  }
  let key_of : (@value.Value) -> @value.Value raise EvalErr = fn(
    v,
  ) raise EvalErr {
    match key_fn {
      None => v
      Some(f) =>
        call_value(ctx, f, [v], [], @errors.Position::new("", 0, 0))
    }
  }
  let cmp_op = if want_max { ">" } else { "<" }
  let items : Array[@value.Value] = []
  if pos_args.length() == 1 {
    let it = match @value.iterate(pos_args[0]) {
      Ok(it) => it
      Err(_) =>
        raise EvalErr(
          make_eval_error(
            ctx,
            "\{fn_name}: \{pos_args[0].type_name()} value is not iterable",
          ),
        )
    }
    while true {
      match it.next() {
        None => break
        Some(v) => items.push(v)
      }
    }
    if items.is_empty() {
      it.done()
      raise EvalErr(
        make_eval_error(ctx, "\{fn_name}: argument is an empty sequence"),
      )
    }
    let result = try {
      let mut best = items[0]
      let mut best_key = key_of(best)
      for i in 1.. 0) || (!want_max && c < 0) {
          best = items[i]
          best_key = item_key
        }
      }
      best
    } catch {
      e => {
        it.done()
        raise e
      }
    }
    it.done()
    result
  } else {
    for v in pos_args {
      items.push(v)
    }
    let mut result = items[0]
    let mut result_key = key_of(result)
    for i in 1.. 0) || (!want_max && c < 0) {
        result = items[i]
        result_key = item_key
      }
    }
    result
  }
}

///|
/// Implements `enumerate(iterable, start=0)`. Produces `(start+i, value)`
/// tuples; `start` must fit in a signed 64-bit integer.
fn builtin_enumerate(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.Value raise EvalErr {
  check_positional(ctx, "enumerate", pos_args, kw_args, 1, 2)
  let start = if pos_args.length() >= 2 {
    BigInt::from_int64(arg_as_int64(ctx, "enumerate", pos_args, 1))
  } else {
    0N
  }
  guard_range_alloc(ctx, "enumerate", pos_args[0])
  let result : Array[@value.Value] = []
  let it = match @value.iterate(pos_args[0]) {
    Ok(it) => it
    Err(_) =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "enumerate: for parameter 1: got \{pos_args[0].type_name()}, want iterable",
        ),
      )
  }
  let mut idx = start
  while true {
    match it.next() {
      None => {
        it.done()
        break
      }
      Some(v) => {
        result.push(@value.Value::Tuple([@value.Value::Int(idx), v]))
        idx = idx + 1N
      }
    }
  }
  @value.Value::List(@value.StarlarkList::new(result))
}

///|
/// Implements `sorted(iterable, key=None, reverse=False)`. All three
/// parameters are accepted positionally or by keyword. Ties are broken by
/// original index to guarantee stability regardless of the underlying
/// `sort_by` implementation.
fn builtin_sorted(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.Value raise EvalErr {
  if pos_args.length() > 3 {
    raise EvalErr(
      make_eval_error(
        ctx,
        "sorted: got \{pos_args.length()} arguments, want at most 3",
      ),
    )
  }
  // Python's sorted permits iterable, key, and reverse to be positional or
  // keyword, thus so do we. Track whether each was bound via kwarg so error
  // messages can quote kwargs names (starlark-go UnpackArgs parity).
  let mut iterable_val : @value.Value? = if pos_args.length() >= 1 {
    Some(pos_args[0])
  } else {
    None
  }
  let mut iterable_set = pos_args.length() >= 1
  let mut iterable_from_kwarg = false
  let mut key_fn : @value.Value? = if pos_args.length() >= 2 {
    Some(pos_args[1])
  } else {
    None
  }
  let mut key_set = pos_args.length() >= 2
  let mut key_from_kwarg = false
  let mut reverse_arg : @value.Value? = if pos_args.length() >= 3 {
    Some(pos_args[2])
  } else {
    None
  }
  let mut reverse_set = pos_args.length() >= 3
  let mut reverse_from_kwarg = false
  for kv in kw_args {
    match kv.0 {
      "iterable" => {
        if iterable_set {
          raise EvalErr(
            make_eval_error(
              ctx, "sorted: got multiple values for keyword argument \"iterable\"",
            ),
          )
        }
        iterable_val = Some(kv.1)
        iterable_set = true
        iterable_from_kwarg = true
      }
      "key" => {
        if key_set {
          raise EvalErr(
            make_eval_error(
              ctx, "sorted: got multiple values for keyword argument \"key\"",
            ),
          )
        }
        key_fn = Some(kv.1)
        key_set = true
        key_from_kwarg = true
      }
      "reverse" => {
        if reverse_set {
          raise EvalErr(
            make_eval_error(
              ctx, "sorted: got multiple values for keyword argument \"reverse\"",
            ),
          )
        }
        reverse_arg = Some(kv.1)
        reverse_set = true
        reverse_from_kwarg = true
      }
      other => {
        let hint = @utf8util.spell_hint(other, ["key", "reverse", "iterable"])
        raise EvalErr(
          make_eval_error(
            ctx,
            "sorted: unexpected keyword argument \"\{other}\"\{hint}",
          ),
        )
      }
    }
  }
  let iterable = match iterable_val {
    Some(v) => v
    None =>
      raise EvalErr(
        make_eval_error(ctx, "sorted: missing argument for iterable"),
      )
  }
  // starlark-go order: check iterable type first, then key, then reverse.
  // Kwarg names are quoted in error messages; positional names are not.
  guard_range_alloc(ctx, "sorted", iterable)
  let it = match @value.iterate(iterable) {
    Ok(it) => it
    Err(_) => {
      let pname = if iterable_from_kwarg { "\"iterable\"" } else { "iterable" }
      raise EvalErr(
        make_eval_error(
          ctx,
          "sorted: for parameter \{pname}: got \{iterable.type_name()}, want iterable",
        ),
      )
    }
  }
  let items : Array[@value.Value] = []
  while true {
    match it.next() {
      None => break
      Some(v) => items.push(v)
    }
  }
  let sorted_result = try {
    match key_fn {
      Some(f) =>
        match f {
          @value.Value::Function(_)
          | @value.Value::Builtin(_)
          | @value.Value::BoundMethod(_)
          | @value.Value::ExtVal(_) => ()
          v => {
            let pname = if key_from_kwarg { "\"key\"" } else { "key" }
            raise EvalErr(
              make_eval_error(
                ctx,
                "sorted: for parameter \{pname}: got \{v.type_name()}, want callable",
              ),
            )
          }
        }
      None => ()
    }
    let reverse = match reverse_arg {
      None => false
      Some(@value.Value::Bool(b)) => b
      Some(v) => {
        let pname = if reverse_from_kwarg { "\"reverse\"" } else { "reverse" }
        raise EvalErr(
          make_eval_error(
            ctx,
            "sorted: for parameter \{pname}: got \{v.type_name()}, want bool",
          ),
        )
      }
    }
    let keys : Array[@value.Value] = match key_fn {
      None => items
      Some(@value.Value::None) =>
        raise EvalErr(
          make_eval_error(
            ctx, "sorted: for parameter key: got NoneType, want callable",
          ),
        )
      Some(f) =>
        items.map(fn(v) raise EvalErr {
          call_value(ctx, f, [v], [], @errors.Position::new("", 0, 0))
        })
    }
    let indices : Array[Int] = Array::make(items.length(), 0)
    for i in 0.. {
          had_type_error = true
          0
        }
        Ok(c) => {
          let cc = if reverse { -c } else { c }
          // Break ties by original index so the sort is stable regardless of the
          // underlying sort_by's stability; reverse keeps equal-key order (not
          // reversed).
          if cc != 0 {
            cc
          } else {
            a - b
          }
        }
      }
    })
    if had_type_error {
      raise EvalErr(sorted_type_error(ctx, keys, reverse))
    }
    let result : Array[@value.Value] = indices.map(fn(i) { items[i] })
    @value.Value::List(@value.StarlarkList::new(result))
  } catch {
    e => {
      it.done()
      raise e
    }
  }
  it.done()
  sorted_result
}

///|
/// Reconstructs the exact comparison-failure error message for `sorted`.
/// Replays starlark-go's insertion sort (`sort.Stable`) over `keys` (with the
/// same `sort.Reverse` operand swap when `reverse` is true) to recover the
/// last failing `<` comparison and its operands, producing a message that
/// matches starlark-go's " <  not implemented" exactly.
fn sorted_type_error(
  ctx : EvalContext,
  keys : Array[@value.Value],
  reverse : Bool,
) -> @errors.EvalError {
  let n = keys.length()
  let idx : Array[Int] = Array::make(n, 0)
  for i in 0.. 0 {
      let (l, r) = if reverse {
        (keys[idx[j - 1]], keys[idx[j]])
      } else {
        (keys[idx[j]], keys[idx[j - 1]])
      }
      let less = match @value.compare_values(l, r) {
        Err(msg) => {
          err = Some(make_eval_error(ctx, msg))
          false
        }
        Ok(c) => c < 0
      }
      if less {
        let tmp = idx[j]
        idx[j] = idx[j - 1]
        idx[j - 1] = tmp
        j -= 1
      } else {
        break
      }
    }
  }
  match err {
    Some(e) => e
    None => make_eval_error(ctx, "sorted: comparison failed")
  }
}

///|
/// Implements `reversed(seq)`. Requires an indexable sequence via
/// `require_seq`; returns a new list with elements in reverse order.
fn builtin_reversed(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
) -> @value.Value raise EvalErr {
  guard_range_alloc(ctx, "reversed", pos_args[0])
  let it = require_seq(ctx, "reversed", pos_args[0])
  let items = it.collect()
  let n = items.length()
  let rev : Array[@value.Value] = Array::make(n, @value.Value::None)
  for i in 0.. @value.Value raise EvalErr {
  if pos_args.length() == 0 {
    return @value.Value::List(@value.StarlarkList::new([]))
  }
  let iterators : Array[@value.StarlarkIterator] = []
  for i, arg in pos_args {
    match @value.iterate(arg) {
      Err(_) => {
        for it in iterators {
          it.done()
        }
        raise EvalErr(
          make_eval_error(
            ctx,
            "zip: argument #\{i + 1} is not iterable: \{arg.type_name()}",
          ),
        )
      }
      Ok(it) => iterators.push(it)
    }
  }
  let result : Array[@value.Value] = []
  while true {
    let tuple_items : Array[@value.Value] = []
    let mut done = false
    for it in iterators {
      match it.next() {
        None => {
          done = true
          break
        }
        Some(v) => tuple_items.push(v)
      }
    }
    if done {
      for it in iterators {
        it.done()
      }
      break
    }
    result.push(@value.Value::Tuple(tuple_items))
  }
  @value.Value::List(@value.StarlarkList::new(result))
}

///|
/// Shared implementation of `any` and `all`. `any_mode=true` short-circuits
/// on the first truthy element; `any_mode=false` short-circuits on the first
/// falsy element.
fn builtin_any_all(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
  any_mode : Bool,
) -> @value.Value raise EvalErr {
  let it = require_seq(ctx, if any_mode { "any" } else { "all" }, pos_args[0])
  let mut result = !any_mode
  while true {
    match it.next() {
      None => {
        it.done()
        break
      }
      Some(v) => {
        let t = v.truth()
        if any_mode && t {
          it.done()
          result = true
          break
        } else if !any_mode && !t {
          it.done()
          result = false
          break
        }
      }
    }
  }
  @value.Value::Bool(result)
}

///|
/// Dispatches a method call on `recv` to the type-specific handler
/// (`call_list_method`, `call_dict_method`, `call_str_method`,
/// `call_set_method`, or `call_bytes_method`).
fn call_method(
  ctx : EvalContext,
  recv : @value.Value,
  method_name : String,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
  _pos : @errors.Position,
) -> @value.Value raise EvalErr {
  match recv {
    @value.Value::List(l) =>
      call_list_method(ctx, l, method_name, pos_args, kw_args)
    @value.Value::Dict(d) =>
      call_dict_method(ctx, d, method_name, pos_args, kw_args)
    @value.Value::String(s) =>
      call_str_method(ctx, s, method_name, pos_args, kw_args)
    @value.Value::Set(s) =>
      call_set_method(ctx, s, method_name, pos_args, kw_args)
    @value.Value::Bytes(b) => call_bytes_method(ctx, b, method_name, pos_args)
    _ =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "'\{recv.type_name()}' has no method '\{method_name}'",
        ),
      )
  }
}

///|
/// Implements all list methods (`append`, `extend`, `pop`, `remove`,
/// `insert`, `clear`, `index`). Mutation operations are guarded by the list's
/// own mutability check.
fn call_list_method(
  ctx : EvalContext,
  l : @value.StarlarkList,
  method_name : String,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.Value raise EvalErr {
  match method_name {
    "append" => {
      check_positional(ctx, "append", pos_args, kw_args, 1, 1)
      check_pfx(ctx, "append", l.push(pos_args[0]))
      @value.Value::None
    }
    "extend" => {
      check_positional(ctx, "extend", pos_args, kw_args, 1, 1)
      if pos_args[0] is @value.Value::List(src) {
        let snapshot = src.copy_items()
        check_pfx(ctx, "extend", l.check_mutable("extend"))
        for item in snapshot {
          l.push(item) |> ignore
        }
      } else {
        check_pfx(ctx, "extend", l.check_mutable("extend"))
        guard_range_alloc(ctx, "extend", pos_args[0])
        let it = require_seq(ctx, "extend", pos_args[0])
        while true {
          match it.next() {
            None => {
              it.done()
              break
            }
            Some(v) => l.push(v) |> ignore
          }
        }
      }
      @value.Value::None
    }
    "pop" => {
      check_positional(ctx, "pop", pos_args, kw_args, 0, 1)
      let n = l.length()
      let i = if pos_args.length() == 1 {
        match pos_args[0] {
          @value.Value::Int(b) => {
            check_int64_range(
              ctx,
              b,
              "pop: for parameter 1: \{b} out of range (want value in signed 64-bit range)",
            )
            b
          }
          v =>
            raise EvalErr(
              make_eval_error(
                ctx,
                "pop: for parameter 1: got \{v.type_name()}, want int",
              ),
            )
        }
      } else {
        BigInt::from_int(n - 1)
      }
      let idx = match adjust_index(i, n.to_int64(), "list") {
        Ok(j) => j
        Err(msg) => raise EvalErr(make_eval_error(ctx, "pop: " + msg))
      }
      check_pfx(ctx, "pop", l.pop_at(idx.to_int(), "pop from"))
    }
    "remove" => {
      check_positional(ctx, "remove", pos_args, kw_args, 1, 1)
      let item = pos_args[0]
      let mut found = -1
      for i in 0.. ignore
      @value.Value::None
    }
    "insert" => {
      check_positional(ctx, "insert", pos_args, kw_args, 2, 2)
      match pos_args[0] {
        @value.Value::Int(i) => {
          check_int64_range(
            ctx,
            i,
            "insert: for parameter 1: \{i} out of range (want value in signed 64-bit range)",
          )
          let len = l.length()
          let j = {
            let len64 = BigInt::from_int(len)
            let jj = if i < 0N { i + len64 } else { i }
            if jj < 0N {
              0
            } else if jj > len64 {
              len
            } else {
              jj.to_int()
            }
          }
          check_pfx(ctx, "insert", l.insert(j, pos_args[1])) |> ignore
          @value.Value::None
        }
        v =>
          raise EvalErr(
            make_eval_error(
              ctx,
              "insert: for parameter 1: got \{v.type_name()}, want int",
            ),
          )
      }
    }
    "clear" => {
      check_positional(ctx, "clear", pos_args, kw_args, 0, 0)
      check_pfx(ctx, "clear", l.clear()) |> ignore
      @value.Value::None
    }
    "index" => {
      check_positional(ctx, "index", pos_args, kw_args, 1, 3)
      let item = pos_args[0]
      let n = l.length()
      let start = if pos_args.length() >= 2 {
        match pos_args[1] {
          @value.Value::Int(i) => {
            check_int64_range(
              ctx,
              i,
              "index: invalid start index: \{i} out of range",
            )
            clamp_slice_index_i64(i.to_int64(), n)
          }
          @value.Value::None => 0
          v =>
            raise EvalErr(
              make_eval_error(
                ctx,
                "index: invalid start index: got \{v.type_name()}, want int",
              ),
            )
        }
      } else {
        0
      }
      let end = if pos_args.length() >= 3 {
        match pos_args[2] {
          @value.Value::Int(i) => {
            check_int64_range(
              ctx,
              i,
              "index: invalid end index: \{i} out of range",
            )
            clamp_slice_index_i64(i.to_int64(), n)
          }
          @value.Value::None => n
          v =>
            raise EvalErr(
              make_eval_error(
                ctx,
                "index: invalid end index: got \{v.type_name()}, want int",
              ),
            )
        }
      } else {
        n
      }
      for i in start..
      raise EvalErr(make_eval_error(ctx, "list has no method '\{method_name}'"))
  }
}

///|
/// Implements all dict methods (`get`, `keys`, `values`, `items`, `pop`,
/// `update`, `clear`, `popitem`, `setdefault`).
fn call_dict_method(
  ctx : EvalContext,
  d : @value.StarlarkDict,
  method_name : String,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.Value raise EvalErr {
  match method_name {
    "get" => {
      check_positional(ctx, "get", pos_args, kw_args, 1, 2)
      let default_val = if pos_args.length() >= 2 {
        pos_args[1]
      } else {
        @value.Value::None
      }
      match check_pfx(ctx, "get", d.get(pos_args[0])) {
        None => default_val
        Some(v) => v
      }
    }
    "keys" => {
      check_positional(ctx, "keys", pos_args, kw_args, 0, 0)
      let keys = d.keys()
      @value.Value::List(@value.StarlarkList::new(keys))
    }
    "values" => {
      check_positional(ctx, "values", pos_args, kw_args, 0, 0)
      let vals : Array[@value.Value] = []
      d.each(fn(_k, v) { vals.push(v) })
      @value.Value::List(@value.StarlarkList::new(vals))
    }
    "items" => {
      check_positional(ctx, "items", pos_args, kw_args, 0, 0)
      let items : Array[@value.Value] = []
      d.each(fn(k, v) { items.push(@value.Value::Tuple([k, v])) })
      @value.Value::List(@value.StarlarkList::new(items))
    }
    "pop" => {
      check_positional(ctx, "pop", pos_args, kw_args, 1, 2)
      let key = pos_args[0]
      match check_pfx(ctx, "pop", d.pop_entry(key)) {
        None =>
          if pos_args.length() >= 2 {
            pos_args[1]
          } else {
            raise EvalErr(make_eval_error(ctx, "pop: missing key"))
          }
        Some(v) => v
      }
    }
    "update" => {
      if pos_args.length() > 1 {
        raise EvalErr(
          make_eval_error(
            ctx,
            "update: got \{pos_args.length()} arguments, want at most 1",
          ),
        )
      }
      update_dict(ctx, "update", d, pos_args, kw_args)
      @value.Value::None
    }
    "clear" => {
      check_positional(ctx, "clear", pos_args, kw_args, 0, 0)
      check(ctx, d.clear())
      @value.Value::None
    }
    "popitem" => {
      check_positional(ctx, "popitem", pos_args, kw_args, 0, 0)
      match check(ctx, d.popitem()) {
        None => raise EvalErr(make_eval_error(ctx, "popitem: empty dict"))
        Some((k, v)) => @value.Value::Tuple([k, v])
      }
    }
    "setdefault" => {
      check_positional(ctx, "setdefault", pos_args, kw_args, 1, 2)
      let key = pos_args[0]
      let default_val = if pos_args.length() >= 2 {
        pos_args[1]
      } else {
        @value.Value::None
      }
      match check_pfx(ctx, "setdefault", d.get(key)) {
        Some(v) => v
        None => {
          check_pfx(ctx, "setdefault", d.set(key, default_val))
          default_val
        }
      }
    }
    _ =>
      raise EvalErr(make_eval_error(ctx, "dict has no method '\{method_name}'"))
  }
}

///|
/// Implements all string methods. Every method reads from the immutable `s`
/// and returns a new value; the string itself is never mutated.
fn call_str_method(
  ctx : EvalContext,
  s : @value.StarlarkString,
  method_name : String,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.Value raise EvalErr {
  match method_name {
    "upper" =>
      @value.Value::String(
        @value.StarlarkString::new(
          map_string_runes(s.raw(), @utf8util.to_upper_rune),
        ),
      )
    "lower" =>
      @value.Value::String(
        @value.StarlarkString::new(
          map_string_runes(s.raw(), @utf8util.to_lower_rune),
        ),
      )
    "strip" | "lstrip" | "rstrip" => {
      check_positional(ctx, method_name, pos_args, kw_args, 0, 1)
      let chars_opt = if pos_args.length() > 0 {
        let c = arg_as_string(ctx, method_name, pos_args, 0)
        if c.raw().length() > 0 {
          Some(c.raw())
        } else {
          None
        }
      } else {
        None
      }
      let from_left = method_name != "rstrip"
      let from_right = method_name != "lstrip"
      let result = match chars_opt {
        None => strip_whitespace(s.raw(), from_left, from_right)
        Some(chars) => strip_chars(s.raw(), chars, from_left, from_right)
      }
      @value.Value::String(@value.StarlarkString::new(result))
    }
    "startswith" => {
      check_positional(ctx, "startswith", pos_args, kw_args, 1, 3)
      let slen = s.byte_len()
      let i = parse_search_index(
        ctx, "startswith", pos_args, 1, slen, 0, "start",
      )
      let j = parse_search_index(
        ctx, "startswith", pos_args, 2, slen, slen, "end",
      )
      let sub = str_byte_slice(s, i, j)
      match_affix(ctx, "startswith", pos_args[0], fn(p) {
        bytes_has_prefix(sub, p)
      })
    }
    "endswith" => {
      check_positional(ctx, "endswith", pos_args, kw_args, 1, 3)
      let slen = s.byte_len()
      let i = parse_search_index(ctx, "endswith", pos_args, 1, slen, 0, "start")
      let j = parse_search_index(
        ctx, "endswith", pos_args, 2, slen, slen, "end",
      )
      let sub = str_byte_slice(s, i, j)
      match_affix(ctx, "endswith", pos_args[0], fn(p) {
        bytes_has_suffix(sub, p)
      })
    }
    "find" => {
      check_positional(ctx, "find", pos_args, kw_args, 1, 3)
      let sub = arg_as_string(ctx, "find", pos_args, 0)
      let slen = s.byte_len()
      let start = parse_search_index(ctx, "find", pos_args, 1, slen, 0, "start")
      let end = parse_search_index(ctx, "find", pos_args, 2, slen, slen, "end")
      let result = if start <= end {
        bytes_index_in(s, start, end, sub)
      } else {
        -1
      }
      @value.Value::Int(BigInt::from_int(result))
    }
    "count" => {
      check_positional(ctx, "count", pos_args, kw_args, 1, 3)
      let sub = arg_as_string(ctx, "count", pos_args, 0)
      let slen = s.byte_len()
      let start = parse_search_index(
        ctx, "count", pos_args, 1, slen, 0, "start",
      )
      let end = parse_search_index(ctx, "count", pos_args, 2, slen, slen, "end")
      let result = if start <= end {
        bytes_count_in(s, start, end, sub)
      } else {
        0
      }
      @value.Value::Int(BigInt::from_int(result))
    }
    "replace" => {
      check_positional(ctx, "replace", pos_args, kw_args, 2, 3)
      let old = arg_as_string(ctx, "replace", pos_args, 0)
      let new_s = arg_as_string(ctx, "replace", pos_args, 1)
      let max_count = if pos_args.length() >= 3 {
        int64_to_count(arg_as_int64(ctx, "replace", pos_args, 2))
      } else {
        -1
      }
      let result = if max_count < 0 {
        s.raw().replace_all(old=old.raw(), new=new_s.raw())
      } else {
        str_replace_count(s.raw(), old.raw(), new_s.raw(), max_count)
      }
      @value.Value::String(@value.StarlarkString::new(result))
    }
    "split" | "rsplit" => {
      check_positional(ctx, method_name, pos_args, kw_args, 0, 2)
      let sep_opt = if pos_args.length() == 0 {
        None
      } else {
        match pos_args[0] {
          @value.Value::None => None
          @value.Value::String(sep) => {
            if sep.raw().length() == 0 {
              raise EvalErr(make_eval_error(ctx, "split: empty separator"))
            }
            Some(sep.raw())
          }
          v =>
            raise EvalErr(
              make_eval_error(
                ctx,
                "split: got \{v.type_name()} for separator, want string",
              ),
            )
        }
      }
      let maxsplit = if pos_args.length() >= 2 {
        int64_to_count(arg_as_int64(ctx, method_name, pos_args, 1))
      } else {
        -1
      }
      if method_name == "split" {
        match sep_opt {
          None => to_value_list(split_whitespace(s.raw(), maxsplit))
          Some(sep_str) =>
            to_value_list(split_by_sep(s.raw(), sep_str, maxsplit))
        }
      } else {
        match sep_opt {
          None => to_value_list(rsplit_whitespace(s.raw(), maxsplit))
          Some(sep_str) =>
            to_value_list(rsplit_by_sep(s.raw(), sep_str, maxsplit))
        }
      }
    }
    "join" => {
      check_positional(ctx, "join", pos_args, kw_args, 1, 1)
      let parts : Array[Bytes] = []
      let it = match @value.iterate(pos_args[0]) {
        Ok(it) => it
        Err(_) =>
          raise EvalErr(
            make_eval_error(
              ctx,
              "join: for parameter 1: got \{pos_args[0].type_name()}, want iterable",
            ),
          )
      }
      while true {
        match it.next() {
          None => {
            it.done()
            break
          }
          Some(@value.Value::String(part)) => parts.push(part.to_bytes())
          Some(v) => {
            it.done()
            raise EvalErr(
              make_eval_error(
                ctx,
                "join: in list, want string, got \{v.type_name()}",
              ),
            )
          }
        }
      }
      let sep_bytes = s.to_bytes()
      let buf = @buffer.Buffer::Buffer()
      let mut first = true
      for part in parts {
        if !first {
          buf.write_bytes(sep_bytes[:])
        }
        first = false
        buf.write_bytes(part[:])
      }
      @value.Value::String(@value.StarlarkString::from_bytes(buf.contents()))
    }
    "format" => str_format(ctx, s.to_bytes(), pos_args, kw_args)
    "capitalize" => {
      let raw = s.raw()
      if raw.length() == 0 {
        @value.Value::String(s)
      } else {
        let buf = StringBuilder::new()
        let mut is_first = true
        for c in raw {
          let cp = c.to_int()
          if is_first {
            buf.write_char(@utf8util.to_title_rune(cp).unsafe_to_char())
            is_first = false
          } else {
            buf.write_char(@utf8util.to_lower_rune(cp).unsafe_to_char())
          }
        }
        @value.Value::String(@value.StarlarkString::new(buf.to_string()))
      }
    }
    "index" => {
      check_positional(ctx, "index", pos_args, [], 1, 3)
      let sub = arg_as_string(ctx, "index", pos_args, 0)
      let slen = s.byte_len()
      let start = parse_search_index(
        ctx, "index", pos_args, 1, slen, 0, "start",
      )
      let end = parse_search_index(ctx, "index", pos_args, 2, slen, slen, "end")
      let result = if start <= end {
        bytes_index_in(s, start, end, sub)
      } else {
        -1
      }
      if result < 0 {
        raise EvalErr(make_eval_error(ctx, "index: substring not found"))
      }
      @value.Value::Int(BigInt::from_int(result))
    }
    "rfind" => {
      check_positional(ctx, "rfind", pos_args, kw_args, 1, 3)
      let sub = arg_as_string(ctx, "rfind", pos_args, 0)
      let slen = s.byte_len()
      let start = parse_search_index(
        ctx, "rfind", pos_args, 1, slen, 0, "start",
      )
      let end = parse_search_index(ctx, "rfind", pos_args, 2, slen, slen, "end")
      let result = if start <= end {
        bytes_last_index_in(s, start, end, sub)
      } else {
        -1
      }
      @value.Value::Int(BigInt::from_int(result))
    }
    "rindex" => {
      check_positional(ctx, "rindex", pos_args, kw_args, 1, 3)
      let sub = arg_as_string(ctx, "rindex", pos_args, 0)
      let slen = s.byte_len()
      let start = parse_search_index(
        ctx, "rindex", pos_args, 1, slen, 0, "start",
      )
      let end = parse_search_index(
        ctx, "rindex", pos_args, 2, slen, slen, "end",
      )
      let result = if start <= end {
        bytes_last_index_in(s, start, end, sub)
      } else {
        -1
      }
      if result < 0 {
        raise EvalErr(make_eval_error(ctx, "rindex: substring not found"))
      }
      @value.Value::Int(BigInt::from_int(result))
    }
    "isalpha" => all_chars_satisfy(s, fn(c) { @utf8util.is_letter(c.to_int()) })
    "isdigit" =>
      all_chars_satisfy(s, fn(c) { @utf8util.is_decimal_digit(c.to_int()) })
    "isalnum" =>
      all_chars_satisfy(s, fn(c) {
        let n = c.to_int()
        @utf8util.is_letter(n) || @utf8util.is_decimal_digit(n)
      })
    "islower" => {
      let raw = s.raw()
      let has_cased = raw
        .iter()
        .any(fn(c) { @utf8util.is_cased_rune(c.to_int()) })
      @value.Value::Bool(
        has_cased && raw == map_string_runes(raw, @utf8util.to_lower_rune),
      )
    }
    "isupper" => {
      let raw = s.raw()
      let has_cased = raw
        .iter()
        .any(fn(c) { @utf8util.is_cased_rune(c.to_int()) })
      @value.Value::Bool(
        has_cased && raw == map_string_runes(raw, @utf8util.to_upper_rune),
      )
    }
    "isspace" => all_chars_satisfy(s, is_whitespace)
    "title" => {
      let buf = StringBuilder::new()
      let mut prev_cased = false
      for c in s.raw() {
        let cp = c.to_int()
        let mapped = if prev_cased {
          @utf8util.to_lower_rune(cp)
        } else {
          @utf8util.to_title_rune(cp)
        }
        buf.write_char(mapped.unsafe_to_char())
        prev_cased = @utf8util.is_cased_rune(mapped)
      }
      @value.Value::String(@value.StarlarkString::new(buf.to_string()))
    }
    "partition" =>
      partition_string(ctx, "partition", s, pos_args, kw_args, find_last=false)
    "rpartition" =>
      partition_string(ctx, "rpartition", s, pos_args, kw_args, find_last=true)
    "removeprefix" => {
      check_positional(ctx, "removeprefix", pos_args, kw_args, 1, 1)
      let prefix = arg_as_string(ctx, "removeprefix", pos_args, 0)
      if s.raw().has_prefix(prefix.raw()) {
        let rest = s.raw()[prefix.raw().length():].to_owned()
        @value.Value::String(@value.StarlarkString::new(rest))
      } else {
        @value.Value::String(s)
      }
    }
    "removesuffix" => {
      check_positional(ctx, "removesuffix", pos_args, kw_args, 1, 1)
      let suffix = arg_as_string(ctx, "removesuffix", pos_args, 0)
      if s.raw().has_suffix(suffix.raw()) {
        let rest = s.raw()[0:s.raw().length() - suffix.raw().length()].to_owned()
        @value.Value::String(@value.StarlarkString::new(rest))
      } else {
        @value.Value::String(s)
      }
    }
    "splitlines" => {
      let keepends = if pos_args.length() > 0 {
        match pos_args[0] {
          @value.Value::Bool(b) => b
          _ =>
            raise EvalErr(
              make_eval_error(
                ctx,
                "splitlines: for parameter 1: got \{pos_args[0].type_name()}, want bool",
              ),
            )
        }
      } else {
        false
      }
      let parts : Array[@value.Value] = []
      let chars = s.raw().to_array()
      let n = chars.length()
      let buf = StringBuilder::new()
      let mut i = 0
      while i < n {
        let c = chars[i]
        if c.to_int() == '\n'.to_int() {
          if keepends {
            buf.write_char(c)
          }
          parts.push(
            @value.Value::String(@value.StarlarkString::new(buf.to_string())),
          )
          buf.reset()
        } else {
          buf.write_char(c)
        }
        i += 1
      }
      if buf.to_string().length() > 0 {
        parts.push(
          @value.Value::String(@value.StarlarkString::new(buf.to_string())),
        )
      }
      @value.Value::List(@value.StarlarkList::new(parts))
    }
    "elems" =>
      @value.Value::StringElems(@value.StarlarkStringElems::new(s, false))
    "elem_ords" =>
      @value.Value::StringElems(@value.StarlarkStringElems::new(s, true))
    "istitle" => {
      let mut cased = false
      let mut prev_cased = false
      let mut valid = true
      for c in s.raw() {
        let n = c.to_int()
        if (n >= 'A'.to_int() && n <= 'Z'.to_int()) ||
          @utf8util.is_title_letter(n) {
          if prev_cased {
            valid = false
            break
          }
          prev_cased = true
          cased = true
        } else if @utf8util.is_lower_letter(n) {
          if !prev_cased {
            valid = false
            break
          }
          prev_cased = true
          cased = true
        } else if @utf8util.is_upper_letter(n) {
          valid = false
          break
        } else {
          prev_cased = false
        }
      }
      @value.Value::Bool(valid && cased)
    }
    "codepoints" =>
      @value.Value::StringCodepoints(
        @value.StarlarkStringCodepoints::new(s, false),
      )
    "codepoint_ords" =>
      @value.Value::StringCodepoints(
        @value.StarlarkStringCodepoints::new(s, true),
      )
    _ =>
      raise EvalErr(
        make_eval_error(ctx, "string has no method '\{method_name}'"),
      )
  }
}

///|
/// Iterates `arg` and adds each element to `result`. On a non-iterable or
/// hash error, raises with a `": argument # ..."` prefix.
fn set_iterate_into(
  ctx : EvalContext,
  arg : @value.Value,
  arg_idx : Int,
  method_nm : String,
  result : @value.StarlarkSet,
) -> Unit raise EvalErr {
  let it = match @value.iterate(arg) {
    Err(_) =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{method_nm}: argument #\{arg_idx} is not iterable: \{arg.type_name()}",
        ),
      )
    Ok(it) => it
  }
  while true {
    match it.next() {
      None => {
        it.done()
        break
      }
      Some(v) =>
        match result.add(v) {
          Err(e) => raise EvalErr(make_eval_error(ctx, "\{method_nm}: \{e}"))
          Ok(_) => ()
        }
    }
  }
}

///|
/// Validates and returns the single optional iterable argument shared by
/// `intersection`, `difference`, `symmetric_difference`, `issubset`, and
/// `issuperset`. Enforces at most one positional argument and requires it to be
/// iterable; a non-iterable raises the parameter-named unpack error.
fn set_other_iter(
  ctx : EvalContext,
  name : String,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.StarlarkIterator raise EvalErr {
  check_positional(ctx, name, pos_args, kw_args, 0, 1)
  if pos_args.length() != 1 {
    raise EvalErr(make_eval_error(ctx, "\{name}: missing argument"))
  }
  match @value.iterate(pos_args[0]) {
    Ok(it) => it
    Err(_) =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{name}: for parameter 1: got \{pos_args[0].type_name()}, want iterable",
        ),
      )
  }
}

///|
/// Implements all set methods (`add`, `discard`, `remove`, `clear`, `pop`,
/// `union`, `intersection`, `difference`, `symmetric_difference`, `issubset`,
/// `issuperset`, `update`).
fn call_set_method(
  ctx : EvalContext,
  s : @value.StarlarkSet,
  method_name : String,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
) -> @value.Value raise EvalErr {
  match method_name {
    "add" => {
      check_positional(ctx, "add", pos_args, kw_args, 1, 1)
      match s.add(pos_args[0]) {
        Err(e) => raise EvalErr(make_eval_error(ctx, "add: \{e}"))
        Ok(_) => ()
      }
      @value.Value::None
    }
    "discard" => {
      check_positional(ctx, "discard", pos_args, kw_args, 1, 1)
      match s.remove(pos_args[0]) {
        Err(e) => raise EvalErr(make_eval_error(ctx, "discard: \{e}"))
        Ok(_) => ()
      }
      @value.Value::None
    }
    "remove" => {
      check_positional(ctx, "remove", pos_args, kw_args, 1, 1)
      match s.remove(pos_args[0]) {
        Err(e) => raise EvalErr(make_eval_error(ctx, "remove: \{e}"))
        Ok(false) => raise EvalErr(make_eval_error(ctx, "remove: missing key"))
        Ok(true) => ()
      }
      @value.Value::None
    }
    "clear" => {
      check_positional(ctx, "clear", pos_args, kw_args, 0, 0)
      if s.length() == 0 {
        return @value.Value::None
      }
      match s.clear() {
        Err(e) => raise EvalErr(make_eval_error(ctx, "clear: \{e}"))
        Ok(_) => ()
      }
      @value.Value::None
    }
    "pop" => {
      check_positional(ctx, "pop", pos_args, kw_args, 0, 0)
      match s.pop_first() {
        Err(e) => raise EvalErr(make_eval_error(ctx, "pop: \{e}"))
        Ok(None) => raise EvalErr(make_eval_error(ctx, "pop: empty set"))
        Ok(Some(v)) => v
      }
    }
    "union" => {
      if kw_args.length() > 0 {
        raise EvalErr(
          make_eval_error(ctx, "union: does not accept keyword arguments"),
        )
      }
      let result = @value.StarlarkSet::new()
      s.each(fn(v) { ignore(result.add(v)) })
      for i in 0.. {
      let result = @value.StarlarkSet::new()
      let it = set_other_iter(ctx, "intersection", pos_args, kw_args)
      while true {
        match it.next() {
          None => {
            it.done()
            break
          }
          Some(v) =>
            if swallowed_contains(s.contains(v)) {
              ignore(result.add(v))
            }
        }
      }
      @value.Value::Set(result)
    }
    "difference" => {
      let other_set = @value.StarlarkSet::new()
      let it = set_other_iter(ctx, "difference", pos_args, kw_args)
      while true {
        match it.next() {
          None => {
            it.done()
            break
          }
          Some(v) => ignore(other_set.add(v))
        }
      }
      let result = @value.StarlarkSet::new()
      s.each(fn(v) {
        if confirmed_absent(other_set.contains(v)) {
          ignore(result.add(v))
        }
      })
      @value.Value::Set(result)
    }
    "symmetric_difference" => {
      let it = set_other_iter(ctx, "symmetric_difference", pos_args, kw_args)
      let other_items = it.collect()
      let other_set = @value.StarlarkSet::new()
      for v in other_items {
        ignore(other_set.add(v))
      }
      let result = @value.StarlarkSet::new()
      s.each(fn(v) {
        if confirmed_absent(other_set.contains(v)) {
          ignore(result.add(v))
        }
      })
      for v in other_items {
        if confirmed_absent(s.contains(v)) {
          ignore(result.add(v))
        }
      }
      @value.Value::Set(result)
    }
    "issubset" => {
      let other_set = @value.StarlarkSet::new()
      let it = set_other_iter(ctx, "issubset", pos_args, kw_args)
      while true {
        match it.next() {
          None => {
            it.done()
            break
          }
          Some(v) => ignore(other_set.add(v))
        }
      }
      let mut all_in = true
      s.each(fn(v) {
        if !swallowed_contains(other_set.contains(v)) {
          all_in = false
        }
      })
      @value.Value::Bool(all_in)
    }
    "issuperset" => {
      let mut all_in = true
      let it = set_other_iter(ctx, "issuperset", pos_args, kw_args)
      while true {
        match it.next() {
          None => {
            it.done()
            break
          }
          Some(v) =>
            if !swallowed_contains(s.contains(v)) {
              it.done()
              all_in = false
              break
            }
        }
      }
      @value.Value::Bool(all_in)
    }
    "update" => {
      if kw_args.length() > 0 {
        raise EvalErr(
          make_eval_error(ctx, "update: does not accept keyword arguments"),
        )
      }
      for i in 0..
            raise EvalErr(
              make_eval_error(
                ctx,
                "update: argument #\{i + 1} is not iterable: \{arg.type_name()}",
              ),
            )
          Ok(it) => it
        }
        while true {
          match it.next() {
            None => {
              it.done()
              break
            }
            Some(v) =>
              match s.add(v) {
                Err(e) => raise EvalErr(make_eval_error(ctx, "update: \{e}"))
                Ok(_) => ()
              }
          }
        }
      }
      @value.Value::None
    }
    _ =>
      raise EvalErr(make_eval_error(ctx, "set has no method '\{method_name}'"))
  }
}

///|
/// Implements `bytes()`. Accepts a `bytes` value (identity), a `string` (UTF-8
/// re-encoded with invalid sequences replaced by U+FFFD, matching
/// starlark-go), or an iterable of ints in `[0, 255]`.
fn builtin_bytes(
  ctx : EvalContext,
  pos_args : Array[@value.Value],
) -> @value.Value raise EvalErr {
  if pos_args.length() != 1 {
    raise EvalErr(
      make_eval_error(
        ctx,
        "bytes: got \{pos_args.length()} arguments, want exactly 1",
      ),
    )
  }
  match pos_args[0] {
    @value.Value::Bytes(b) => @value.Value::Bytes(b)
    @value.Value::String(s) => {
      // starlark-go replaces invalid encodings with U+FFFD when converting a
      // string to bytes (library.go utf8Transcode). When the bytes are already
      // valid UTF-8 (the common case), the transcode is a no-op, so the raw
      // bytes are returned unchanged to avoid an extra String/Bytes allocation.
      let raw = s.to_bytes()
      if @utf8util.is_valid_utf8(raw) {
        @value.Value::Bytes(raw)
      } else {
        @value.Value::Bytes(@utf8.encode(@utf8util.decode_utf8_lossy(raw)))
      }
    }
    other => {
      let it = match @value.iterate(other) {
        Err(_) =>
          raise EvalErr(
            make_eval_error(
              ctx,
              "bytes: got \{other.type_name()}, want string, bytes, or iterable of ints",
            ),
          )
        Ok(it) => it
      }
      let buf : Array[Byte] = []
      let mut idx = 0
      while true {
        match it.next() {
          None => {
            it.done()
            break
          }
          Some(@value.Value::Int(n)) => {
            if n < 0N || n > 255N {
              it.done()
              raise EvalErr(
                make_eval_error(
                  ctx,
                  "bytes: at index \{idx}, \{n} out of range (want value in unsigned 8-bit range)",
                ),
              )
            }
            buf.push(n.to_int().to_byte())
            idx += 1
          }
          Some(v) => {
            it.done()
            raise EvalErr(
              make_eval_error(
                ctx,
                "bytes: at index \{idx}, got \{v.type_name()}, want int",
              ),
            )
          }
        }
      }
      @value.Value::Bytes(Bytes::from_array(buf))
    }
  }
}

///|
/// Dispatches `bytes` method calls. Currently only `elems` is supported,
/// which returns a `BytesElems` iterator.
fn call_bytes_method(
  ctx : EvalContext,
  b : Bytes,
  method_name : String,
  _pos_args : Array[@value.Value],
) -> @value.Value raise EvalErr {
  match method_name {
    "elems" => @value.Value::BytesElems(@value.StarlarkBytesElems::new(b))
    _ =>
      raise EvalErr(
        make_eval_error(ctx, "bytes has no method '\{method_name}'"),
      )
  }
}

///|
/// Normalizes a possibly-negative `Int64` index against length `n` and clamps
/// it into the inclusive range `[0, n]`, the valid domain for `list.index`'s
/// half-open `start`/`end` parameters.
fn clamp_slice_index_i64(raw : Int64, n : Int) -> Int {
  let n64 = n.to_int64()
  let j = if raw < 0L { raw + n64 } else { raw }
  if j < 0L {
    0
  } else if j > n64 {
    n
  } else {
    j.to_int()
  }
}

///|
/// Shared body for `startswith`/`endswith`. `arg` is a single string or a
/// tuple of strings; returns `Bool(true)` when the `check` predicate holds
/// for the single string or any tuple element. `name` prefixes type-error
/// messages.
fn match_affix(
  ctx : EvalContext,
  name : String,
  arg : @value.Value,
  check : (@value.StarlarkString) -> Bool,
) -> @value.Value raise EvalErr {
  match arg {
    @value.Value::String(p) => @value.Value::Bool(check(p))
    @value.Value::Tuple(t) => {
      let mut found = false
      for idx, item in t {
        match item {
          @value.Value::String(p) =>
            if check(p) {
              found = true
              break
            }
          v =>
            raise EvalErr(
              make_eval_error(
                ctx,
                "\{name}: want string, got \{v.type_name()}, for element \{idx}",
              ),
            )
        }
      }
      @value.Value::Bool(found)
    }
    v =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{name}: got \{v.type_name()}, want string or tuple of string",
        ),
      )
  }
}

///|
/// Validates that `kw_args` is empty and that the positional-argument count is
/// in `[min, max]`. `name` prefixes every error message.
fn check_positional(
  ctx : EvalContext,
  name : String,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
  min : Int,
  max : Int,
) -> Unit raise EvalErr {
  if kw_args.length() > 0 {
    raise EvalErr(make_eval_error(ctx, "\{name}: unexpected keyword arguments"))
  }
  let n = pos_args.length()
  if n < min {
    let atleast = if min < max { "at least " } else { "" }
    raise EvalErr(
      make_eval_error(ctx, "\{name}: got \{n} arguments, want \{atleast}\{min}"),
    )
  }
  if n > max {
    let atmost = if max > min { "at most " } else { "" }
    raise EvalErr(
      make_eval_error(ctx, "\{name}: got \{n} arguments, want \{atmost}\{max}"),
    )
  }
}

///|
/// Raises when `kw_args` is non-empty, with the message
/// `" does not accept keyword arguments"`.
fn reject_kwargs(
  ctx : EvalContext,
  name : String,
  kw_args : Array[(String, @value.Value)],
) -> Unit raise EvalErr {
  if kw_args.length() > 0 {
    raise EvalErr(
      make_eval_error(ctx, "\{name} does not accept keyword arguments"),
    )
  }
}

///|
/// Coerces the `idx`-th (0-based) positional argument to a `StarlarkString`,
/// raising a type-error on mismatch.
fn arg_as_string(
  ctx : EvalContext,
  name : String,
  pos_args : Array[@value.Value],
  idx : Int,
) -> @value.StarlarkString raise EvalErr {
  match pos_args[idx] {
    @value.Value::String(s) => s
    v =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{name}: for parameter \{idx + 1}: got \{v.type_name()}, want string",
        ),
      )
  }
}

///|
/// Normalizes a method's `count` parameter: negative values become `-1`
/// (unlimited); non-negative values exceeding int32 are clamped to
/// `max_int32`.
fn int64_to_count(n : Int64) -> Int {
  if n < 0L {
    -1
  } else if n > max_int32.to_int64() {
    max_int32.to_int()
  } else {
    n.to_int()
  }
}

///|
/// Coerces the `idx`-th (0-based) positional argument to an `Int64`, raising a
/// type-error or an out-of-range error on mismatch.
fn arg_as_int64(
  ctx : EvalContext,
  name : String,
  pos_args : Array[@value.Value],
  idx : Int,
) -> Int64 raise EvalErr {
  match pos_args[idx] {
    @value.Value::Int(n) => {
      check_int64_range(
        ctx,
        n,
        "\{name}: for parameter \{idx + 1}: \{n} out of range (want value in signed 64-bit range)",
      )
      n.to_int64()
    }
    v =>
      raise EvalErr(
        make_eval_error(
          ctx,
          "\{name}: for parameter \{idx + 1}: got \{v.type_name()}, want int",
        ),
      )
  }
}

///|
/// Decodes an optional start/end index argument for string search methods.
/// Accepts an `Int` (clamped to `[0, slen]`) or `None` (returns
/// `default_val`); raises on any other type.
fn parse_search_index(
  ctx : EvalContext,
  mname : String,
  args : Array[@value.Value],
  pos : Int,
  slen : Int,
  default_val : Int,
  kind : String,
) -> Int raise EvalErr {
  if args.length() > pos {
    match args[pos] {
      @value.Value::Int(n) => {
        check_int64_range(
          ctx,
          n,
          "\{mname}: invalid \{kind} index: \{n} out of range",
        )
        let i64 = n.to_int64()
        let slen64 = slen.to_int64()
        if i64 >= slen64 {
          slen
        } else if i64 < -slen64 {
          0
        } else {
          clamp_byte_index(i64.to_int(), slen)
        }
      }
      @value.Value::None => default_val
      v =>
        raise EvalErr(
          make_eval_error(
            ctx,
            "\{mname}: invalid \{kind} index: got \{v.type_name()}, want int",
          ),
        )
    }
  } else {
    default_val
  }
}

///|
/// Resolves a possibly-negative byte index against `blen` (negatives count
/// from the end) and clamps the result to `[0, blen]`.
fn clamp_byte_index(idx : Int, blen : Int) -> Int {
  if idx < 0 {
    (blen + idx).max(0)
  } else {
    idx.min(blen)
  }
}

///|
/// Returns the bytes of `s` in the byte-offset range `[lo, hi)` as a new
/// `StarlarkString`. Returns an empty string when `lo >= hi`.
fn str_byte_slice(
  s : @value.StarlarkString,
  lo : Int,
  hi : Int,
) -> @value.StarlarkString {
  if lo >= hi {
    return @value.StarlarkString::new("")
  }
  let buf : Array[Byte] = []
  for i in lo.. Int {
  let nlen = needle.byte_len()
  if nlen == 0 {
    return lo
  }
  let limit = hi - nlen
  let mut i = lo
  while i <= limit {
    let mut k = 0
    while k < nlen && hay.byte_at(i + k) == needle.byte_at(k) {
      k = k + 1
    }
    if k == nlen {
      return i
    }
    i = i + 1
  }
  -1
}

///|
/// Returns the byte offset of the last occurrence of `needle` within
/// `hay[lo:hi]`, or `-1` if not found. An empty `needle` returns `hi`.
fn bytes_last_index_in(
  hay : @value.StarlarkString,
  lo : Int,
  hi : Int,
  needle : @value.StarlarkString,
) -> Int {
  let nlen = needle.byte_len()
  if nlen == 0 {
    return hi
  }
  let mut i = hi - nlen
  while i >= lo {
    let mut k = 0
    while k < nlen && hay.byte_at(i + k) == needle.byte_at(k) {
      k = k + 1
    }
    if k == nlen {
      return i
    }
    i = i - 1
  }
  -1
}

///|
/// Returns true when `s` begins with `prefix` (byte-level comparison).
fn bytes_has_prefix(
  s : @value.StarlarkString,
  prefix : @value.StarlarkString,
) -> Bool {
  let plen = prefix.byte_len()
  if plen > s.byte_len() {
    return false
  }
  for k in 0.. Bool {
  let slen = suffix.byte_len()
  let off = s.byte_len() - slen
  if off < 0 {
    return false
  }
  for k in 0.. Int {
  let nlen = needle.byte_len()
  if nlen == 0 {
    let bytes = hay.to_bytes()
    let mut runes = 0
    let mut i = lo
    while i < hi {
      let (_, width) = @utf8util.utf8_decode_rune(bytes, i)
      runes = runes + 1
      i = i + width
    }
    return runes + 1
  }
  let mut count = 0
  let mut i = lo
  while i <= hi - nlen {
    let mut k = 0
    while k < nlen && hay.byte_at(i + k) == needle.byte_at(k) {
      k = k + 1
    }
    if k == nlen {
      count = count + 1
      i = i + nlen
    } else {
      i = i + 1
    }
  }
  count
}

///|
/// Returns the byte offset of the first occurrence of `sub` in `s`, or `-1`.
/// An empty `sub` returns 0.
fn find_substr(s : String, sub : String) -> Int {
  if sub.length() == 0 {
    return 0
  }
  let slen = s.length()
  let sublen = sub.length()
  if sublen > slen {
    return -1
  }
  for i in 0..<=(slen - sublen) {
    let mut match_ = true
    for j in 0.. Bool,
) -> @value.Value {
  if s.raw().length() == 0 {
    return @value.Value::Bool(false)
  }
  for c in s.raw() {
    if !pred(c) {
      return @value.Value::Bool(false)
    }
  }
  @value.Value::Bool(true)
}

///|
/// Shared implementation of `str.partition` / `str.rpartition`. Splits `s` on
/// the first (`find_last=false`) or last (`find_last=true`) occurrence of the
/// separator. When the separator is absent the original string occupies the
/// first slot for `partition` and the last slot for `rpartition`.
fn partition_string(
  ctx : EvalContext,
  mname : String,
  s : @value.StarlarkString,
  pos_args : Array[@value.Value],
  kw_args : Array[(String, @value.Value)],
  find_last~ : Bool,
) -> @value.Value raise EvalErr {
  check_positional(ctx, mname, pos_args, kw_args, 1, 1)
  let sep = arg_as_string(ctx, mname, pos_args, 0)
  if sep.byte_len() == 0 {
    raise EvalErr(make_eval_error(ctx, mname + ": empty separator"))
  }
  let idx = if find_last {
    bytes_last_index_in(s, 0, s.byte_len(), sep)
  } else {
    bytes_index_in(s, 0, s.byte_len(), sep)
  }
  let empty = @value.Value::String(@value.StarlarkString::new(""))
  if idx < 0 {
    if find_last {
      @value.Value::Tuple([empty, empty, @value.Value::String(s)])
    } else {
      @value.Value::Tuple([@value.Value::String(s), empty, empty])
    }
  } else {
    let before = str_byte_slice(s, 0, idx)
    let after = str_byte_slice(s, idx + sep.byte_len(), s.byte_len())
    @value.Value::Tuple([
      @value.Value::String(before),
      @value.Value::String(sep),
      @value.Value::String(after),
    ])
  }
}

///|
/// Reports whether `c` is a Unicode whitespace character. Mirrors Go's
/// `unicode.IsSpace` (which Starlark uses for split/strip/`isspace`): ASCII
/// controls and space, the Latin-1 NEL and no-break space, and the Unicode
/// `White_Space` category.
fn is_whitespace(c : Char) -> Bool {
  // Mirror Go's unicode.IsSpace, which Starlark uses for whitespace-based
  // split/strip and isspace: ASCII controls + space, the Latin-1 NEL and
  // no-break space, and the Unicode White_Space code points.
  let n = c.to_int()
  match n {
    0x09 | 0x0A | 0x0B | 0x0C | 0x0D | 0x20 => true
    0x85 | 0xA0 => true
    0x1680 => true
    0x2028 | 0x2029 | 0x202F | 0x205F | 0x3000 => true
    _ => n >= 0x2000 && n <= 0x200A
  }
}

///|
/// Applies a per-rune mapping to every codepoint of a string and rebuilds it.
fn map_string_runes(raw : String, f : (Int) -> Int) -> String {
  let buf = StringBuilder::new()
  for c in raw {
    buf.write_char(f(c.to_int()).unsafe_to_char())
  }
  buf.to_string()
}

///|
/// Wraps an array of strings into a Starlark list of `String` values.
fn to_value_list(parts : Array[String]) -> @value.Value {
  let items : Array[@value.Value] = []
  for p in parts {
    items.push(@value.Value::String(@value.StarlarkString::new(p)))
  }
  @value.Value::List(@value.StarlarkList::new(items))
}

///|
/// Splits `s` on runs of Unicode whitespace, producing at most `maxsplit+1`
/// parts when `maxsplit >= 0`. Leading and trailing whitespace is ignored and
/// consecutive whitespace is collapsed, matching Python's `str.split()`.
fn split_whitespace(s : String, maxsplit : Int) -> Array[String] {
  let chars = s.to_array()
  let n = chars.length()
  let result : Array[String] = []
  let mut start = -1
  for i in 0..= 0 {
        if maxsplit >= 0 && result.length() == maxsplit {
          break
        }
        let buf = StringBuilder::new()
        for k in start..= 0 {
    let buf = StringBuilder::new()
    for k in start.. Array[String] {
  let chars = s.to_array()
  let n = chars.length()
  let result : Array[String] = []
  let mut end_pos = -1
  let mut i = n - 1
  while i >= 0 {
    let c = chars[i]
    if is_whitespace(c) {
      if end_pos >= 0 {
        if maxsplit >= 0 && result.length() == maxsplit {
          break
        }
        let buf = StringBuilder::new()
        for k in (i + 1)..= 0 {
    let buf = StringBuilder::new()
    for k in 0.. Int {
  if sub.length() == 0 {
    return from
  }
  let slen = s.length()
  let sublen = sub.length()
  if from + sublen > slen {
    return -1
  }
  for i in from..<=(slen - sublen) {
    let mut match_ = true
    for j in 0..= 0`. Unlike whitespace splitting, empty parts are
/// preserved.
fn split_by_sep(s : String, sep : String, maxsplit : Int) -> Array[String] {
  let seplen = sep.length()
  let slen = s.length()
  let parts : Array[String] = []
  let mut start = 0
  let mut splits = 0
  while start <= slen {
    if maxsplit >= 0 && splits >= maxsplit {
      parts.push(s[start:].to_owned())
      break
    }
    let idx = find_substr_from(s, sep, start)
    if idx < 0 {
      parts.push(s[start:].to_owned())
      break
    }
    parts.push(s[start:idx].to_owned())
    start = idx + seplen
    splits += 1
  }
  parts
}

///|
/// Right-to-left variant of `split_by_sep`, matching starlark-go's
/// algorithm: compute the full non-overlapping left-to-right split, then
/// merge the leftmost excess pieces back together with `sep` so only
/// `maxsplit` splits remain. This must not be computed independently by
/// scanning from the end, since a self-overlapping `sep` (e.g. `"XX"`
/// inside `"XXX"`) picks different split boundaries in each direction.
/// Delegates to `split_by_sep` when `maxsplit < 0` (unlimited).
fn rsplit_by_sep(s : String, sep : String, maxsplit : Int) -> Array[String] {
  if maxsplit < 0 {
    return split_by_sep(s, sep, -1)
  }
  let parts = split_by_sep(s, sep, -1)
  let excess = parts.length() - maxsplit
  if excess <= 0 {
    return parts
  }
  let merged = StringBuilder::new()
  for i in 0.. 0 {
      merged.write_string(sep)
    }
    merged.write_string(parts[i])
  }
  let result : Array[String] = [merged.to_string()]
  for i in excess.. String {
  let chars = s.to_array()
  let n = chars.length()
  let mut start = 0
  let mut end = n
  if from_left {
    while start < end && is_whitespace(chars[start]) {
      start += 1
    }
  }
  if from_right {
    while end > start && is_whitespace(chars[end - 1]) {
      end -= 1
    }
  }
  let buf = StringBuilder::new()
  for i in start.. String {
  let char_set = chars.to_array()
  let s_chars = s.to_array()
  let n = s_chars.length()
  let mut start = 0
  let mut end = n
  if from_left {
    while start < end {
      let c = s_chars[start]
      if char_set.contains(c) {
        start += 1
      } else {
        break
      }
    }
  }
  if from_right {
    while end > start {
      let c = s_chars[end - 1]
      if char_set.contains(c) {
        end -= 1
      } else {
        break
      }
    }
  }
  let buf = StringBuilder::new()
  for i in start.. String {
  if old.length() == 0 {
    let buf = StringBuilder::new()
    let chars = s.to_array()
    let mut replacements = 0
    for i in 0.. @value.Value raise EvalErr {
  let buf = @buffer.Buffer::Buffer()
  let n = template_bytes.length()
  let mut i = 0
  let mut auto_idx = 0
  let mut auto_mode = false
  let mut manual_mode = false
  while i < n {
    let b = template_bytes[i]
    if b == b'{' {
      if i + 1 < n && template_bytes[i + 1] == b'{' {
        buf.write_byte(b'{')
        i += 2
      } else {
        let start = i + 1
        let mut j = start
        let mut in_name = true
        while j < n && template_bytes[j] != b'}' {
          if template_bytes[j] == b'!' || template_bytes[j] == b':' {
            in_name = false
          } else if template_bytes[j] == b'{' && in_name {
            raise EvalErr(
              make_eval_error(
                ctx, "format: nested replacement fields not supported",
              ),
            )
          }
          j += 1
        }
        if j >= n {
          raise EvalErr(make_eval_error(ctx, "format: unmatched '{' in format"))
        }
        let field = @utf8.decode_lossy(template_bytes[start:j])
        i = j + 1
        let (name, conv) = if find_substr(field, "!") >= 0 {
          let bang = find_substr(field, "!")
          let raw_name = field[0:bang].to_owned()
          let after_bang = field[bang + 1:].to_owned()
          let colon = find_substr(after_bang, ":")
          let (conv_part, spec_part) = if colon >= 0 {
            (after_bang[0:colon].to_owned(), after_bang[colon + 1:].to_owned())
          } else {
            (after_bang, "")
          }
          if spec_part != "" {
            raise EvalErr(
              make_eval_error(
                ctx,
                "format spec features not supported in replacement fields: \{spec_part}",
              ),
            )
          }
          (raw_name, conv_part)
        } else {
          let colon = find_substr(field, ":")
          if colon >= 0 {
            let spec_part = field[colon + 1:].to_owned()
            if spec_part != "" {
              raise EvalErr(
                make_eval_error(
                  ctx,
                  "format spec features not supported in replacement fields: \{spec_part}",
                ),
              )
            }
            (field[0:colon].to_owned(), "s")
          } else {
            (field, "s")
          }
        }
        if find_substr(name, ".") >= 0 {
          raise EvalErr(
            make_eval_error(
              ctx,
              "format: attribute syntax x.y is not supported in replacement fields: \{name}",
            ),
          )
        }
        if find_substr(name, "[") >= 0 {
          raise EvalErr(
            make_eval_error(
              ctx,
              "format: element syntax a[i] is not supported in replacement fields: \{name}",
            ),
          )
        }
        let arg = if name == "" {
          if manual_mode {
            raise EvalErr(
              make_eval_error(
                ctx, "format: cannot switch from manual field specification to automatic field numbering",
              ),
            )
          }
          auto_mode = true
          if auto_idx >= args.length() {
            raise EvalErr(
              make_eval_error(ctx, "format: tuple index out of range"),
            )
          }
          let v = args[auto_idx]
          auto_idx += 1
          v
        } else {
          let mut is_digit = name.length() > 0
          for k in 0.. '9' {
              is_digit = false
              break
            }
          }
          // A field name made entirely of digits is a positional index only
          // when it fits in a machine int; an overflowing all-digit name
          // falls through to the keyword path (matches Go's decimal()).
          let index : Int? = if is_digit {
            match parse_int_str(name, 10) {
              Ok(n) =>
                if n.compare_int64(@int64.MAX_VALUE) <= 0 &&
                  n.compare_int64(@int64.MIN_VALUE) >= 0 {
                  Some(n.to_int64().to_int())
                } else {
                  None
                }
              Err(_) => None
            }
          } else {
            None
          }
          match index {
            Some(n) => {
              if auto_mode {
                raise EvalErr(
                  make_eval_error(
                    ctx, "format: cannot switch from automatic field numbering to manual field specification",
                  ),
                )
              }
              manual_mode = true
              if n >= args.length() {
                raise EvalErr(
                  make_eval_error(ctx, "format: tuple index out of range"),
                )
              }
              args[n]
            }
            None => {
              let mut found : @value.Value? = None
              for kv in kw_args {
                let (k, v) = kv
                if k == name {
                  found = Some(v)
                  break
                }
              }
              match found {
                None =>
                  raise EvalErr(
                    make_eval_error(ctx, "format: keyword \{name} not found"),
                  )
                Some(v) => v
              }
            }
          }
        }
        match conv {
          "s" =>
            match arg {
              @value.Value::String(sv) => buf.write_bytes(sv.to_bytes()[:])
              _ => buf.write_string_utf8(check(ctx, arg.to_str_checked()))
            }
          "r" => buf.write_string_utf8(check(ctx, arg.repr_checked()))
          other =>
            raise EvalErr(
              make_eval_error(ctx, "format: unknown conversion \"\{other}\""),
            )
        }
      }
    } else if b == b'}' {
      if i + 1 < n && template_bytes[i + 1] == b'}' {
        buf.write_byte(b'}')
        i += 2
      } else {
        raise EvalErr(make_eval_error(ctx, "format: single '}' in format"))
      }
    } else {
      buf.write_byte(template_bytes[i])
      i += 1
    }
  }
  @value.Value::String(@value.StarlarkString::from_bytes(buf.contents()))
}

///|
/// Parses a signed integer string in the given `base` (2–36, or 0 for
/// auto-detection). Base 0 detects `0x`/`0o`/`0b` prefixes; rejects bare
/// leading zeros in base-0 mode. Returns an error string on failure.
fn parse_int_str(s : String, base : Int) -> Result[BigInt, String] {
  if s.length() == 0 {
    return Err("invalid literal with base \{base}: \{s}")
  }
  let negative = s[0] == '-'
  let start = if negative || s[0] == '+' { 1 } else { 0 }
  if start >= s.length() {
    return Err("invalid literal with base \{base}: \{s}")
  }
  let actual_base = if base == 0 {
    if s[start] == '0' && start + 1 < s.length() {
      match s[start + 1] {
        'x' | 'X' => 16
        'o' | 'O' => 8
        'b' | 'B' => 2
        _ => {
          if s[start + 1] >= '1' && s[start + 1] <= '9' {
            return Err("invalid literal with base 0: \{s}")
          }
          10
        }
      }
    } else {
      10
    }
  } else {
    base
  }
  let digit_start = if s[start] == '0' && start + 1 < s.length() {
    let prefix = s[start + 1]
    if (prefix == 'x' || prefix == 'X') && actual_base == 16 {
      start + 2
    } else if (prefix == 'o' || prefix == 'O') && actual_base == 8 {
      start + 2
    } else if (prefix == 'b' || prefix == 'B') && actual_base == 2 {
      start + 2
    } else {
      start
    }
  } else {
    start
  }
  if digit_start >= s.length() {
    return Err("invalid literal with base \{base}: \{s}")
  }
  let digits = s[digit_start:].to_owned()
  for i in 0..= '0'.to_int() && c <= '9'.to_int() {
      c - '0'.to_int()
    } else if c >= 'a'.to_int() && c <= 'z'.to_int() {
      c - 'a'.to_int() + 10
    } else if c >= 'A'.to_int() && c <= 'Z'.to_int() {
      c - 'A'.to_int() + 10
    } else {
      return Err("invalid literal with base \{base}: \{s}")
    }
    if digit >= actual_base {
      return Err("invalid literal with base \{base}: \{s}")
    }
  }
  let result = BigInt::from_string(digits, radix=actual_base)
  Ok(if negative { -result } else { result })
}

///|
/// Returns true when `c` is an ASCII hexadecimal digit (0–9, a–f, A–F).
fn is_hex_digit(c : UInt16) -> Bool {
  (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}

///|
/// Returns the numeric value of an ASCII hex digit (0–15). Precondition:
/// `is_hex_digit(c)`.
fn hex_digit_val(c : UInt16) -> Int {
  let ci = c.to_int()
  if c >= '0' && c <= '9' {
    ci - '0'.to_int()
  } else if c >= 'a' && c <= 'f' {
    ci - 'a'.to_int() + 10
  } else {
    ci - 'A'.to_int() + 10
  }
}

///|
/// Parses a C99 hexadecimal float literal (`0x.p`). Returns
/// `None` when `s` is not a valid hex float, allowing the caller to fall back
/// to decimal parsing.
fn parse_hex_float_str(s : String) -> Double? {
  let n = s.length()
  let mut i = 0
  let negative = i < n && s[i] == '-'
  if i < n && (s[i] == '+' || s[i] == '-') {
    i += 1
  }
  if i + 1 >= n || s[i] != '0' || (s[i + 1] != 'x' && s[i + 1] != 'X') {
    return None
  }
  i += 2
  let int_start = i
  while i < n && is_hex_digit(s[i]) {
    i += 1
  }
  let int_end = i
  let mut frac_start = i
  let mut frac_end = i
  if i < n && s[i] == '.' {
    i += 1
    frac_start = i
    while i < n && is_hex_digit(s[i]) {
      i += 1
    }
    frac_end = i
  }
  if int_end == int_start && frac_end == frac_start {
    return None
  }
  if i >= n || (s[i] != 'p' && s[i] != 'P') {
    return None
  }
  i += 1
  let exp_neg = i < n && s[i] == '-'
  if i < n && (s[i] == '+' || s[i] == '-') {
    i += 1
  }
  let exp_start = i
  let mut exp_val = 0
  while i < n && s[i] >= '0' && s[i] <= '9' {
    exp_val = exp_val * 10 + s[i].to_int() - '0'.to_int()
    if exp_val > 2100 {
      exp_val = 2100
    }
    i += 1
  }
  if i == exp_start || i != n {
    return None
  }
  let mut mantissa = 0.0
  for j in int_start.. Bool {
  let n = s.length()
  let mut i = 0
  if i < n && (s[i] == '+' || s[i] == '-') {
    i += 1
  }
  let int_start = i
  while i < n && s[i] >= '0' && s[i] <= '9' {
    i += 1
  }
  let has_int_digits = i > int_start
  let mut has_frac_digits = false
  if i < n && s[i] == '.' {
    i += 1
    let frac_start = i
    while i < n && s[i] >= '0' && s[i] <= '9' {
      i += 1
    }
    has_frac_digits = i > frac_start
  }
  if !has_int_digits && !has_frac_digits {
    return false
  }
  if i < n && (s[i] == 'e' || s[i] == 'E') {
    i += 1
    if i < n && (s[i] == '+' || s[i] == '-') {
      i += 1
    }
    let exp_start = i
    while i < n && s[i] >= '0' && s[i] <= '9' {
      i += 1
    }
    if i == exp_start {
      return false
    }
  }
  i == n
}