Skip to main content

josie_core/
vm.rs

1//! Typed VM evaluator for compiled [`Expr`](crate::compiler::Expr).
2//!
3//! This module is the hot execution loop for compiled pipeline expressions.
4//! It avoids runtime string operator dispatch by matching over enum variants.
5
6use crate::compiler::Expr;
7use crate::jval::{JVal, format_f64};
8use crate::runtime::{Context, EvalError, Operators, State};
9use serde_json::Value;
10use std::rc::Rc;
11
12/// Iterator-local variables — direct struct field access, no HashMap.
13#[derive(Debug, Clone)]
14pub struct IterLocals {
15    pub item: JVal,
16    pub index: JVal,
17    pub acc: JVal,
18}
19
20impl IterLocals {
21    /// Create iterator locals with `item` and `index`.
22    #[inline]
23    pub fn new(item: JVal, index: i64) -> Self {
24        Self {
25            item,
26            index: JVal::Int(index),
27            acc: JVal::Null,
28        }
29    }
30
31    /// Create iterator locals with explicit reduce accumulator.
32    #[inline]
33    pub fn with_acc(item: JVal, index: i64, acc: JVal) -> Self {
34        Self {
35            item,
36            index: JVal::Int(index),
37            acc,
38        }
39    }
40
41    /// Create empty locals for non-iterator evaluation contexts.
42    #[inline]
43    pub fn empty() -> Self {
44        Self {
45            item: JVal::Null,
46            index: JVal::Int(0),
47            acc: JVal::Null,
48        }
49    }
50}
51
52/// Evaluate a compiled expression.
53///
54/// Direct match on Expr variants — zero HashMap dispatch in the hot path.
55/// Falls back to the existing Operators table only for `Expr::Call`.
56pub fn eval_expr(
57    expr: &Expr,
58    locals: &IterLocals,
59    state: &mut State,
60    operators: &Operators,
61) -> Result<JVal, EvalError> {
62    match expr {
63        // === Literals ===
64        Expr::Literal(v) => Ok(v.clone()),
65
66        // === Locals (zero overhead) ===
67        Expr::LocalItem => Ok(locals.item.clone()),
68        Expr::LocalIndex => Ok(locals.index.clone()),
69        Expr::LocalAcc => Ok(locals.acc.clone()),
70
71        // === State variable access ===
72        Expr::Var(path) => Ok(read_state_jval(state, path).unwrap_or(JVal::Null)),
73        Expr::VarDynamic(path_expr) => {
74            let path_val = eval_expr(path_expr, locals, state, operators)?;
75            match path_val {
76                JVal::Str(s) => Ok(read_state_jval(state, &s).unwrap_or(JVal::Null)),
77                _ => Ok(JVal::Null),
78            }
79        }
80
81        // === Arithmetic ===
82        Expr::Add(args) => {
83            if args.len() == 2 {
84                let a = eval_expr(&args[0], locals, state, operators)?;
85                let b = eval_expr(&args[1], locals, state, operators)?;
86                return Ok(add_jvals(a, b));
87            }
88            let mut total_i = 0i64;
89            let mut all_int = true;
90            let mut total_f = 0.0f64;
91            for arg in args {
92                let v = eval_expr(arg, locals, state, operators)?;
93                match &v {
94                    JVal::Int(n) if all_int => {
95                        total_i = total_i.wrapping_add(*n);
96                        total_f += *n as f64;
97                    }
98                    _ => {
99                        all_int = false;
100                        total_f += v.as_f64().unwrap_or(0.0);
101                    }
102                }
103            }
104            Ok(if all_int {
105                JVal::Int(total_i)
106            } else {
107                JVal::Float(total_f)
108            })
109        }
110
111        Expr::Sub(a, b) => {
112            let av = eval_expr(a, locals, state, operators)?;
113            let bv = eval_expr(b, locals, state, operators)?;
114            match (&av, &bv) {
115                (JVal::Int(x), JVal::Int(y)) => Ok(JVal::Int(x.wrapping_sub(*y))),
116                _ => Ok(JVal::Float(
117                    av.as_f64().unwrap_or(0.0) - bv.as_f64().unwrap_or(0.0),
118                )),
119            }
120        }
121
122        Expr::Mul(args) => {
123            if args.len() == 2 {
124                let a = eval_expr(&args[0], locals, state, operators)?;
125                let b = eval_expr(&args[1], locals, state, operators)?;
126                return Ok(mul_jvals(a, b));
127            }
128            let mut prod_i = 1i64;
129            let mut all_int = true;
130            let mut prod_f = 1.0f64;
131            for arg in args {
132                let v = eval_expr(arg, locals, state, operators)?;
133                match &v {
134                    JVal::Int(n) if all_int => {
135                        prod_i = prod_i.wrapping_mul(*n);
136                        prod_f *= *n as f64;
137                    }
138                    _ => {
139                        all_int = false;
140                        prod_f *= v.as_f64().unwrap_or(0.0);
141                    }
142                }
143            }
144            Ok(if all_int {
145                JVal::Int(prod_i)
146            } else {
147                JVal::Float(prod_f)
148            })
149        }
150
151        Expr::Div(a, b) => {
152            let av = eval_expr(a, locals, state, operators)?;
153            let bv = eval_expr(b, locals, state, operators)?;
154            let denom = bv.as_f64().unwrap_or(0.0);
155            if denom == 0.0 {
156                return Err(EvalError::new("division by zero"));
157            }
158            let numer = av.as_f64().unwrap_or(0.0);
159            // Return Int if both operands are Int and result is exact
160            if let (JVal::Int(x), JVal::Int(y)) = (&av, &bv) {
161                if *y != 0 && x % y == 0 {
162                    return Ok(JVal::Int(x / y));
163                }
164            }
165            Ok(JVal::Float(numer / denom))
166        }
167
168        Expr::Mod(a, b) => {
169            let av = eval_expr(a, locals, state, operators)?;
170            let bv = eval_expr(b, locals, state, operators)?;
171            match (&av, &bv) {
172                (JVal::Int(x), JVal::Int(y)) if *y != 0 => Ok(JVal::Int(x % y)),
173                _ => {
174                    let denom = bv.as_i64().unwrap_or(0);
175                    if denom == 0 {
176                        return Err(EvalError::new("mod by zero"));
177                    }
178                    Ok(JVal::Int(av.as_i64().unwrap_or(0) % denom))
179                }
180            }
181        }
182
183        // === Comparison ===
184        Expr::Eq(a, b) => {
185            let av = eval_expr(a, locals, state, operators)?;
186            let bv = eval_expr(b, locals, state, operators)?;
187            Ok(JVal::Bool(av == bv))
188        }
189        Expr::Neq(a, b) => {
190            let av = eval_expr(a, locals, state, operators)?;
191            let bv = eval_expr(b, locals, state, operators)?;
192            Ok(JVal::Bool(av != bv))
193        }
194        Expr::Gt(a, b) => {
195            let av = eval_expr(a, locals, state, operators)?;
196            let bv = eval_expr(b, locals, state, operators)?;
197            Ok(JVal::Bool(
198                av.cmp_numeric_or_string(&bv).is_some_and(|o| o.is_gt()),
199            ))
200        }
201        Expr::Lt(a, b) => {
202            let av = eval_expr(a, locals, state, operators)?;
203            let bv = eval_expr(b, locals, state, operators)?;
204            Ok(JVal::Bool(
205                av.cmp_numeric_or_string(&bv).is_some_and(|o| o.is_lt()),
206            ))
207        }
208        Expr::Gte(a, b) => {
209            let av = eval_expr(a, locals, state, operators)?;
210            let bv = eval_expr(b, locals, state, operators)?;
211            Ok(JVal::Bool(
212                av.cmp_numeric_or_string(&bv).is_some_and(|o| !o.is_lt()),
213            ))
214        }
215        Expr::Lte(a, b) => {
216            let av = eval_expr(a, locals, state, operators)?;
217            let bv = eval_expr(b, locals, state, operators)?;
218            Ok(JVal::Bool(
219                av.cmp_numeric_or_string(&bv).is_some_and(|o| !o.is_gt()),
220            ))
221        }
222
223        // === Logic (short-circuit) ===
224        Expr::And(args) => {
225            let mut last = JVal::Bool(true);
226            for arg in args {
227                last = eval_expr(arg, locals, state, operators)?;
228                if !last.is_truthy() {
229                    return Ok(JVal::Bool(false));
230                }
231            }
232            Ok(last)
233        }
234        Expr::Or(args) => {
235            for arg in args {
236                let v = eval_expr(arg, locals, state, operators)?;
237                if v.is_truthy() {
238                    return Ok(v);
239                }
240            }
241            Ok(JVal::Bool(false))
242        }
243        Expr::Not(a) => {
244            let v = eval_expr(a, locals, state, operators)?;
245            Ok(JVal::Bool(!v.is_truthy()))
246        }
247        Expr::If(cond, then_expr, else_expr) => {
248            let cv = eval_expr(cond, locals, state, operators)?;
249            if cv.is_truthy() {
250                eval_expr(then_expr, locals, state, operators)
251            } else if let Some(e) = else_expr {
252                eval_expr(e, locals, state, operators)
253            } else {
254                Ok(JVal::Null)
255            }
256        }
257
258        // === String ===
259        Expr::Concat(args) => {
260            let mut out = String::new();
261            for arg in args {
262                let v = eval_expr(arg, locals, state, operators)?;
263                jval_write_str(&v, &mut out);
264            }
265            Ok(JVal::Str(Rc::from(out.as_str())))
266        }
267        Expr::Trim(a) => {
268            let v = eval_expr(a, locals, state, operators)?;
269            Ok(JVal::Str(Rc::from(
270                match &v {
271                    JVal::Str(s) => s.trim().to_string(),
272                    other => other.display_string().trim().to_string(),
273                }
274                .as_str(),
275            )))
276        }
277        Expr::StrLen(a) => {
278            let v = eval_expr(a, locals, state, operators)?;
279            let n = match &v {
280                JVal::Str(s) => s.chars().count(),
281                other => other.display_string().chars().count(),
282            };
283            Ok(JVal::Int(n as i64))
284        }
285        Expr::Lower(a) => {
286            let v = eval_expr(a, locals, state, operators)?;
287            Ok(JVal::Str(Rc::from(
288                match &v {
289                    JVal::Str(s) => s.to_lowercase(),
290                    other => other.display_string().to_lowercase(),
291                }
292                .as_str(),
293            )))
294        }
295        Expr::Upper(a) => {
296            let v = eval_expr(a, locals, state, operators)?;
297            Ok(JVal::Str(Rc::from(
298                match &v {
299                    JVal::Str(s) => s.to_uppercase(),
300                    other => other.display_string().to_uppercase(),
301                }
302                .as_str(),
303            )))
304        }
305        Expr::Contains(hay, needle) => {
306            let h = eval_expr(hay, locals, state, operators)?;
307            let n = eval_expr(needle, locals, state, operators)?;
308            let result = match &h {
309                JVal::Str(s) => match &n {
310                    JVal::Str(ns) => s.contains(ns.as_ref()),
311                    _ => s.contains(&n.display_string().as_str()),
312                },
313                JVal::Array(arr) => arr.contains(&n),
314                _ => false,
315            };
316            Ok(JVal::Bool(result))
317        }
318        Expr::Template(fmt_expr, arg_exprs) => {
319            let fmt = eval_expr(fmt_expr, locals, state, operators)?;
320            let mut out = fmt.display_string();
321            for arg_expr in arg_exprs {
322                let v = eval_expr(arg_expr, locals, state, operators)?;
323                let rep = v.display_string();
324                if let Some(pos) = out.find("{}") {
325                    out.replace_range(pos..pos + 2, &rep);
326                }
327            }
328            Ok(JVal::Str(Rc::from(out.as_str())))
329        }
330
331        // === Type conversion ===
332        Expr::ToInt(a) => {
333            let v = eval_expr(a, locals, state, operators)?;
334            Ok(JVal::Int(v.as_i64().unwrap_or(0)))
335        }
336        Expr::ToFloat(a) => {
337            let v = eval_expr(a, locals, state, operators)?;
338            Ok(JVal::Float(v.as_f64().unwrap_or(0.0)))
339        }
340        Expr::ToString(a) => {
341            let v = eval_expr(a, locals, state, operators)?;
342            Ok(JVal::Str(Rc::from(
343                match v {
344                    JVal::Str(s) => return Ok(JVal::Str(s)),
345                    JVal::Null => String::new(),
346                    JVal::Int(n) => n.to_string(),
347                    JVal::Float(f) => format_f64(f),
348                    JVal::Bool(b) => b.to_string(),
349                    other => other.display_string(),
350                }
351                .as_str(),
352            )))
353        }
354
355        // === Collections ===
356        Expr::Len(a) => {
357            let v = eval_expr(a, locals, state, operators)?;
358            let n = match &v {
359                JVal::Str(s) => s.chars().count(),
360                JVal::Array(a) => a.len(),
361                JVal::Object(o) => o.len(),
362                _ => 0,
363            };
364            Ok(JVal::Int(n as i64))
365        }
366        Expr::Push(list_expr, item_expr) => {
367            let list = eval_expr(list_expr, locals, state, operators)?;
368            let item = eval_expr(item_expr, locals, state, operators)?;
369            let mut arr = match list {
370                JVal::Array(a) => Rc::try_unwrap(a).unwrap_or_else(|rc| (*rc).clone()),
371                _ => Vec::new(),
372            };
373            arr.push(item);
374            Ok(JVal::Array(Rc::new(arr)))
375        }
376        Expr::Get(collection_expr, key_expr) => {
377            let collection = eval_expr(collection_expr, locals, state, operators)?;
378            let key = eval_expr(key_expr, locals, state, operators)?;
379            let out = match (&collection, &key) {
380                (JVal::Object(obj), JVal::Str(k)) => obj.get(k.as_ref()).cloned(),
381                (JVal::Array(arr), JVal::Int(n)) if *n >= 0 => arr.get(*n as usize).cloned(),
382                (JVal::Array(arr), JVal::Str(s)) => {
383                    s.parse::<usize>().ok().and_then(|i| arr.get(i).cloned())
384                }
385                _ => None,
386            };
387            Ok(out.unwrap_or(JVal::Null))
388        }
389
390        // === Control flow ===
391        Expr::Do(args) => {
392            let mut last = JVal::Null;
393            for arg in args {
394                last = eval_expr(arg, locals, state, operators)?;
395            }
396            Ok(last)
397        }
398        Expr::Match(value_expr, cases) => {
399            let value = eval_expr(value_expr, locals, state, operators)?;
400            let wildcard = JVal::Str(Rc::from("_"));
401            for (pat_expr, result_expr) in cases {
402                let pat = eval_expr(pat_expr, locals, state, operators)?;
403                if pat == wildcard || pat == value {
404                    return eval_expr(result_expr, locals, state, operators);
405                }
406            }
407            Ok(JVal::Null)
408        }
409
410        // === State mutation ===
411        Expr::Set(path, value_expr) => {
412            let value = eval_expr(value_expr, locals, state, operators)?;
413            write_state_jval(state, path, value.clone());
414            Ok(value)
415        }
416
417        // === Tree-level iteration ===
418        Expr::Map(list_expr, body_expr) => {
419            let list = eval_expr(list_expr, locals, state, operators)?;
420            match list {
421                JVal::Array(arr) => {
422                    let mut out = Vec::with_capacity(arr.len());
423                    for (i, item) in arr.iter().enumerate() {
424                        let inner = IterLocals::new(item.clone(), i as i64);
425                        out.push(eval_expr(body_expr, &inner, state, operators)?);
426                    }
427                    Ok(JVal::Array(Rc::new(out)))
428                }
429                _ => Err(EvalError::new("map first arg must be array")),
430            }
431        }
432        Expr::Filter(list_expr, body_expr) => {
433            let list = eval_expr(list_expr, locals, state, operators)?;
434            match list {
435                JVal::Array(arr) => {
436                    let mut out = Vec::new();
437                    for (i, item) in arr.iter().enumerate() {
438                        let inner = IterLocals::new(item.clone(), i as i64);
439                        let keep = eval_expr(body_expr, &inner, state, operators)?;
440                        if keep.is_truthy() {
441                            out.push(item.clone());
442                        }
443                    }
444                    Ok(JVal::Array(Rc::new(out)))
445                }
446                _ => Err(EvalError::new("filter first arg must be array")),
447            }
448        }
449
450        // === Events (fast path does not carry EventContext; return Null) ===
451        Expr::EventValue | Expr::EventKey | Expr::EventPrevent => Ok(JVal::Null),
452
453        // === Side effects ===
454        Expr::Log(args) => {
455            let mut vals = Vec::with_capacity(args.len());
456            for arg in args {
457                vals.push(eval_expr(arg, locals, state, operators)?);
458            }
459            let json_vals: Vec<Value> = vals.iter().map(Value::from).collect();
460            eprintln!("[josie.log] {}", serde_json::Value::Array(json_vals));
461            Ok(JVal::Array(Rc::new(vals)))
462        }
463        Expr::Effect(args) => {
464            let mut last = JVal::Null;
465            for arg in args {
466                last = eval_expr(arg, locals, state, operators)?;
467            }
468            Ok(last)
469        }
470
471        // === Fallback: custom/unknown operator ===
472        Expr::Call(op_name, arg_exprs) => {
473            let mut json_args: Vec<Value> = Vec::with_capacity(arg_exprs.len());
474            for arg_expr in arg_exprs {
475                let jval = eval_expr(arg_expr, locals, state, operators)?;
476                json_args.push(Value::from(jval));
477            }
478            if let Some(op) = operators.get(op_name) {
479                let mut ctx = Context {
480                    state,
481                    operators,
482                    event: None,
483                };
484                let result = op(&json_args, &mut ctx)?;
485                Ok(JVal::from(result))
486            } else {
487                // Unknown op: try stripping "core." prefix
488                let bare = op_name.strip_prefix("core.").unwrap_or(op_name);
489                if let Some(op) = operators.get(bare) {
490                    let mut ctx = Context {
491                        state,
492                        operators,
493                        event: None,
494                    };
495                    let result = op(&json_args, &mut ctx)?;
496                    return Ok(JVal::from(result));
497                }
498                Err(EvalError::new(format!("unknown operator '{op_name}'")))
499            }
500        }
501    }
502}
503
504// ─── helpers ─────────────────────────────────────────────────────────────────
505
506#[inline]
507fn add_jvals(a: JVal, b: JVal) -> JVal {
508    match (&a, &b) {
509        (JVal::Int(x), JVal::Int(y)) => JVal::Int(x.wrapping_add(*y)),
510        _ => JVal::Float(a.as_f64().unwrap_or(0.0) + b.as_f64().unwrap_or(0.0)),
511    }
512}
513
514#[inline]
515fn mul_jvals(a: JVal, b: JVal) -> JVal {
516    match (&a, &b) {
517        (JVal::Int(x), JVal::Int(y)) => JVal::Int(x.wrapping_mul(*y)),
518        _ => JVal::Float(a.as_f64().unwrap_or(0.0) * b.as_f64().unwrap_or(0.0)),
519    }
520}
521
522fn jval_write_str(v: &JVal, out: &mut String) {
523    use std::fmt::Write;
524    match v {
525        JVal::Str(s) => out.push_str(s),
526        JVal::Null => {}
527        JVal::Int(n) => {
528            let _ = write!(out, "{n}");
529        }
530        JVal::Float(f) => out.push_str(&format_f64(*f)),
531        JVal::Bool(b) => {
532            let _ = write!(out, "{b}");
533        }
534        other => out.push_str(&other.display_string()),
535    }
536}
537
538fn read_state_jval(state: &State, path: &str) -> Option<JVal> {
539    if let Some(rest) = path.strip_prefix("client.") {
540        return map_get_jval(&state.client, rest);
541    }
542    if let Some(rest) = path.strip_prefix("server.") {
543        return map_get_jval(&state.server, rest);
544    }
545    // Bare name: check client first, then server
546    map_get_jval(&state.client, path).or_else(|| map_get_jval(&state.server, path))
547}
548
549fn map_get_jval(map: &serde_json::Map<String, Value>, path: &str) -> Option<JVal> {
550    if path.is_empty() {
551        return Some(JVal::from(Value::Object(map.clone())));
552    }
553    let mut parts = path.split('.');
554    let first = parts.next()?;
555    let mut current = map.get(first)?;
556    for part in parts {
557        match current {
558            Value::Object(obj) => current = obj.get(part)?,
559            Value::Array(arr) => {
560                let idx = part.parse::<usize>().ok()?;
561                current = arr.get(idx)?;
562            }
563            _ => return None,
564        }
565    }
566    Some(JVal::from(current.clone()))
567}
568
569fn write_state_jval(state: &mut State, path: &str, value: JVal) {
570    let json_val = Value::from(value);
571    let (scope, rest) = match path.split_once('.') {
572        Some(pair) => pair,
573        None => {
574            state.client.insert(path.to_string(), json_val);
575            return;
576        }
577    };
578    let target = match scope {
579        "client" => &mut state.client,
580        "server" => &mut state.server,
581        _ => {
582            state.client.insert(path.to_string(), json_val);
583            return;
584        }
585    };
586    // Simple single-level key after scope
587    if !rest.contains('.') {
588        target.insert(rest.to_string(), json_val);
589    } else {
590        // Nested path: reuse existing set_in_map logic via Value mutation
591        // For simplicity, insert via a temporary Value
592        use serde_json::Map;
593        fn set_nested(map: &mut Map<String, Value>, path: &str, value: Value) {
594            let parts: Vec<&str> = path.split('.').filter(|p| !p.is_empty()).collect();
595            if parts.is_empty() {
596                return;
597            }
598            let mut current = map;
599            for part in &parts[..parts.len().saturating_sub(1)] {
600                let entry = current
601                    .entry((*part).to_string())
602                    .or_insert_with(|| Value::Object(Map::new()));
603                if !entry.is_object() {
604                    *entry = Value::Object(Map::new());
605                }
606                current = entry.as_object_mut().unwrap();
607            }
608            current.insert(parts.last().unwrap().to_string(), value);
609        }
610        set_nested(target, rest, json_val);
611    }
612}