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