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