Skip to main content

doge_runtime/ops/
index.rs

1use bigdecimal::{ToPrimitive, Zero};
2use num_bigint::BigInt;
3
4use crate::error::{DogeError, DogeResult};
5use crate::value::Value;
6
7/// Resolve a possibly-negative index against a length, or raise a catchable
8/// out-of-bounds error. Negative indices count from the end (`-1` is the last).
9fn normalize_index(i: i64, len: usize) -> DogeResult<usize> {
10    let len_i = len as i64;
11    let idx = if i < 0 { i + len_i } else { i };
12    if idx < 0 || idx >= len_i {
13        Err(DogeError::index_out_of_bounds(format!(
14            "index {i} is out of bounds for length {len}"
15        )))
16    } else {
17        Ok(idx as usize)
18    }
19}
20
21/// Resolve an arbitrary-precision index. Any value too large to fit a machine
22/// index is, by definition, out of bounds for a real container — a catchable
23/// error, so an over-large `Int` index never panics.
24fn normalize_bigindex(i: &BigInt, len: usize) -> DogeResult<usize> {
25    match i.to_i64() {
26        Some(v) => normalize_index(v, len),
27        None => Err(DogeError::index_out_of_bounds(format!(
28            "index {i} is out of bounds for length {len}"
29        ))),
30    }
31}
32
33/// `container[index]`. List by Int, Dict by Str key, Str by character index
34/// (never byte index — `"héllo"[1] == "é"`).
35pub fn index_get(container: &Value, index: &Value) -> DogeResult {
36    match (container, index) {
37        (Value::List(items), Value::Int(i)) => {
38            let items = items.borrow();
39            let idx = normalize_bigindex(i, items.len())?;
40            Ok(items[idx].clone())
41        }
42        (Value::Str(s), Value::Int(i)) => {
43            let chars: Vec<char> = s.chars().collect();
44            let idx = normalize_bigindex(i, chars.len())?;
45            Ok(Value::str(chars[idx].to_string()))
46        }
47        (Value::Bytes(b), Value::Int(i)) => {
48            let idx = normalize_bigindex(i, b.len())?;
49            Ok(Value::int(b[idx]))
50        }
51        (Value::Dict(d), Value::Str(k)) => d
52            .borrow()
53            .get(k.as_ref())
54            .cloned()
55            .ok_or_else(|| DogeError::key_error(format!("no such key: {k:?}"))),
56        (Value::List(_) | Value::Str(_) | Value::Bytes(_), _) => {
57            Err(DogeError::type_error(format!(
58                "cannot index {} with {} (need an Int)",
59                container.describe(),
60                index.describe()
61            )))
62        }
63        (Value::Dict(_), _) => Err(DogeError::type_error(format!(
64            "cannot index a Dict with {} (keys are Str)",
65            index.describe()
66        ))),
67        // Explicit variants force a decision for each new indexable Value.
68        (
69            Value::Int(_)
70            | Value::Float(_)
71            | Value::Decimal(_)
72            | Value::Bool(_)
73            | Value::None
74            | Value::Object(_)
75            | Value::Function(_)
76            | Value::Class(_)
77            | Value::BoundMethod(_)
78            | Value::Error(_)
79            | Value::Socket(_)
80            | Value::Pup(_)
81            | Value::Bowl(_),
82            _,
83        ) => Err(DogeError::type_error(format!(
84            "cannot index {}",
85            container.describe()
86        ))),
87    }
88}
89
90/// `container[index] = value`. List and Dict are mutable in place; Str is
91/// immutable, so assigning into one is a catchable type error.
92pub fn index_set(container: &Value, index: &Value, value: Value) -> DogeResult<()> {
93    match (container, index) {
94        (Value::List(items), Value::Int(i)) => {
95            let mut items = items.borrow_mut();
96            let idx = normalize_bigindex(i, items.len())?;
97            items[idx] = value;
98            Ok(())
99        }
100        (Value::Dict(d), Value::Str(k)) => {
101            d.borrow_mut().insert(k.to_string(), value);
102            Ok(())
103        }
104        (Value::Str(_), _) => Err(DogeError::type_error(
105            "cannot assign into a Str (Str values are immutable)",
106        )),
107        (Value::Bytes(_), _) => Err(DogeError::type_error(
108            "cannot assign into a Bytes (Bytes values are immutable)",
109        )),
110        (Value::List(_), _) => Err(DogeError::type_error(format!(
111            "cannot index a List with {} (need an Int)",
112            index.describe()
113        ))),
114        (Value::Dict(_), _) => Err(DogeError::type_error(format!(
115            "cannot index a Dict with {} (keys are Str)",
116            index.describe()
117        ))),
118        // Explicit variants force a decision for each new assignable Value.
119        (
120            Value::Int(_)
121            | Value::Float(_)
122            | Value::Decimal(_)
123            | Value::Bool(_)
124            | Value::None
125            | Value::Object(_)
126            | Value::Function(_)
127            | Value::Class(_)
128            | Value::BoundMethod(_)
129            | Value::Error(_)
130            | Value::Socket(_)
131            | Value::Pup(_)
132            | Value::Bowl(_),
133            _,
134        ) => Err(DogeError::type_error(format!(
135            "cannot index into {}",
136            container.describe()
137        ))),
138    }
139}
140
141/// A slice bound (`start`/`end`) as an optional `i64`: the omitted parts of a
142/// slice arrive as `Value::None`, an explicit bound must be an Int.
143fn slice_bound(what: &str, v: &Value) -> DogeResult<Option<i64>> {
144    match v {
145        Value::None => Ok(None),
146        // Out-of-range Ints saturate because slice bounds clamp rather than error.
147        Value::Int(n) => Ok(Some(bigint_to_bound(n))),
148        _ => Err(DogeError::type_error(format!(
149            "a slice {what} must be an Int, not {}",
150            v.describe()
151        ))),
152    }
153}
154
155/// A slice bound as an `i64`, saturating an out-of-range `Int` toward the end it
156/// points at (a huge positive value clamps to the length, a huge negative to the
157/// start).
158fn bigint_to_bound(n: &BigInt) -> i64 {
159    n.to_i64()
160        .unwrap_or(if n.sign() == num_bigint::Sign::Minus {
161            i64::MIN
162        } else {
163            i64::MAX
164        })
165}
166
167/// The slice step: `None` defaults to `1`, an explicit `0` is a catchable value
168/// error, and a non-Int is a type error. A step past the machine range saturates
169/// (it selects at most one element either way).
170fn slice_step(v: &Value) -> DogeResult<i64> {
171    match v {
172        Value::None => Ok(1),
173        Value::Int(n) if n.is_zero() => Err(DogeError::value_error("a slice step cannot be zero")),
174        Value::Int(n) => Ok(bigint_to_bound(n)),
175        _ => Err(DogeError::type_error(format!(
176            "a slice step must be an Int, not {}",
177            v.describe()
178        ))),
179    }
180}
181
182/// Resolve a slice's `start`, `end`, and `step` against a length into the exact
183/// list of indices it selects, clamping out-of-range bounds (Python semantics):
184/// negative bounds count from the end, and a negative step walks backward.
185fn slice_indices(start: Option<i64>, end: Option<i64>, step: i64, len: usize) -> Vec<usize> {
186    let len = len as i64;
187    let (lower, upper) = if step < 0 { (-1, len - 1) } else { (0, len) };
188    let clamp = |bound: i64| {
189        let bound = if bound < 0 { bound + len } else { bound };
190        bound.clamp(lower, upper)
191    };
192    let start = match start {
193        Some(s) => clamp(s),
194        None => {
195            if step < 0 {
196                upper
197            } else {
198                lower
199            }
200        }
201    };
202    let end = match end {
203        Some(e) => clamp(e),
204        None => {
205            if step < 0 {
206                lower
207            } else {
208                upper
209            }
210        }
211    };
212
213    let mut indices = Vec::new();
214    let mut i = start;
215    if step > 0 {
216        while i < end {
217            indices.push(i as usize);
218            i += step;
219        }
220    } else {
221        while i > end {
222            indices.push(i as usize);
223            i += step;
224        }
225    }
226    indices
227}
228
229/// `container[start:end:step]`. A List yields a new List and a Str a new Str
230/// (character-based); every other value is a catchable type error. Bounds clamp
231/// rather than erroring, matching Python.
232pub fn slice_get(container: &Value, start: &Value, end: &Value, step: &Value) -> DogeResult {
233    let step = slice_step(step)?;
234    let start = slice_bound("start", start)?;
235    let end = slice_bound("end", end)?;
236    match container {
237        Value::List(items) => {
238            let items = items.borrow();
239            let picked = slice_indices(start, end, step, items.len())
240                .into_iter()
241                .map(|i| items[i].clone())
242                .collect();
243            Ok(Value::list(picked))
244        }
245        Value::Str(s) => {
246            let chars: Vec<char> = s.chars().collect();
247            let picked: String = slice_indices(start, end, step, chars.len())
248                .into_iter()
249                .map(|i| chars[i])
250                .collect();
251            Ok(Value::str(picked))
252        }
253        Value::Bytes(b) => {
254            let picked: Vec<u8> = slice_indices(start, end, step, b.len())
255                .into_iter()
256                .map(|i| b[i])
257                .collect();
258            Ok(Value::bytes(picked))
259        }
260        // Explicit variants force a decision for each new sliceable Value.
261        Value::Dict(_)
262        | Value::Int(_)
263        | Value::Float(_)
264        | Value::Decimal(_)
265        | Value::Bool(_)
266        | Value::None
267        | Value::Object(_)
268        | Value::Function(_)
269        | Value::Class(_)
270        | Value::BoundMethod(_)
271        | Value::Error(_)
272        | Value::Socket(_)
273        | Value::Pup(_)
274        | Value::Bowl(_) => Err(DogeError::type_error(format!(
275            "cannot slice {}",
276            container.describe()
277        ))),
278    }
279}
280
281/// The sequence a `for` loop walks: a List's elements, a Str's characters, or a
282/// Dict's keys in insertion order, captured as an owned snapshot taken when the
283/// loop starts. Mutating the original value inside the loop body does not change
284/// what the loop visits. Any other value is a catchable type error.
285pub fn iter_value(v: &Value) -> DogeResult<Vec<Value>> {
286    match v {
287        Value::List(items) => Ok(items.borrow().clone()),
288        Value::Str(s) => Ok(s.chars().map(|c| Value::str(c.to_string())).collect()),
289        Value::Bytes(b) => Ok(b.iter().map(|&x| Value::int(x)).collect()),
290        Value::Dict(entries) => Ok(entries
291            .borrow()
292            .iter()
293            .map(|(k, _)| Value::str(k))
294            .collect()),
295        // Explicit variants force a decision for each new iterable Value.
296        Value::Int(_)
297        | Value::Float(_)
298        | Value::Decimal(_)
299        | Value::Bool(_)
300        | Value::None
301        | Value::Object(_)
302        | Value::Function(_)
303        | Value::Class(_)
304        | Value::BoundMethod(_)
305        | Value::Error(_)
306        | Value::Socket(_)
307        | Value::Pup(_)
308        | Value::Bowl(_) => Err(DogeError::type_error(format!(
309            "cannot loop over {}",
310            v.describe()
311        ))),
312    }
313}
314
315/// Unpack `v` into the values a multiple-assignment binds: the same sequence a
316/// `for` loop walks (a List's elements, a Str's characters, or a Dict's keys),
317/// split to `fixed` leading targets plus, when `rest` is set, a trailing
318/// collector that gathers every surplus value into a List. The returned Vec has
319/// exactly `fixed` elements without a collector, or `fixed + 1` with one (its
320/// last element being the collector List). A non-iterable value or a length that
321/// cannot fill the targets is a catchable error, so `pls`/`oh no` can handle it.
322pub fn unpack_value(v: &Value, fixed: usize, rest: bool) -> DogeResult<Vec<Value>> {
323    let mut values = iter_value(v)
324        .map_err(|_| DogeError::type_error(format!("cannot unpack {}", v.describe())))?;
325    if rest {
326        if values.len() < fixed {
327            return Err(DogeError::value_error(format!(
328                "expected at least {fixed} values to unpack, but got {}",
329                values.len()
330            )));
331        }
332        let collected = values.split_off(fixed);
333        values.push(Value::list(collected));
334    } else if values.len() != fixed {
335        return Err(DogeError::value_error(format!(
336            "expected {fixed} values to unpack, but got {}",
337            values.len()
338        )));
339    }
340    Ok(values)
341}