Skip to main content

bop/
ops.rs

1//! Operator primitives shared across execution engines.
2//!
3//! These are pure functions over `Value` — no interpreter state, no AST.
4//! The tree-walking evaluator, the bytecode VM, and AOT-Rust output all
5//! dispatch to these so the language-level semantics of `+`, `*`, `==`,
6//! etc. live in exactly one place.
7//!
8//! Short-circuiting operators (`&&`, `||`) are NOT here: they depend on
9//! evaluation order and are the engine's responsibility.
10
11#[cfg(feature = "no_std")]
12use alloc::{format, string::ToString, vec::Vec};
13
14use crate::builtins::{
15    check_array_concat_memory, check_string_concat_memory, check_string_repeat_memory, error,
16    error_with_hint,
17};
18use crate::error::BopError;
19use crate::value::{Value, values_equal};
20
21// ─── Numeric coercion helpers ──────────────────────────────────────
22//
23// Int↔Number interplay follows Python's rules:
24// - `Int op Int` stays Int (overflow → `BopError`).
25// - `Int op Number` / `Number op Int` widens to `Number`.
26// - `Number op Number` stays `Number`.
27//
28// Division is split: `/` always returns `Number`; `//` always
29// returns `Int` via truncation toward zero. Modulo mirrors the
30// operand types.
31
32/// Promote a value to `f64` if it's a numeric type. Used for
33/// cross-type widening where one side is `Number`.
34fn to_f64(v: &Value) -> Option<f64> {
35    match v {
36        Value::Int(n) => Some(*n as f64),
37        Value::Number(n) => Some(*n),
38        _ => None,
39    }
40}
41
42fn int_overflow(op: &str, line: u32) -> BopError {
43    error(line, format!("Integer overflow in `{}`", op))
44}
45
46pub fn add(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
47    match (left, right) {
48        (Value::Int(a), Value::Int(b)) => a
49            .checked_add(*b)
50            .map(Value::Int)
51            .ok_or_else(|| int_overflow("+", line)),
52        (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
53        (Value::Int(a), Value::Number(b)) => Ok(Value::Number(*a as f64 + b)),
54        (Value::Number(a), Value::Int(b)) => Ok(Value::Number(a + *b as f64)),
55        (Value::Str(a), Value::Str(b)) => {
56            check_string_concat_memory(a.len(), b.len(), line)?;
57            Ok(Value::new_str(format!("{}{}", a, b)))
58        }
59        (Value::Str(a), b) => {
60            let b_display = format!("{}", b);
61            check_string_concat_memory(a.len(), b_display.len(), line)?;
62            Ok(Value::new_str(format!("{}{}", a, b_display)))
63        }
64        (a, Value::Str(b)) => {
65            let a_display = format!("{}", a);
66            check_string_concat_memory(a_display.len(), b.len(), line)?;
67            Ok(Value::new_str(format!("{}{}", a_display, b)))
68        }
69        (Value::Array(a), Value::Array(b)) => {
70            check_array_concat_memory(a.len(), b.len(), line)?;
71            let mut result = a.to_vec();
72            result.extend(b.to_vec());
73            Ok(Value::new_array(result))
74        }
75        _ => Err(error(
76            line,
77            format!("Can't add {} and {}", left.type_name(), right.type_name()),
78        )),
79    }
80}
81
82pub fn sub(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
83    match (left, right) {
84        (Value::Int(a), Value::Int(b)) => a
85            .checked_sub(*b)
86            .map(Value::Int)
87            .ok_or_else(|| int_overflow("-", line)),
88        (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a - b)),
89        (Value::Int(a), Value::Number(b)) => Ok(Value::Number(*a as f64 - b)),
90        (Value::Number(a), Value::Int(b)) => Ok(Value::Number(a - *b as f64)),
91        _ => Err(error(
92            line,
93            format!(
94                "Can't use `-` with {} and {}",
95                left.type_name(),
96                right.type_name()
97            ),
98        )),
99    }
100}
101
102pub fn mul(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
103    match (left, right) {
104        (Value::Int(a), Value::Int(b)) => a
105            .checked_mul(*b)
106            .map(Value::Int)
107            .ok_or_else(|| int_overflow("*", line)),
108        (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a * b)),
109        (Value::Int(a), Value::Number(b)) => Ok(Value::Number(*a as f64 * b)),
110        (Value::Number(a), Value::Int(b)) => Ok(Value::Number(a * *b as f64)),
111        // String repeat accepts any numeric count. Integers use
112        // their direct value; floats cast through `as usize`
113        // after a positivity / finiteness check (unchanged from
114        // the pre-phase-6 behaviour).
115        (Value::Str(s), Value::Int(n)) | (Value::Int(n), Value::Str(s)) => {
116            if *n < 0 {
117                return Err(error(line, format!("Can't repeat a string {} times", n)));
118            }
119            let count = *n as usize;
120            check_string_repeat_memory(s.len(), count, line)?;
121            Ok(Value::new_str(s.repeat(count)))
122        }
123        (Value::Str(s), Value::Number(n)) | (Value::Number(n), Value::Str(s)) => {
124            let nf = *n;
125            if nf < 0.0 || !nf.is_finite() {
126                return Err(error(line, format!("Can't repeat a string {} times", nf)));
127            }
128            let count = nf as usize;
129            check_string_repeat_memory(s.len(), count, line)?;
130            Ok(Value::new_str(s.repeat(count)))
131        }
132        _ => Err(error(
133            line,
134            format!(
135                "Can't multiply {} and {}",
136                left.type_name(),
137                right.type_name()
138            ),
139        )),
140    }
141}
142
143/// `/` always returns a `Number`, even for `Int / Int`. Matches
144/// Python's `/` and sidesteps the "1 / 2 == 0" surprise.
145pub fn div(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
146    let a = to_f64(left).ok_or_else(|| {
147        error(
148            line,
149            format!("Can't divide {} by {}", left.type_name(), right.type_name()),
150        )
151    })?;
152    let b = to_f64(right).ok_or_else(|| {
153        error(
154            line,
155            format!("Can't divide {} by {}", left.type_name(), right.type_name()),
156        )
157    })?;
158    if b == 0.0 {
159        return Err(error_with_hint(
160            line,
161            "Division by zero",
162            "You can't divide by 0.",
163        ));
164    }
165    Ok(Value::Number(a / b))
166}
167
168pub fn rem(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
169    match (left, right) {
170        (Value::Int(_), Value::Int(b)) if *b == 0 => Err(error_with_hint(
171            line,
172            "Modulo by zero",
173            "You can't use % with 0.",
174        )),
175        (Value::Int(a), Value::Int(b)) => a
176            .checked_rem(*b)
177            .map(Value::Int)
178            .ok_or_else(|| int_overflow("%", line)),
179        (Value::Number(_), Value::Number(b)) if *b == 0.0 => Err(error_with_hint(
180            line,
181            "Modulo by zero",
182            "You can't use % with 0.",
183        )),
184        (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a % b)),
185        (Value::Int(a), Value::Number(b)) => {
186            if *b == 0.0 {
187                return Err(error_with_hint(
188                    line,
189                    "Modulo by zero",
190                    "You can't use % with 0.",
191                ));
192            }
193            Ok(Value::Number((*a as f64) % b))
194        }
195        (Value::Number(a), Value::Int(b)) => {
196            if *b == 0 {
197                return Err(error_with_hint(
198                    line,
199                    "Modulo by zero",
200                    "You can't use % with 0.",
201                ));
202            }
203            Ok(Value::Number(a % (*b as f64)))
204        }
205        _ => Err(error(
206            line,
207            format!(
208                "Can't use % with {} and {}",
209                left.type_name(),
210                right.type_name()
211            ),
212        )),
213    }
214}
215
216pub fn eq(left: &Value, right: &Value) -> Value {
217    Value::Bool(values_equal(left, right))
218}
219
220pub fn not_eq(left: &Value, right: &Value) -> Value {
221    Value::Bool(!values_equal(left, right))
222}
223
224pub fn lt(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
225    compare(left, right, |a, b| a < b, "<", line)
226}
227
228pub fn gt(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
229    compare(left, right, |a, b| a > b, ">", line)
230}
231
232pub fn lt_eq(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
233    compare(left, right, |a, b| a <= b, "<=", line)
234}
235
236pub fn gt_eq(left: &Value, right: &Value, line: u32) -> Result<Value, BopError> {
237    compare(left, right, |a, b| a >= b, ">=", line)
238}
239
240pub fn neg(val: &Value, line: u32) -> Result<Value, BopError> {
241    match val {
242        Value::Int(n) => n
243            .checked_neg()
244            .map(Value::Int)
245            .ok_or_else(|| int_overflow("-", line)),
246        Value::Number(n) => Ok(Value::Number(-n)),
247        _ => Err(error(line, format!("Can't negate a {}", val.type_name()))),
248    }
249}
250
251pub fn not(val: &Value) -> Value {
252    Value::Bool(!val.is_truthy())
253}
254
255/// Coerce any numeric index (Int or Number) to an `i64`. Returns
256/// `None` for non-numeric values. Used by the `index_get` /
257/// `index_set` paths so both `arr[0]` (Int) and `arr[0.0]`
258/// (Number) still work after phase 6's Int/Number split.
259fn numeric_index(idx: &Value) -> Option<i64> {
260    match idx {
261        Value::Int(n) => Some(*n),
262        Value::Number(n) => Some(*n as i64),
263        _ => None,
264    }
265}
266
267pub fn index_get(obj: &Value, idx: &Value, line: u32) -> Result<Value, BopError> {
268    match obj {
269        Value::Array(arr) => {
270            let i = numeric_index(idx).ok_or_else(|| {
271                error(
272                    line,
273                    format!(
274                        "Can't index {} with {}",
275                        obj.type_name(),
276                        idx.type_name()
277                    ),
278                )
279            })?;
280            let actual = if i < 0 {
281                (arr.len() as i64 + i) as usize
282            } else {
283                i as usize
284            };
285            arr.get(actual).cloned().ok_or_else(|| {
286                error(
287                    line,
288                    format!(
289                        "Index {} is out of bounds (array has {} items)",
290                        i,
291                        arr.len()
292                    ),
293                )
294            })
295        }
296        Value::Str(s) => {
297            let i = numeric_index(idx).ok_or_else(|| {
298                error(
299                    line,
300                    format!(
301                        "Can't index {} with {}",
302                        obj.type_name(),
303                        idx.type_name()
304                    ),
305                )
306            })?;
307            let chars: Vec<char> = s.chars().collect();
308            let actual = if i < 0 {
309                (chars.len() as i64 + i) as usize
310            } else {
311                i as usize
312            };
313            chars
314                .get(actual)
315                .map(|c| Value::new_str(c.to_string()))
316                .ok_or_else(|| {
317                    error(
318                        line,
319                        format!(
320                            "Index {} is out of bounds (string has {} characters)",
321                            i,
322                            chars.len()
323                        ),
324                    )
325                })
326        }
327        Value::Dict(entries) => match idx {
328            // Missing keys return `none` — matches Python / JS /
329            // Lua convention and lines up with the language's
330            // "any variable can be `none`" story. Callers who
331            // need "present vs absent" disambiguation use
332            // `d.has(key)` explicitly, or `d[key].is_some()` to
333            // reach the same check via method.
334            Value::Str(key) => Ok(entries
335                .iter()
336                .find(|(k, _)| k.as_str() == key.as_str())
337                .map(|(_, v)| v.clone())
338                .unwrap_or(Value::None)),
339            _ => Err(error(
340                line,
341                format!(
342                    "Can't index {} with {}",
343                    obj.type_name(),
344                    idx.type_name()
345                ),
346            )),
347        },
348        _ => Err(error(
349            line,
350            format!("Can't index {} with {}", obj.type_name(), idx.type_name()),
351        )),
352    }
353}
354
355pub fn index_set(
356    obj: &mut Value,
357    idx: &Value,
358    val: Value,
359    line: u32,
360) -> Result<(), BopError> {
361    match obj {
362        Value::Array(arr) => {
363            let i = numeric_index(idx).ok_or_else(|| {
364                error(line, "Can't set index with these types")
365            })?;
366            let len = arr.len();
367            let actual = if i < 0 {
368                (len as i64 + i) as usize
369            } else {
370                i as usize
371            };
372            if actual >= len {
373                return Err(error(
374                    line,
375                    format!("Index {} is out of bounds (array has {} items)", i, len),
376                ));
377            }
378            arr.set(actual, val);
379            Ok(())
380        }
381        Value::Dict(entries) => match idx {
382            Value::Str(key) => {
383                entries.set_key(key, val);
384                Ok(())
385            }
386            _ => Err(error(line, "Can't set index with these types")),
387        },
388        _ => Err(error(line, "Can't set index with these types")),
389    }
390}
391
392// ─── Internal helpers ───────────────────────────────────────────────────────
393
394fn compare(
395    left: &Value,
396    right: &Value,
397    f: impl Fn(f64, f64) -> bool,
398    op_str: &str,
399    line: u32,
400) -> Result<Value, BopError> {
401    match (left, right) {
402        // Int / Int uses exact integer comparison so magnitudes
403        // beyond f64's 2^53 precision still compare correctly.
404        (Value::Int(a), Value::Int(b)) => {
405            let result = match op_str {
406                "<" => a < b,
407                ">" => a > b,
408                "<=" => a <= b,
409                _ => a >= b,
410            };
411            Ok(Value::Bool(result))
412        }
413        (Value::Number(a), Value::Number(b)) => Ok(Value::Bool(f(*a, *b))),
414        // Cross-type numeric comparison widens through `f64`.
415        (Value::Int(a), Value::Number(b)) => Ok(Value::Bool(f(*a as f64, *b))),
416        (Value::Number(a), Value::Int(b)) => Ok(Value::Bool(f(*a, *b as f64))),
417        (Value::Str(a), Value::Str(b)) => {
418            let result = match op_str {
419                "<" => a < b,
420                ">" => a > b,
421                "<=" => a <= b,
422                _ => a >= b,
423            };
424            Ok(Value::Bool(result))
425        }
426        _ => Err(error(
427            line,
428            format!(
429                "Can't compare {} and {} with `{}`",
430                left.type_name(),
431                right.type_name(),
432                op_str
433            ),
434        )),
435    }
436}