Skip to main content

doge_runtime/ops/
index.rs

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