@@ -8813,6 +8813,38 @@ impl RegVm {
88138813 folder: &VmClosure,
88148814 base: usize,
88158815 ) -> Result<VmValue, EvalError> {
8816+ // Fast path: a fold whose folder is a recognized simple numeric binary
8817+ // closure (`|acc, x| acc <op> x`) over a list of scalar `Int`/`Float`
8818+ // values is the hot shape for sum/product-style reductions. Running it as
8819+ // a tight loop over the element values — calling the *same*
8820+ // `eval_numeric_binary` the interpreter uses, in the *same* operand
8821+ // order, on the *same* values — avoids a full frame setup + bytecode
8822+ // dispatch per element while producing bit-identical results (identical
8823+ // f64 ops, order, NaN/inf, and error behavior). Any case that does not
8824+ // exactly match (wrong shape, non-scalar element, captures present)
8825+ // falls through to the generic interpreter path below.
8826+ if let Some(form) = recognize_numeric_binary_closure(unit, folder) {
8827+ if matches!(state, VmValue::Int(_) | VmValue::Float(_)) {
8828+ let list = list.borrow();
8829+ if list
8830+ .iter()
8831+ .all(|item| matches!(item, VmValue::Int(_) | VmValue::Float(_)))
8832+ {
8833+ for item in list.iter() {
8834+ // Preserve the closure's operand order exactly: `state`
8835+ // and `item` are placed at the two param registers, so
8836+ // whichever param the lhs/rhs reads determines the order.
8837+ let (lhs, rhs) = if form.lhs_is_state {
8838+ (&state, item)
8839+ } else {
8840+ (item, &state)
8841+ };
8842+ state = eval_numeric_binary(form.op, lhs, rhs)?;
8843+ }
8844+ return Ok(state);
8845+ }
8846+ }
8847+ }
88168848 let len = list.borrow().len();
88178849 for index in 0..len {
88188850 let item = list_item_at(&list, index, "List.fold")?;
@@ -13064,6 +13096,71 @@ fn int_overflow_error(operation: &str, lhs: i64, rhs: i64) -> EvalError {
1306413096 ))
1306513097}
1306613098
13099+ /// A folder closure recognized as a single numeric binary op over its two
13100+ /// parameters: `op` is the arithmetic operator and `lhs_is_state` says whether
13101+ /// the op's left operand is the accumulator (param 0) — i.e. `acc <op> x` — or
13102+ /// the element (`x <op> acc`). Used to fast-path `List.fold` without losing the
13103+ /// closure's exact operand order.
13104+ struct NumericBinaryClosure {
13105+ op: BinaryOp,
13106+ lhs_is_state: bool,
13107+ }
13108+
13109+ /// Recognize `|state, x| state <op> x` (or `x <op> state`) closures with no
13110+ /// captures whose body is exactly one arithmetic instruction returning its
13111+ /// result. Returns `None` for anything else so the caller falls back to the
13112+ /// generic interpreter (the recognizer is intentionally conservative — a missed
13113+ /// match only forgoes a speedup, never changes results).
13114+ fn recognize_numeric_binary_closure(
13115+ unit: &RegUnit,
13116+ closure: &VmClosure,
13117+ ) -> Option<NumericBinaryClosure> {
13118+ // The fast path supplies the two operands by value at the param registers;
13119+ // captured values would not be supplied, so require a capture-free closure.
13120+ if !closure.captures.is_empty() {
13121+ return None;
13122+ }
13123+ let function = &unit.functions[closure.function];
13124+ if function.params != 2 {
13125+ return None;
13126+ }
13127+ // Param registers: captures occupy `0..captures.len()` (none here), then the
13128+ // two params at registers 0 and 1.
13129+ let state_reg = 0usize;
13130+ let item_reg = 1usize;
13131+ let [instr, RegInstr::Return { src }] = function.code.as_slice() else {
13132+ return None;
13133+ };
13134+ let (op, dst, lhs, rhs) = arithmetic_binop_parts(instr)?;
13135+ if dst != *src {
13136+ return None;
13137+ }
13138+ // Both operands must be exactly the two distinct param registers (so every
13139+ // input is a supplied param and nothing else is read).
13140+ let lhs_is_state = if lhs == state_reg && rhs == item_reg {
13141+ true
13142+ } else if lhs == item_reg && rhs == state_reg {
13143+ false
13144+ } else {
13145+ return None;
13146+ };
13147+ Some(NumericBinaryClosure { op, lhs_is_state })
13148+ }
13149+
13150+ /// If `instr` is one of the numeric arithmetic instructions handled by
13151+ /// [`eval_numeric_binary`], return `(op, dst, lhs, rhs)`. Bitwise/shift ops are
13152+ /// excluded: they are `Int`-only and not routed through `eval_numeric_binary`.
13153+ fn arithmetic_binop_parts(instr: &RegInstr) -> Option<(BinaryOp, Reg, Reg, Reg)> {
13154+ match instr {
13155+ RegInstr::AddInt { dst, lhs, rhs } => Some((BinaryOp::Add, *dst, *lhs, *rhs)),
13156+ RegInstr::SubInt { dst, lhs, rhs } => Some((BinaryOp::Subtract, *dst, *lhs, *rhs)),
13157+ RegInstr::MulInt { dst, lhs, rhs } => Some((BinaryOp::Multiply, *dst, *lhs, *rhs)),
13158+ RegInstr::DivInt { dst, lhs, rhs } => Some((BinaryOp::Divide, *dst, *lhs, *rhs)),
13159+ RegInstr::ModInt { dst, lhs, rhs } => Some((BinaryOp::Modulo, *dst, *lhs, *rhs)),
13160+ _ => None,
13161+ }
13162+ }
13163+
1306713164fn eval_numeric_binary(op: BinaryOp, lhs: &VmValue, rhs: &VmValue) -> Result<VmValue, EvalError> {
1306813165 match (lhs, rhs) {
1306913166 (VmValue::Int(lhs), VmValue::Int(rhs)) => match op {
0 commit comments