///|
/// Translates a Starlark index `i` for a sequence of `length` elements into a
/// non-negative offset. Negative indices count from the end. Returns `Err` when
/// `i` falls outside Go's int32 range (matching starlark-go's `AsInt32` check)
/// or the adjusted offset is out of bounds.
fn adjust_index(
i : BigInt,
length : Int64,
type_name : String,
) -> Result[Int64, String] {
// Go's AsInt32: fail if value doesn't fit int32 range.
// Produces " index: N out of range" (no range bounds).
if i > max_int32 || i < min_int32 {
return Err("\{type_name} index: \{i} out of range")
}
let len = BigInt::from_int64(length)
let j = if i < 0N { i + len } else { i }
if j < 0N || j >= len {
if length == 0L {
Err("index \{i} out of range: empty \{type_name}")
} else {
Err("\{type_name} index \{i} out of range [\{-length}:\{length - 1L}]")
}
} else {
Ok(j.to_int64())
}
}
///|
/// Returns the source spelling of a unary operator, used in
/// "unknown unary op" error messages.
fn unary_op_to_str(op : @syntax.UnaryOp) -> String {
match op {
@syntax.UnaryOp::OpPlus => "+"
@syntax.UnaryOp::OpMinus => "-"
@syntax.UnaryOp::OpBitNot => "~"
@syntax.UnaryOp::OpNot => "not"
}
}
///|
/// Returns the source spelling of a binary arithmetic/bitwise operator, used
/// in "unknown binary op" error messages.
fn op_to_str(op : @syntax.BinaryOp) -> String {
match op {
@syntax.BinaryOp::OpAdd => "+"
@syntax.BinaryOp::OpSub => "-"
@syntax.BinaryOp::OpMul => "*"
@syntax.BinaryOp::OpDiv => "/"
@syntax.BinaryOp::OpFloorDiv => "//"
@syntax.BinaryOp::OpMod => "%"
@syntax.BinaryOp::OpBitAnd => "&"
@syntax.BinaryOp::OpBitOr => "|"
@syntax.BinaryOp::OpBitXor => "^"
@syntax.BinaryOp::OpLShift => "<<"
@syntax.BinaryOp::OpRShift => ">>"
_ => "?"
}
}
///|
/// Applies a unary operator to `v`. Custom values are given first refusal via
/// their `get_unary` hook; all built-in types are handled inline.
fn eval_unary(
ctx : EvalContext,
op : @syntax.UnaryOp,
v : @value.Value,
_pos : @errors.Position,
) -> @value.Value raise EvalErr {
if v is @value.Value::ExtVal(c) {
match c.get_unary(unary_op_to_str(op)) {
Some(Ok(r)) => return r
Some(Err(e)) => raise EvalErr(make_eval_error(ctx, e))
None => ()
}
}
match op {
@syntax.UnaryOp::OpPlus =>
match v {
@value.Value::Int(_) | @value.Value::Float(_) => v
_ =>
raise EvalErr(
make_eval_error(ctx, "unknown unary op: + \{v.type_name()}"),
)
}
@syntax.UnaryOp::OpMinus =>
match v {
@value.Value::Int(n) => @value.Value::Int(-n)
@value.Value::Float(f) => @value.Value::Float(-f)
_ =>
raise EvalErr(
make_eval_error(ctx, "unknown unary op: - \{v.type_name()}"),
)
}
@syntax.UnaryOp::OpBitNot =>
match v {
@value.Value::Int(n) => @value.Value::Int(-n - 1N)
_ =>
raise EvalErr(
make_eval_error(ctx, "unknown unary op: ~ \{v.type_name()}"),
)
}
@syntax.UnaryOp::OpNot => @value.Value::Bool(!v.truth())
}
}
///|
/// Applies a binary operator to `lv` and `rv`. Left-hand `ExtVal`s are given
/// first refusal, then right-hand; both non-`ExtVal` pairs dispatch to the
/// operator-specific sub-functions below. `and`/`or` short-circuit before
/// reaching this function and must not be passed here.
fn eval_binary(
ctx : EvalContext,
lv : @value.Value,
op : @syntax.BinaryOp,
rv : @value.Value,
pos : @errors.Position,
) -> @value.Value raise EvalErr {
if lv is @value.Value::ExtVal(lc) {
match lc.get_binary(op_to_str(op), rv, true) {
Some(Ok(v)) => return v
Some(Err(e)) => raise EvalErr(make_eval_error(ctx, e))
None => ()
}
}
if rv is @value.Value::ExtVal(rc) {
match rc.get_binary(op_to_str(op), lv, false) {
Some(Ok(v)) => return v
Some(Err(e)) => raise EvalErr(make_eval_error(ctx, e))
None => ()
}
}
match op {
@syntax.BinaryOp::OpAdd => eval_add(ctx, lv, rv, pos)
@syntax.BinaryOp::OpSub => eval_arith_sub(ctx, lv, rv, pos)
@syntax.BinaryOp::OpMul => eval_mul(ctx, lv, rv, pos)
@syntax.BinaryOp::OpDiv => eval_div(ctx, lv, rv, pos)
@syntax.BinaryOp::OpFloorDiv => eval_floor_div(ctx, lv, rv, pos)
@syntax.BinaryOp::OpMod => eval_mod(ctx, lv, rv, pos)
@syntax.BinaryOp::OpBitAnd => eval_bitwise(ctx, lv, rv, pos, "land")
@syntax.BinaryOp::OpBitOr => eval_bitwise(ctx, lv, rv, pos, "lor")
@syntax.BinaryOp::OpBitXor => eval_bitwise(ctx, lv, rv, pos, "lxor")
@syntax.BinaryOp::OpLShift => eval_shift(ctx, lv, rv, pos, "lshift")
@syntax.BinaryOp::OpRShift => eval_shift(ctx, lv, rv, pos, "rshift")
@syntax.BinaryOp::OpEq =>
match @value.starlark_equals_depth(lv, rv, @value.compare_limit) {
Ok(b) => @value.Value::Bool(b)
Err(e) => raise EvalErr(make_eval_error(ctx, e))
}
@syntax.BinaryOp::OpNe =>
match @value.starlark_equals_depth(lv, rv, @value.compare_limit) {
Ok(b) => @value.Value::Bool(!b)
Err(e) => raise EvalErr(make_eval_error(ctx, e))
}
@syntax.BinaryOp::OpLt => eval_cmp(ctx, lv, rv, pos, -1, true)
@syntax.BinaryOp::OpLe => eval_cmp(ctx, lv, rv, pos, -1, false)
@syntax.BinaryOp::OpGt => eval_cmp(ctx, lv, rv, pos, 1, true)
@syntax.BinaryOp::OpGe => eval_cmp(ctx, lv, rv, pos, 1, false)
@syntax.BinaryOp::OpIn => in_op(ctx, lv, rv, pos)
@syntax.BinaryOp::OpNotIn => {
let r = in_op(ctx, lv, rv, pos)
match r {
@value.Value::Bool(b) => @value.Value::Bool(!b)
_ => @value.Value::Bool(false)
}
}
@syntax.BinaryOp::OpAnd | @syntax.BinaryOp::OpOr =>
raise EvalErr(
make_eval_error(ctx, "and/or must be handled before eval_binary"),
)
}
}
///|
/// Converts `n` to a finite `Double`, raising when the magnitude overflows
/// IEEE-754 double range.
fn finite_double(ctx : EvalContext, n : BigInt) -> Double raise EvalErr {
match @numeric.bigint_to_finite_double(n) {
Ok(f) => f
Err(msg) => raise EvalErr(make_eval_error(ctx, msg))
}
}
///|
/// Implements `+`: numeric addition for int/float pairs; concatenation for
/// string, bytes, list, and tuple.
fn eval_add(
ctx : EvalContext,
lv : @value.Value,
rv : @value.Value,
_pos : @errors.Position,
) -> @value.Value raise EvalErr {
match (lv, rv) {
(@value.Value::Int(a), @value.Value::Int(b)) => @value.Value::Int(a + b)
(@value.Value::Float(a), @value.Value::Float(b)) =>
@value.Value::Float(a + b)
(@value.Value::Int(a), @value.Value::Float(b)) =>
@value.Value::Float(finite_double(ctx, a) + b)
(@value.Value::Float(a), @value.Value::Int(b)) =>
@value.Value::Float(a + finite_double(ctx, b))
(@value.Value::String(a), @value.Value::String(b)) => {
let new_bytes : Array[Byte] = []
let ab = a.to_bytes()
for i in 0.. {
let items : Array[@value.Value] = []
for v in a.iter() {
items.push(v)
}
for v in b.iter() {
items.push(v)
}
@value.Value::List(@value.StarlarkList::new(items))
}
(@value.Value::Tuple(a), @value.Value::Tuple(b)) => {
let items : Array[@value.Value] = []
for v in a {
items.push(v)
}
for v in b {
items.push(v)
}
@value.Value::Tuple(items)
}
(@value.Value::Bytes(a), @value.Value::Bytes(b)) => {
let items : Array[Byte] = []
for i in 0..
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} + \{rv.type_name()}",
),
)
}
}
///|
/// Implements `-`: numeric subtraction for int/float pairs; set difference for
/// set × set (elements in `lv` not in `rv`).
fn eval_arith_sub(
ctx : EvalContext,
lv : @value.Value,
rv : @value.Value,
_pos : @errors.Position,
) -> @value.Value raise EvalErr {
match (lv, rv) {
(@value.Value::Int(a), @value.Value::Int(b)) => @value.Value::Int(a - b)
(@value.Value::Float(a), @value.Value::Float(b)) =>
@value.Value::Float(a - b)
(@value.Value::Int(a), @value.Value::Float(b)) =>
@value.Value::Float(finite_double(ctx, a) - b)
(@value.Value::Float(a), @value.Value::Int(b)) =>
@value.Value::Float(a - finite_double(ctx, b))
(@value.Value::Set(ls), @value.Value::Set(rs)) => {
let result = @value.StarlarkSet::new()
ls.each(fn(v) {
if confirmed_absent(rs.contains(v)) {
ignore(result.add(v))
}
})
@value.Value::Set(result)
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} - \{rv.type_name()}",
),
)
}
}
///|
/// Implements `*`: numeric multiplication for int/float pairs; sequence
/// repetition (`seq * n` or `n * seq`) for string, bytes, list, and tuple.
fn eval_mul(
ctx : EvalContext,
lv : @value.Value,
rv : @value.Value,
_pos : @errors.Position,
) -> @value.Value raise EvalErr {
match (lv, rv) {
(@value.Value::Int(a), @value.Value::Int(b)) => @value.Value::Int(a * b)
(@value.Value::Float(a), @value.Value::Float(b)) =>
@value.Value::Float(a * b)
(@value.Value::Int(a), @value.Value::Float(b)) =>
@value.Value::Float(finite_double(ctx, a) * b)
(@value.Value::Float(a), @value.Value::Int(b)) =>
@value.Value::Float(a * finite_double(ctx, b))
(@value.Value::String(s), @value.Value::Int(n)) =>
@value.Value::String(check(ctx, repeat_string(s, n)))
(@value.Value::Int(n), @value.Value::String(s)) =>
@value.Value::String(check(ctx, repeat_string(s, n)))
(@value.Value::List(l), @value.Value::Int(n)) =>
@value.Value::List(check(ctx, repeat_list(l, n)))
(@value.Value::Int(n), @value.Value::List(l)) =>
@value.Value::List(check(ctx, repeat_list(l, n)))
(@value.Value::Tuple(t), @value.Value::Int(n)) =>
@value.Value::Tuple(check(ctx, repeat_tuple(t, n)))
(@value.Value::Int(n), @value.Value::Tuple(t)) =>
@value.Value::Tuple(check(ctx, repeat_tuple(t, n)))
(@value.Value::Bytes(b), @value.Value::Int(n)) =>
@value.Value::Bytes(check(ctx, repeat_bytes(b, n)))
(@value.Value::Int(n), @value.Value::Bytes(b)) =>
@value.Value::Bytes(check(ctx, repeat_bytes(b, n)))
_ =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} * \{rv.type_name()}",
),
)
}
}
///|
/// Upper bound of Go's `AsInt32` range; slice/index values exceeding this are
/// out of range.
let max_int32 : BigInt = 2147483647N
///|
/// Lower bound of Go's `AsInt32` range; slice/index values below this are out
/// of range.
let min_int32 : BigInt = -2147483648N
///|
/// Maximum valid byte value (255); used to range-check `in bytes` with an int
/// needle.
let max_byte_val : BigInt = 255N
///|
/// Maximum left-shift count. Beyond 512 bits the BigInt would grow without
/// bound; shifts at or above this limit raise an error.
let max_lshift_bits : BigInt = 512N
///|
/// Maximum valid Unicode code point (U+10FFFF), used for `%c` format
/// validation and `chr`.
let unicode_max_cp_bigint : BigInt = 0x10FFFFN
///|
/// Inclusive lower bound of the Unicode surrogate block (U+D800..U+DFFF).
let unicode_surr_first : Int = 0xD800
///|
/// Inclusive upper bound of the Unicode surrogate block (U+D800..U+DFFF).
let unicode_surr_last : Int = 0xDFFF
///|
/// Decimal places for `%e`/`%f` printf format, matching C's
/// `printf("%.6e")`/`printf("%.6f")`.
let printf_float_precision : Int = 6
///|
/// Validates the count for a sequence repeat (`seq * n`). Returns the resolved
/// element count, treating a non-positive `n` as 0 (empty result); errors when
/// the count exceeds int32 range or `elem_len * count` would exceed the
/// allocation cap.
fn checked_repeat_count(n : BigInt, elem_len : Int) -> Result[Int, String] {
if n <= 0N {
return Ok(0)
}
if n > max_int32 {
return Err("repeat count \{n} too large")
}
let count = n.to_int()
if elem_len > 0 &&
elem_len.to_int64() * count.to_int64() >= max_alloc_elems.to_int64() {
return Err("excessive repeat (\{elem_len} * \{count} elements)")
}
Ok(count)
}
///|
/// Repeats the bytes of `s` exactly `n` times. Non-positive `n` yields an
/// empty string; out-of-int32 or allocation-cap violations return an `Err`.
fn repeat_string(
s : @value.StarlarkString,
n : BigInt,
) -> Result[@value.StarlarkString, String] {
let byte_len = s.byte_len()
let count = match checked_repeat_count(n, byte_len) {
Ok(c) => c
Err(e) => return Err(e)
}
let original_bytes = s.to_bytes()
let all_bytes : Array[Byte] = []
for _ in 0.. Result[@value.StarlarkList, String] {
let count = match checked_repeat_count(n, l.length()) {
Ok(c) => c
Err(e) => return Err(e)
}
let items : Array[@value.Value] = []
for _ in 0.. Result[Array[@value.Value], String] {
let count = match checked_repeat_count(n, t.length()) {
Ok(c) => c
Err(e) => return Err(e)
}
let items : Array[@value.Value] = []
for _ in 0.. Result[Bytes, String] {
let byte_len = b.length()
let count = match checked_repeat_count(n, byte_len) {
Ok(c) => c
Err(e) => return Err(e)
}
let items : Array[Byte] = []
for _ in 0.. @value.Value raise EvalErr {
if !ctx.opts.allow_float {
raise EvalErr(make_eval_error(ctx, "real division is not allowed"))
}
match (lv, rv) {
(@value.Value::Int(a), @value.Value::Int(b)) => {
if b.is_zero() {
raise EvalErr(make_eval_error(ctx, "floating-point division by zero"))
}
@value.Value::Float(finite_double(ctx, a) / finite_double(ctx, b))
}
(@value.Value::Float(a), @value.Value::Float(b)) => {
if b == 0.0 {
raise EvalErr(make_eval_error(ctx, "floating-point division by zero"))
}
@value.Value::Float(a / b)
}
(@value.Value::Int(a), @value.Value::Float(b)) => {
if b == 0.0 {
raise EvalErr(make_eval_error(ctx, "floating-point division by zero"))
}
@value.Value::Float(finite_double(ctx, a) / b)
}
(@value.Value::Float(a), @value.Value::Int(b)) => {
if b.is_zero() {
raise EvalErr(make_eval_error(ctx, "floating-point division by zero"))
}
@value.Value::Float(a / finite_double(ctx, b))
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} / \{rv.type_name()}",
),
)
}
}
///|
/// Implements floor division (`//`). For int pairs, delegates to
/// `@numeric.floor_div` which produces Python's signed-floor semantics. For
/// float operands, divides then applies `floor`.
fn eval_floor_div(
ctx : EvalContext,
lv : @value.Value,
rv : @value.Value,
_pos : @errors.Position,
) -> @value.Value raise EvalErr {
match (lv, rv) {
(@value.Value::Int(a), @value.Value::Int(b)) =>
@value.Value::Int(check(ctx, @numeric.floor_div(a, b)))
(@value.Value::Float(a), @value.Value::Float(b)) => {
if b == 0.0 {
raise EvalErr(make_eval_error(ctx, "floored division by zero"))
}
@value.Value::Float((a / b).floor())
}
(@value.Value::Int(a), @value.Value::Float(b)) => {
if b == 0.0 {
raise EvalErr(make_eval_error(ctx, "floored division by zero"))
}
@value.Value::Float((finite_double(ctx, a) / b).floor())
}
(@value.Value::Float(a), @value.Value::Int(b)) => {
if b.is_zero() {
raise EvalErr(make_eval_error(ctx, "floored division by zero"))
}
@value.Value::Float((a / finite_double(ctx, b)).floor())
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} // \{rv.type_name()}",
),
)
}
}
///|
/// Computes `a % b` with floor (not truncation) semantics: adjusts the
/// truncating remainder so the result carries the sign of the divisor `b`.
fn float_floor_mod(a : Double, b : Double) -> Double {
let r = a % b
// `%` truncates toward zero; floor-mod's result must carry the divisor's
// sign, so when remainder and divisor disagree in sign add one divisor back.
if r != 0.0 && (r < 0.0) != (b < 0.0) {
r + b
} else {
r
}
}
///|
/// Returns true when `f` is strictly negative or negative zero. Uses the
/// `1.0 / f` sign trick to distinguish `-0.0` from `+0.0`.
fn signbit(f : Double) -> Bool {
// Distinguishes -0.0 from +0.0: 1.0 / -0.0 is -inf (< 0), 1.0 / +0.0 is +inf.
f < 0.0 || (f == 0.0 && 1.0 / f < 0.0)
}
///|
/// Implements `%`: floor modulo for numeric pairs; percent format when the
/// left operand is a string (delegates to `percent_format`).
fn eval_mod(
ctx : EvalContext,
lv : @value.Value,
rv : @value.Value,
_pos : @errors.Position,
) -> @value.Value raise EvalErr {
match (lv, rv) {
(@value.Value::Int(a), @value.Value::Int(b)) =>
@value.Value::Int(check(ctx, @numeric.starlark_mod(a, b)))
(@value.Value::Float(a), @value.Value::Float(b)) => {
if b == 0.0 {
raise EvalErr(make_eval_error(ctx, "floating-point modulo by zero"))
}
@value.Value::Float(float_floor_mod(a, b))
}
(@value.Value::Int(a), @value.Value::Float(b)) => {
if b == 0.0 {
raise EvalErr(make_eval_error(ctx, "floating-point modulo by zero"))
}
@value.Value::Float(float_floor_mod(finite_double(ctx, a), b))
}
(@value.Value::Float(a), @value.Value::Int(b)) => {
if b.is_zero() {
raise EvalErr(make_eval_error(ctx, "floating-point modulo by zero"))
}
@value.Value::Float(float_floor_mod(a, finite_double(ctx, b)))
}
(@value.Value::String(s), _) => check(ctx, percent_format(s.raw(), rv))
_ =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} % \{rv.type_name()}",
),
)
}
}
///|
/// Coerces `arg` to a `BigInt` for `%d`/`%o`/`%x`/`%X` format specs.
/// Floats are truncated toward zero; `NaN`/`Inf` and non-numeric types are
/// rejected.
fn percent_to_int(arg : @value.Value) -> Result[BigInt, String] {
match arg {
@value.Value::Int(n) => Ok(n)
@value.Value::Float(f) =>
if f.is_inf() {
Err("cannot convert float infinity to integer")
} else if f.is_nan() {
Err("cannot convert float NaN to integer")
} else {
Ok(@numeric.double_to_bigint(f))
}
_ => Err("cannot convert \{arg.type_name()} to int")
}
}
///|
/// Implements the `%` format operator for strings. Supports positional
/// arguments (via a tuple or single value) and named `%(key)spec` arguments
/// (via a dict). Mirrors Go's `fmt.Sprintf` sub-language as used by
/// starlark-go.
fn percent_format(
fmt : String,
args : @value.Value,
) -> Result[@value.Value, String] {
let is_mapping = match args {
@value.Value::Dict(_) => true
_ => false
}
let arg_list : Array[@value.Value] = match args {
@value.Value::Tuple(t) => t
_ => [args]
}
let buf = @buffer.Buffer::Buffer()
let chars = fmt.to_array()
let n = chars.length()
let mut i = 0
let mut arg_idx = 0
while i < n {
let c = chars[i]
if c != '%' {
buf.write_char_utf8(c)
i += 1
continue
}
i += 1
if i >= n {
// Trailing '%': positional path. Argument availability is checked
// before reporting the missing conversion verb.
if arg_idx >= arg_list.length() && !is_mapping {
return Err("not enough arguments for format string")
}
return Err("incomplete format")
}
if chars[i] == '%' {
buf.write_char_utf8('%')
i += 1
continue
}
// Resolve (arg, spec): named %(key)spec or positional spec
let mut arg_val : @value.Value = @value.Value::None
let mut spec : Char = ' '
if chars[i] == '(' {
i += 1 // skip '('
let key_buf = StringBuilder::new()
while i < n && chars[i] != ')' {
key_buf.write_char(chars[i])
i += 1
}
if i >= n {
return Err("incomplete format key")
}
i += 1 // skip ')'
if i >= n {
return Err("incomplete format")
}
let key = key_buf.to_string()
spec = chars[i]
i += 1
match args {
@value.Value::Dict(d) => {
let kv = @value.Value::String(@value.StarlarkString::new(key))
match d.get(kv) {
Ok(Some(v)) => arg_val = v
Ok(None) => return Err("key not found: \{key}")
Err(e) => return Err(e)
}
}
_ => return Err("format requires a mapping")
}
} else {
if arg_idx >= arg_list.length() {
return Err("not enough arguments for format string")
}
arg_val = arg_list[arg_idx]
arg_idx += 1
spec = chars[i]
i += 1
}
let arg = arg_val
match spec {
's' =>
match arg {
@value.Value::String(sv) => buf.write_bytes(sv.to_bytes()[:])
_ =>
match arg.to_str_checked() {
Err(e) => return Err(e)
Ok(s) => buf.write_string_utf8(s)
}
}
'r' =>
match arg.repr_checked() {
Err(e) => return Err(e)
Ok(s) => buf.write_string_utf8(s)
}
'd' | 'i' =>
match percent_to_int(arg) {
Ok(n2) => buf.write_string_utf8(n2.to_string())
Err(cause) => return Err("%\{spec} format requires integer: \{cause}")
}
'o' =>
match percent_to_int(arg) {
Ok(n2) =>
if n2 < 0N {
buf.write_char_utf8('-')
buf.write_string_utf8(int64_to_octal(-n2))
} else {
buf.write_string_utf8(int64_to_octal(n2))
}
Err(cause) => return Err("%o format requires integer: \{cause}")
}
'x' =>
match percent_to_int(arg) {
Ok(n2) =>
if n2 < 0N {
buf.write_char_utf8('-')
buf.write_string_utf8(int64_to_hex(-n2, false))
} else {
buf.write_string_utf8(int64_to_hex(n2, false))
}
Err(cause) => return Err("%x format requires integer: \{cause}")
}
'X' =>
match percent_to_int(arg) {
Ok(n2) =>
if n2 < 0N {
buf.write_char_utf8('-')
buf.write_string_utf8(int64_to_hex(-n2, true))
} else {
buf.write_string_utf8(int64_to_hex(n2, true))
}
Err(cause) => return Err("%X format requires integer: \{cause}")
}
'e' | 'E' | 'f' | 'g' | 'G' => {
let f = match arg {
@value.Value::Float(f) => f
@value.Value::Int(n2) => @numeric.bigint_to_double(n2)
_ =>
return Err("%\{spec} format requires float, not \{arg.type_name()}")
}
let s2 = match spec {
'e' => format_float_e(f, false)
'E' => format_float_e(f, true)
'f' => format_float_f(f)
'g' | 'G' => {
let s3 = format_float_g(f)
if spec == 'G' && !f.is_nan() && !f.is_inf() {
s3.to_upper()
} else {
s3
}
}
_ => ""
}
buf.write_string_utf8(s2)
}
'c' =>
match arg {
@value.Value::Int(n2) => {
if n2 < 0N || n2 > unicode_max_cp_bigint {
return Err(
"%c format requires a valid Unicode code point, got \{n2}",
)
}
let cp = n2.to_int()
if cp >= unicode_surr_first && cp <= unicode_surr_last {
buf.write_char_utf8('\u{FFFD}')
} else {
buf.write_char_utf8(cp.unsafe_to_char())
}
}
@value.Value::String(s2) => {
let bytes = s2.to_bytes()
if bytes.length() == 0 {
return Err("%c format requires a single-character string")
}
let (rune_val, size) = @utf8util.utf8_decode_rune(bytes, 0)
if size != bytes.length() {
return Err("%c format requires a single-character string")
}
// Mirror Go's WriteRune: invalid UTF-8 → U+FFFD, valid → the rune.
let rune_char = if rune_val < 0 {
'\u{FFFD}'
} else {
rune_val.unsafe_to_char()
}
buf.write_char_utf8(rune_char)
}
_ =>
return Err(
"%c format requires int or single-character string, not \{arg.type_name()}",
)
}
_ => return Err("unknown conversion %\{spec}")
}
}
if arg_idx < arg_list.length() && !is_mapping {
return Err("too many arguments for format string")
}
Ok(@value.Value::String(@value.StarlarkString::from_bytes(buf.contents())))
}
///|
/// Formats a non-negative `BigInt` as an octal string without a leading `0o`
/// prefix, matching the `%o` conversion spec.
fn int64_to_octal(n : BigInt) -> String {
if n.is_zero() {
return "0"
}
let buf = StringBuilder::new()
let mut v = n
let digits : Array[Int] = []
while v > 0N {
digits.push((v % 8N).to_int())
v = v / 8N
}
let mut j = digits.length() - 1
while j >= 0 {
buf.write_char(('0'.to_int() + digits[j]).unsafe_to_char())
j -= 1
}
buf.to_string()
}
///|
/// Formats a non-negative `BigInt` as a hex string without a `0x` prefix.
/// `upper` selects uppercase A–F; lowercase otherwise. Used by `%x`/`%X`.
fn int64_to_hex(n : BigInt, upper : Bool) -> String {
if n.is_zero() {
return "0"
}
let buf = StringBuilder::new()
let mut v = n
let digits : Array[Int] = []
while v > 0N {
digits.push((v % 16N).to_int())
v = v / 16N
}
let base_char = if upper { 'A'.to_int() } else { 'a'.to_int() }
let mut j = digits.length() - 1
while j >= 0 {
let d = digits[j]
if d < 10 {
buf.write_char(('0'.to_int() + d).unsafe_to_char())
} else {
buf.write_char((base_char + d - 10).unsafe_to_char())
}
j -= 1
}
buf.to_string()
}
///|
/// Returns `10^n` as a `BigInt`, used in exact-arithmetic float formatting.
fn pow10_big(n : Int) -> BigInt {
let mut r = 1N
for _ in 0.. String {
let s = n.to_string()
let pad = width - s.length()
if pad <= 0 {
return s
}
let pbuf = StringBuilder::new()
for _ in 0.. (BigInt, Int) {
let bits = abs_f.reinterpret_as_uint64()
// 52 = mantissa bits; 0x7FF = 11-bit biased-exponent mask (bits 52..62)
let biased = ((bits >> 52) & 0x7FFUL).to_int()
// 52-bit fractional mantissa (bits 0..51)
let frac = bits & 0x000FFFFFFFFFFFFFUL
if biased == 0 {
// Subnormal: true exponent = 1 - 1023 - 52 = -1074
(BigInt::from_uint64(frac), -1074)
} else {
// Normal: prepend implicit leading 1 (bit 52); true exponent = biased - 1023 - 52 = biased - 1075
(BigInt::from_uint64(frac | 0x0010000000000000UL), biased - 1075)
}
}
///|
/// Divides `num` by `den` (both non-negative, `den > 0`) rounding the
/// quotient to the nearest integer with ties broken toward even ("round half
/// to even"), matching Go's `strconv.FormatFloat` rounding.
fn round_half_even_div(num : BigInt, den : BigInt) -> BigInt {
let q = num / den
let r = num % den
let twice = r * 2N
if twice < den {
q
} else if twice > den {
q + 1N
} else if (q % 2N).is_zero() {
q
} else {
q + 1N
}
}
///|
/// Rounds `abs_f × 10^scale` to the nearest integer using exact big-integer
/// arithmetic on the true binary value of `abs_f`, avoiding the rounding error
/// of an intermediate floating-point multiply. `abs_f` must be finite and ≥ 0.
fn round_scaled(abs_f : Double, scale : Int) -> BigInt {
let (mant, e) = decompose_double(abs_f)
let mut num = mant
let mut den = 1N
if e >= 0 {
num = num << e
} else {
den = den << -e
}
if scale >= 0 {
num = num * pow10_big(scale)
} else {
den = den * pow10_big(-scale)
}
round_half_even_div(num, den)
}
///|
/// Formats `f` in scientific notation with 6 decimal places (`%e`/`%E`),
/// matching C's `printf("%.6e")` output exactly. `upper` selects `E` vs `e`.
fn format_float_e(f : Double, upper : Bool) -> String {
if f.is_nan() {
return "nan"
}
if f == @double.infinity {
return "+inf"
}
if f == @double.neg_infinity {
return "-inf"
}
let e_char = if upper { "E" } else { "e" }
let is_neg = signbit(f)
let sign_str = if is_neg { "-" } else { "" }
let abs_f = if f < 0.0 { -f } else { f }
if abs_f == 0.0 {
return "\{sign_str}0.000000\{e_char}+00"
}
// Normalize to [10^p, 10^(p+1)) to produce exactly p+1 significant digits
// (1 before the decimal point, p = printf_float_precision after it).
let lo = pow10_big(printf_float_precision)
let hi = pow10_big(printf_float_precision + 1)
let mut exp = @math.log10(abs_f).floor().to_int()
let mut n = round_scaled(abs_f, printf_float_precision - exp)
while n < lo {
exp = exp - 1
n = round_scaled(abs_f, printf_float_precision - exp)
}
while n >= hi {
exp = exp + 1
n = round_scaled(abs_f, printf_float_precision - exp)
}
let digits = n.to_string()
let int_digit = digits[0:1].to_owned()
let frac_str = digits[1:printf_float_precision + 1].to_owned()
let exp_sign = if exp >= 0 { "+" } else { "-" }
let exp_abs = if exp >= 0 { exp } else { -exp }
let exp_str = if exp_abs < 10 { "0\{exp_abs}" } else { exp_abs.to_string() }
"\{sign_str}\{int_digit}.\{frac_str}\{e_char}\{exp_sign}\{exp_str}"
}
///|
/// Formats `f` in fixed-point notation with 6 decimal places (`%f`), matching
/// C's `printf("%.6f")` output exactly.
fn format_float_f(f : Double) -> String {
if f.is_nan() {
return "nan"
}
if f == @double.infinity {
return "+inf"
}
if f == @double.neg_infinity {
return "-inf"
}
let is_neg = signbit(f)
let sign_str = if is_neg { "-" } else { "" }
let abs_f = if f < 0.0 { -f } else { f }
let n = round_scaled(abs_f, printf_float_precision)
let scale = pow10_big(printf_float_precision)
let int_str = (n / scale).to_string()
let frac_str = pad_bigint(n % scale, printf_float_precision)
"\{sign_str}\{int_str}.\{frac_str}"
}
///|
/// Formats `f` for `%g`/`%G`, delegating to `@numeric.format_float` (the
/// general-purpose shortest-representation formatter).
fn format_float_g(f : Double) -> String {
@numeric.format_float(f)
}
///|
/// Implements `&`/`|`/`^` for integer pairs; `|` for dict pairs (union, right
/// takes precedence); and `&`/`|`/`^` for set pairs (intersection, union,
/// symmetric difference).
fn eval_bitwise(
ctx : EvalContext,
lv : @value.Value,
rv : @value.Value,
_pos : @errors.Position,
op : String,
) -> @value.Value raise EvalErr {
let op_char = match op {
"land" => "&"
"lor" => "|"
_ => "^"
}
match (lv, rv) {
(@value.Value::Int(a), @value.Value::Int(b)) => {
let r = match op {
"land" => a & b
"lor" => a | b
"lxor" => a ^ b
_ => a
}
@value.Value::Int(r)
}
(@value.Value::Dict(ld), @value.Value::Dict(rd)) if op == "lor" => {
let result = @value.StarlarkDict::new()
ld.each(fn(k, v) { ignore(result.set(k, v)) })
rd.each(fn(k, v) { ignore(result.set(k, v)) })
@value.Value::Dict(result)
}
(@value.Value::Dict(_), _) if op == "lor" =>
raise EvalErr(
make_eval_error(ctx, "unknown binary op: dict | \{rv.type_name()}"),
)
(@value.Value::Set(ls), @value.Value::Set(rs)) => {
let result = @value.StarlarkSet::new()
match op {
"lor" => {
ls.each(fn(v) { ignore(result.add(v)) })
rs.each(fn(v) { ignore(result.add(v)) })
}
"land" =>
rs.each(fn(v) {
if swallowed_contains(ls.contains(v)) {
ignore(result.add(v))
}
})
_ => {
ls.each(fn(v) {
if confirmed_absent(rs.contains(v)) {
ignore(result.add(v))
}
})
rs.each(fn(v) {
if confirmed_absent(ls.contains(v)) {
ignore(result.add(v))
}
})
}
}
@value.Value::Set(result)
}
(@value.Value::Set(_), _) =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: set \{op_char} \{rv.type_name()}",
),
)
(_, @value.Value::Set(_)) =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} \{op_char} set",
),
)
_ =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} \{op_char} \{rv.type_name()}",
),
)
}
}
///|
/// Implements `<<` and `>>` for integer pairs. Both directions first require
/// the shift count to fit in the int32 range (matching starlark-go's
/// `AsInt32`); left shifts at or above `max_lshift_bits` (512) additionally
/// raise an error, while right shifts within int32 range have no further
/// cap — a large enough count simply yields `0` (positive) or `-1`
/// (negative).
fn eval_shift(
ctx : EvalContext,
lv : @value.Value,
rv : @value.Value,
_pos : @errors.Position,
dir : String,
) -> @value.Value raise EvalErr {
match (lv, rv) {
(@value.Value::Int(a), @value.Value::Int(b)) => {
if b > max_int32 || b < min_int32 {
raise EvalErr(make_eval_error(ctx, "\{b} out of range"))
}
if b < 0N {
raise EvalErr(make_eval_error(ctx, "negative shift count: \{b}"))
}
match dir {
"lshift" => {
if b >= max_lshift_bits {
raise EvalErr(make_eval_error(ctx, "shift count too large: \{b}"))
}
@value.Value::Int(a << b.to_int())
}
_ => @value.Value::Int(a >> b.to_int())
}
}
_ => {
let op_sym = if dir == "lshift" { "<<" } else { ">>" }
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{lv.type_name()} \{op_sym} \{rv.type_name()}",
),
)
}
}
}
///|
/// Returns true when every element of `s1` is contained in `s2`.
fn set_is_subset(s1 : @value.StarlarkSet, s2 : @value.StarlarkSet) -> Bool {
let mut all_in = true
s1.each(fn(v) {
if all_in && !swallowed_contains(s2.contains(v)) {
all_in = false
}
})
all_in
}
///|
/// Implements ordered comparisons (`<`/`<=`/`>`/`>=`). Set pairs use the
/// subset/superset partial order; all other pairs delegate to
/// `@value.compare_values`. `strict_sign > 0` means `>` / `>=`; `strict`
/// distinguishes `<` from `<=`.
fn eval_cmp(
ctx : EvalContext,
lv : @value.Value,
rv : @value.Value,
_pos : @errors.Position,
strict_sign : Int,
strict : Bool,
) -> @value.Value raise EvalErr {
match (lv, rv) {
(@value.Value::Set(ls), @value.Value::Set(rs)) => {
let result = if strict_sign > 0 {
let is_sup = set_is_subset(rs, ls)
if strict {
is_sup && ls.length() > rs.length()
} else {
is_sup
}
} else {
let is_sub = set_is_subset(ls, rs)
if strict {
is_sub && ls.length() < rs.length()
} else {
is_sub
}
}
@value.Value::Bool(result)
}
_ => {
let op = if strict_sign > 0 {
if strict {
">"
} else {
">="
}
} else if strict {
"<"
} else {
"<="
}
let c = check(ctx, @value.compare_values(lv, rv, op~))
let result = if strict {
if strict_sign > 0 {
c > 0
} else {
c < 0
}
} else if strict_sign > 0 {
c >= 0
} else {
c <= 0
}
@value.Value::Bool(result)
}
}
}
///|
/// Implements the `in` membership test for all collection types. Dict
/// containment silences hash errors (matching starlark-go's `Mapping.Get`
/// semantics); set containment propagates them. Bytes accepts both a bytes
/// needle and an int byte value.
fn in_op(
ctx : EvalContext,
needle : @value.Value,
haystack : @value.Value,
_pos : @errors.Position,
) -> @value.Value raise EvalErr {
match haystack {
@value.Value::List(l) => {
for item in l.iter() {
if check(
ctx,
@value.starlark_equals_depth(needle, item, @value.compare_limit),
) {
return @value.Value::Bool(true)
}
}
@value.Value::Bool(false)
}
@value.Value::Tuple(t) => {
for item in t {
if check(
ctx,
@value.starlark_equals_depth(needle, item, @value.compare_limit),
) {
return @value.Value::Bool(true)
}
}
@value.Value::Bool(false)
}
// Dict silences all hash errors (starlark-go Mapping.Get: "cannot
// distinguish true errors from key not found"). Set propagates them
// (starlark-go Set.Has returns the error from the hashtable lookup).
@value.Value::Dict(d) =>
match d.get(needle) {
Ok(Some(_)) => @value.Value::Bool(true)
_ => @value.Value::Bool(false)
}
@value.Value::Set(s) => @value.Value::Bool(check(ctx, s.contains(needle)))
@value.Value::String(s) =>
match needle {
@value.Value::String(n) => @value.Value::Bool(s.raw().contains(n.raw()))
_ =>
raise EvalErr(
make_eval_error(
ctx,
"'in ' requires string as left operand, not \{needle.type_name()}",
),
)
}
@value.Value::Range(r) =>
match needle {
@value.Value::Int(n) =>
if n.compare_int64(@int64.MAX_VALUE) > 0 ||
n.compare_int64(@int64.MIN_VALUE) < 0 {
@value.Value::Bool(false)
} else {
@value.Value::Bool(r.contains(n.to_int64()))
}
@value.Value::Float(f) => {
if f.is_nan() || f.is_inf() {
raise EvalErr(
make_eval_error(
ctx,
"'in ' requires integer as left operand, not \{needle.type_name()}",
),
)
}
if f >= @int64.MAX_VALUE.to_double() ||
f < @int64.MIN_VALUE.to_double() {
@value.Value::Bool(false)
} else {
@value.Value::Bool(r.contains(f.to_int64()))
}
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"'in ' requires integer as left operand, not \{needle.type_name()}",
),
)
}
@value.Value::Bytes(haystack_b) =>
match needle {
@value.Value::Bytes(needle_b) => {
let hlen = haystack_b.length()
let nlen = needle_b.length()
if nlen == 0 {
return @value.Value::Bool(true)
}
if nlen > hlen {
return @value.Value::Bool(false)
}
let mut found = false
for i in 0..<=(hlen - nlen) {
let mut match_ = true
for j in 0.. {
if n < 0N || n > max_byte_val {
raise EvalErr(
make_eval_error(ctx, "int in bytes: \{n} out of range"),
)
}
let b = n.to_int().to_byte()
let mut found = false
for i in 0..
raise EvalErr(
make_eval_error(
ctx,
"'in bytes' requires bytes or int as left operand, not \{needle.type_name()}",
),
)
}
@value.Value::ExtVal(c) =>
match c.get_contains(needle) {
Some(Ok(b)) => @value.Value::Bool(b)
Some(Err(e)) => raise EvalErr(make_eval_error(ctx, e))
None =>
raise EvalErr(
make_eval_error(
ctx,
"argument of type '\{haystack.type_name()}' is not iterable",
),
)
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"unknown binary op: \{needle.type_name()} in \{haystack.type_name()}",
),
)
}
}
///|
/// Integer-indexed lookup for list, tuple, string, bytes, range, and
/// `StringElems`; dict lookup by arbitrary hashable key; `ExtVal` via the
/// length + index hooks. Negative integer indices count from the end.
fn eval_index(
ctx : EvalContext,
obj : @value.Value,
idx : @value.Value,
_pos : @errors.Position,
) -> @value.Value raise EvalErr {
match obj {
@value.Value::List(l) =>
match idx {
@value.Value::Int(i) => {
let j = check(ctx, adjust_index(i, l.length().to_int64(), "list"))
l[j.to_int()]
}
_ =>
raise EvalErr(
make_eval_error(ctx, "list index: got \{idx.type_name()}, want int"),
)
}
@value.Value::Tuple(t) =>
match idx {
@value.Value::Int(i) => {
let j = check(ctx, adjust_index(i, t.length().to_int64(), "tuple"))
t[j.to_int()]
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"tuple index: got \{idx.type_name()}, want int",
),
)
}
@value.Value::String(s) =>
match idx {
@value.Value::Int(i) => {
let j = check(ctx, adjust_index(i, s.byte_len().to_int64(), "string"))
let one_byte = Bytes::from_array([s.byte_at(j.to_int())])
@value.Value::String(@value.StarlarkString::from_bytes(one_byte))
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"string index: got \{idx.type_name()}, want int",
),
)
}
@value.Value::Bytes(b) =>
match idx {
@value.Value::Int(i) => {
let j = check(ctx, adjust_index(i, b.length().to_int64(), "bytes"))
@value.Value::Bytes(Bytes::from_array([b[j.to_int()]]))
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"bytes index: got \{idx.type_name()}, want int",
),
)
}
@value.Value::Dict(d) =>
match check(ctx, d.get(idx)) {
None => {
let key_str = match idx.repr_checked() {
Ok(s) => s
Err(_) => ""
}
raise EvalErr(make_eval_error(ctx, "key \{key_str} not in dict"))
}
Some(v) => v
}
@value.Value::Range(r) =>
match idx {
@value.Value::Int(i) => {
let len = r.length()
if len < 0L {
raise EvalErr(make_eval_error(ctx, "range has no len"))
}
let j = check(ctx, adjust_index(i, len, "range"))
@value.Value::Int(BigInt::from_int64(r.index_at(j)))
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"range index: got \{idx.type_name()}, want int",
),
)
}
@value.Value::StringElems(e) =>
match idx {
@value.Value::Int(i) => {
let bytes = e.source_string().to_bytes()
let j = check(
ctx,
adjust_index(i, bytes.length().to_int64(), "string.elems"),
)
if e.is_ords() {
@value.Value::Int(BigInt::from_int(bytes[j.to_int()].to_int()))
} else {
let one = Bytes::from_array([bytes[j.to_int()]])
@value.Value::String(@value.StarlarkString::from_bytes(one))
}
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"string.elems index: got \{idx.type_name()}, want int",
),
)
}
@value.Value::ExtVal(c) =>
match idx {
@value.Value::Int(i) => {
let len = match c.get_length() {
Ok(n) => n
Err(e) => raise EvalErr(make_eval_error(ctx, e))
}
let j = check(ctx, adjust_index(i, len.to_int64(), obj.type_name()))
match c.get_index(j.to_int()) {
Ok(v) => v
Err(e) => raise EvalErr(make_eval_error(ctx, e))
}
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"\{obj.type_name()} index: got \{idx.type_name()}, want int",
),
)
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"unhandled index operation \{obj.type_name()}[\{idx.type_name()}]",
),
)
}
}
///|
/// Item assignment for list (integer index) and dict (arbitrary hashable key),
/// and `ExtVal` (via `HasSetKey` or `HasSetIndex` hooks in that precedence
/// order). All other types raise.
fn set_index(
ctx : EvalContext,
obj : @value.Value,
idx : @value.Value,
val : @value.Value,
_pos : @errors.Position,
) -> Unit raise EvalErr {
match obj {
@value.Value::List(l) =>
match idx {
@value.Value::Int(i) => {
let j = check(ctx, adjust_index(i, l.length().to_int64(), "list"))
check(ctx, l.set(j.to_int(), val))
}
_ =>
raise EvalErr(
make_eval_error(ctx, "got \{idx.type_name()}, want int"),
)
}
@value.Value::Dict(d) => check(ctx, d.set(idx, val))
@value.Value::ExtVal(c) =>
// HasSetKey (arbitrary key) takes precedence; otherwise fall back to the
// integer-indexed HasSetIndex hook.
match c.do_set_key(idx, val) {
Some(Ok(_)) => ()
Some(Err(e)) => raise EvalErr(make_eval_error(ctx, e))
None =>
match idx {
@value.Value::Int(i) => {
let len = match c.get_length() {
Ok(n) => n
Err(e) => raise EvalErr(make_eval_error(ctx, e))
}
let j = check(
ctx,
adjust_index(i, len.to_int64(), obj.type_name()),
)
match c.do_set_index(j.to_int(), val) {
Some(Ok(_)) => ()
Some(Err(e)) => raise EvalErr(make_eval_error(ctx, e))
None =>
raise EvalErr(
make_eval_error(
ctx,
"\{obj.type_name()} value does not support item assignment",
),
)
}
}
_ =>
raise EvalErr(
make_eval_error(ctx, "got \{idx.type_name()}, want int"),
)
}
}
_ =>
raise EvalErr(
make_eval_error(
ctx,
"\{obj.type_name()} value does not support item assignment",
),
)
}
}
///|
/// Slices list, tuple, string, bytes, range, and `ExtVal` by optional
/// `[lo:hi:step]` bounds. Omitted bounds and step default to natural
/// start/end/1; a zero step always raises.
fn eval_slice(
ctx : EvalContext,
obj : @value.Value,
lo_opt : Int?,
hi_opt : Int?,
step_opt : Int?,
) -> @value.Value raise EvalErr {
let step = match step_opt {
None => 1
Some(s) => s
}
if step == 0 {
raise EvalErr(make_eval_error(ctx, "zero is not a valid slice step"))
}
match obj {
@value.Value::List(l) => {
let len = l.length()
let (start, stop) = normalize_slice(lo_opt, hi_opt, len, step)
let items : Array[@value.Value] = []
if step > 0 {
let mut i = start
while i < stop {
items.push(l[i])
i += step
}
} else {
let mut i = start
while i > stop {
items.push(l[i])
i += step
}
}
@value.Value::List(@value.StarlarkList::new(items))
}
@value.Value::Tuple(t) => {
let len = t.length()
let (start, stop) = normalize_slice(lo_opt, hi_opt, len, step)
let items : Array[@value.Value] = []
if step > 0 {
let mut i = start
while i < stop {
items.push(t[i])
i += step
}
} else {
let mut i = start
while i > stop {
items.push(t[i])
i += step
}
}
@value.Value::Tuple(items)
}
@value.Value::String(s) => {
let len = s.byte_len()
let (start, stop) = normalize_slice(lo_opt, hi_opt, len, step)
let bytes_arr : Array[Byte] = []
if step > 0 {
let mut i = start
while i < stop {
bytes_arr.push(s.byte_at(i))
i += step
}
} else {
let mut i = start
while i > stop {
bytes_arr.push(s.byte_at(i))
i += step
}
}
@value.Value::String(
@value.StarlarkString::from_bytes(Bytes::from_array(bytes_arr)),
)
}
@value.Value::Range(r) => {
let len = r.length()
if len < 0L {
raise EvalErr(make_eval_error(ctx, "range has no len"))
}
let (start, end_idx_raw) = normalize_slice_64(lo_opt, hi_opt, len, step)
let end_idx = if step > 0 && end_idx_raw < start {
start
} else if step < 0 && end_idx_raw > start {
start
} else {
end_idx_raw
}
let new_start = r.start() + r.step() * start
let new_stop = r.start() + r.step() * end_idx
let new_step = r.step() * step.to_int64()
@value.Value::Range(
@value.StarlarkRange::new(new_start, new_stop, new_step),
)
}
@value.Value::Bytes(b) => {
let len = b.length()
let (start, stop) = normalize_slice(lo_opt, hi_opt, len, step)
let bytes_arr : Array[Byte] = []
if step > 0 {
let mut i = start
while i < stop {
bytes_arr.push(b[i])
i += step
}
} else {
let mut i = start
while i > stop {
bytes_arr.push(b[i])
i += step
}
}
@value.Value::Bytes(Bytes::from_array(bytes_arr))
}
@value.Value::ExtVal(c) => {
let len = match c.get_length() {
Ok(n) => n
Err(e) => raise EvalErr(make_eval_error(ctx, e))
}
let (start, stop) = normalize_slice(lo_opt, hi_opt, len, step)
match c.do_slice(start, stop, step) {
Some(Ok(v)) => v
Some(Err(e)) => raise EvalErr(make_eval_error(ctx, e))
None =>
raise EvalErr(
make_eval_error(ctx, "invalid slice operand \{obj.type_name()}"),
)
}
}
_ =>
raise EvalErr(
make_eval_error(ctx, "invalid slice operand \{obj.type_name()}"),
)
}
}
///|
/// Clamps `[lo_opt, hi_opt]` slice bounds against an `Int` sequence length
/// for the given `step` direction. Omitted bounds default to the natural
/// start/end; out-of-range values are clamped into `[0, len]` (positive step)
/// or `[-1, len-1]` (negative step).
fn normalize_slice(
lo_opt : Int?,
hi_opt : Int?,
len : Int,
step : Int,
) -> (Int, Int) {
if step > 0 {
let start = match lo_opt {
None => 0
Some(i) => {
let j = if i < 0 { i + len } else { i }
if j < 0 {
0
} else if j > len {
len
} else {
j
}
}
}
let stop = match hi_opt {
None => len
Some(i) => {
let j = if i < 0 { i + len } else { i }
if j < 0 {
0
} else if j > len {
len
} else {
j
}
}
}
(start, stop)
} else {
let start = match lo_opt {
None => len - 1
Some(i) => {
let j = if i < 0 { i + len } else { i }
if j < 0 {
-1
} else if j >= len {
len - 1
} else {
j
}
}
}
let stop = match hi_opt {
None => -1
Some(i) => {
let j = if i < 0 { i + len } else { i }
if j < -1 {
-1
} else if j >= len {
len - 1
} else {
j
}
}
}
(start, stop)
}
}
///|
/// 64-bit variant of `normalize_slice`, used for `range` slices whose length
/// may exceed `Int` range.
fn normalize_slice_64(
lo_opt : Int?,
hi_opt : Int?,
len : Int64,
step : Int,
) -> (Int64, Int64) {
if step > 0 {
let start = match lo_opt {
None => 0L
Some(i) => {
let j = if i < 0 { i.to_int64() + len } else { i.to_int64() }
if j < 0L {
0L
} else if j > len {
len
} else {
j
}
}
}
let stop = match hi_opt {
None => len
Some(i) => {
let j = if i < 0 { i.to_int64() + len } else { i.to_int64() }
if j < 0L {
0L
} else if j > len {
len
} else {
j
}
}
}
(start, stop)
} else {
let start = match lo_opt {
None => len - 1L
Some(i) => {
let j = if i < 0 { i.to_int64() + len } else { i.to_int64() }
if j < 0L {
-1L
} else if j >= len {
len - 1L
} else {
j
}
}
}
let stop = match hi_opt {
None => -1L
Some(i) => {
let j = if i < 0 { i.to_int64() + len } else { i.to_int64() }
if j < -1L {
-1L
} else if j >= len {
len - 1L
} else {
j
}
}
}
(start, stop)
}
}
///|
/// Converts an augmented-assignment operator (`+=`, `-=`, etc.) to its
/// corresponding binary operator, so `eval_aug_val` can delegate to
/// `eval_binary` for non-special-cased operators.
fn aug_to_bin_op(aug_op : @syntax.AugOp) -> @syntax.BinaryOp {
match aug_op {
@syntax.AugOp::AugAdd => @syntax.BinaryOp::OpAdd
@syntax.AugOp::AugSub => @syntax.BinaryOp::OpSub
@syntax.AugOp::AugMul => @syntax.BinaryOp::OpMul
@syntax.AugOp::AugDiv => @syntax.BinaryOp::OpDiv
@syntax.AugOp::AugFloorDiv => @syntax.BinaryOp::OpFloorDiv
@syntax.AugOp::AugMod => @syntax.BinaryOp::OpMod
@syntax.AugOp::AugBitAnd => @syntax.BinaryOp::OpBitAnd
@syntax.AugOp::AugBitOr => @syntax.BinaryOp::OpBitOr
@syntax.AugOp::AugBitXor => @syntax.BinaryOp::OpBitXor
@syntax.AugOp::AugLShift => @syntax.BinaryOp::OpLShift
@syntax.AugOp::AugRShift => @syntax.BinaryOp::OpRShift
}
}