///|
/// 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
}