use crate::OpCode;
use crate::arena::{ContextStack, DataValue, IterGuard};
use crate::node::{PathSegment, ReduceHint};
use crate::operators::meta::ArithOp;
use crate::{CompiledNode, Engine, Result};
use bumpalo::Bump;
use datavalue::NumberValue;
use super::fused::{FieldCursor, FusedMapBody, arith_number, with_arith};
use super::input::{Items, IterArgKind, IterSrc, ResolvedInput, resolve_iter_input, resolve_value};
use super::nesting::AccumulatorDepth;
#[inline]
pub(crate) fn evaluate_reduce<'a>(
args: &'a [CompiledNode],
iter_arg_kind: IterArgKind,
ctx: &mut ContextStack<'a>,
engine: &Engine,
arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
let body = &args[1];
let initial: &'a DataValue<'a> = if args.len() == 3 {
engine.dispatch_node(&args[2], ctx, arena)?
} else {
crate::arena::singletons::singleton_null()
};
let resolved = 'source: {
if super::fast_paths::allowed(ctx, engine) && is_map_candidate(&args[0]) {
match try_fused_reduce_map(args, initial, ctx, engine, arena)? {
FusedOutcome::Done(value) => return Ok(value),
FusedOutcome::Mapped(mapped) => break 'source resolve_value(mapped, ctx)?,
FusedOutcome::Bail => {}
}
}
resolve_iter_input(&args[0], iter_arg_kind, ctx, engine, arena)?
};
let src = match resolved {
ResolvedInput::Iterable(s) => s,
ResolvedInput::Empty => return Ok(initial),
ResolvedInput::Bridge(av) => {
return reduce_arena_bridge(av, body, initial, ctx, engine, arena);
}
};
if src.is_empty() {
return Ok(initial);
}
if super::fast_paths::allowed(ctx, engine)
&& let Some(result) = try_reduce_fast_path(&src, initial, body, arena)
{
ctx.charge(FOLD_BODY_COST * src.len() as u64)?;
return Ok(result);
}
reduce_general(src.len(), src.0.iter(), body, initial, ctx, engine, arena)
}
#[inline]
fn reduce_general<'a>(
len: usize,
items: impl Iterator<Item = &'a DataValue<'a>>,
body: &'a CompiledNode,
initial: &'a DataValue<'a>,
ctx: &mut ContextStack<'a>,
engine: &Engine,
arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
let total = len as u32;
let mut acc_av: &'a DataValue<'a> = initial;
let mut depth = AccumulatorDepth::new(body);
let mut guard = IterGuard::new(ctx);
for (i, item) in items.enumerate() {
guard.step_reduce(item, acc_av);
acc_av = engine.run_iter_body(body, guard.stack(), arena, i as u32, total)?;
depth.step(acc_av)?;
}
drop(guard);
depth.finish(acc_av)?;
Ok(acc_av)
}
#[inline]
fn reduce_arena_bridge<'a>(
input: &'a DataValue<'a>,
body: &'a CompiledNode,
initial: &'a DataValue<'a>,
ctx: &mut ContextStack<'a>,
engine: &Engine,
arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
match input {
DataValue::Object(pairs) => reduce_general(
pairs.len(),
pairs.iter().map(|(_k, v)| v),
body,
initial,
ctx,
engine,
arena,
),
_ => Ok(initial),
}
}
const FOLD_BODY_COST: u64 = 3;
enum FusedOutcome<'a> {
Done(&'a DataValue<'a>),
Mapped(&'a DataValue<'a>),
Bail,
}
#[inline(always)]
fn is_map_candidate(node: &CompiledNode) -> bool {
let node = match node {
CompiledNode::Cse(data) => &data.inner,
node => node,
};
matches!(
node,
CompiledNode::BuiltinOperator {
opcode: OpCode::Map,
..
}
)
}
#[inline(never)]
fn try_fused_reduce_map<'a>(
args: &'a [CompiledNode],
initial: &'a DataValue<'a>,
ctx: &mut ContextStack<'a>,
engine: &Engine,
arena: &'a Bump,
) -> Result<FusedOutcome<'a>> {
let (map_node, memo_slot) = match &args[0] {
CompiledNode::Cse(data) => (&data.inner, Some(data.slot)),
node => (node, None),
};
let CompiledNode::BuiltinOperator {
opcode: OpCode::Map,
args: map_args,
iter_arg_kind: map_iter_kind,
..
} = map_node
else {
return Ok(FusedOutcome::Bail);
};
if map_args.len() != 2 {
return Ok(FusedOutcome::Bail);
}
let Some(fold) = detect_fold_shape(&args[1]) else {
return Ok(FusedOutcome::Bail);
};
if !fold.current_segments.is_empty() {
return Ok(FusedOutcome::Bail);
}
let Some(map_body) = FusedMapBody::detect(&map_args[1]) else {
return Ok(FusedOutcome::Bail);
};
let Some(acc) = initial.as_number().copied() else {
return Ok(FusedOutcome::Bail);
};
let lit = match &map_body {
FusedMapBody::ArithVarLit { lit, .. } => match lit.as_number() {
Some(n) => *n,
None => return Ok(FusedOutcome::Bail),
},
_ => NumberValue::from_i64(0),
};
if let Some(slot) = memo_slot
&& ctx.cse_slot(slot).is_some()
{
return Ok(FusedOutcome::Bail);
}
let fused = FusedMap {
map_args,
map_iter_kind: *map_iter_kind,
memo_slot,
fold,
map_body,
acc,
lit,
};
let outcome = fused.run(initial, ctx, engine, arena);
if outcome.is_err() {
ctx.push_error_step(args[0].id());
}
outcome
}
struct FusedMap<'n> {
map_args: &'n [CompiledNode],
map_iter_kind: IterArgKind,
memo_slot: Option<u16>,
fold: FoldShape<'n>,
map_body: FusedMapBody<'n>,
acc: NumberValue,
lit: NumberValue,
}
impl<'n> FusedMap<'n> {
fn run<'a>(
&self,
initial: &'a DataValue<'a>,
ctx: &mut ContextStack<'a>,
engine: &Engine,
arena: &'a Bump,
) -> Result<FusedOutcome<'a>>
where
'n: 'a,
{
ctx.charge(1)?;
let items =
match resolve_iter_input(&self.map_args[0], self.map_iter_kind, ctx, engine, arena)? {
ResolvedInput::Iterable(src) if !src.is_empty() => {
if let Some(value) =
run_fused_fold(&src, self.acc, self.lit, &self.fold, &self.map_body)
{
let per_item = self.map_body.body_cost() + 1 + FOLD_BODY_COST;
ctx.charge(per_item * src.len() as u64)?;
return Ok(FusedOutcome::Done(alloc_number(arena, value)));
}
Items::Array(src)
}
ResolvedInput::Iterable(_) | ResolvedInput::Empty => {
self.remember(crate::arena::singletons::singleton_empty_array(), ctx);
return Ok(FusedOutcome::Done(initial));
}
ResolvedInput::Bridge(DataValue::Object(pairs)) => Items::Object(pairs),
ResolvedInput::Bridge(value) => Items::Scalar(value),
};
let mapped = super::map::evaluate_map(items, self.map_args, ctx, engine, arena)?;
self.remember(mapped, ctx);
Ok(FusedOutcome::Mapped(mapped))
}
fn remember<'a>(&self, mapped: &'a DataValue<'a>, ctx: &mut ContextStack<'a>) {
if let Some(slot) = self.memo_slot {
ctx.fill_cse_slot(slot, mapped);
}
}
}
fn run_fused_fold(
src: &IterSrc<'_>,
mut acc: NumberValue,
lit: NumberValue,
fold: &FoldShape<'_>,
map_body: &FusedMapBody<'_>,
) -> Option<NumberValue> {
let op = fold.op;
let acc_is_lhs = fold.acc_is_lhs;
let mut cursors = FusedCursors::new(map_body);
with_arith!(op, |f| {
if acc_is_lhs {
for item in src.0 {
let mapped = mapped_number(map_body, &mut cursors, item, lit)?;
acc = f(acc, mapped);
}
} else {
for item in src.0 {
let mapped = mapped_number(map_body, &mut cursors, item, lit)?;
acc = f(mapped, acc);
}
}
});
Some(acc)
}
#[inline(always)]
fn alloc_number<'a>(arena: &'a Bump, n: NumberValue) -> &'a DataValue<'a> {
if let NumberValue::Integer(i) = n
&& let Some(singleton) = crate::arena::singletons::singleton_small_int(i)
{
return singleton;
}
arena.alloc(DataValue::Number(n))
}
struct FusedCursors<'n> {
a: FieldCursor<'n>,
b: Option<FieldCursor<'n>>,
}
impl<'n> FusedCursors<'n> {
fn new(map_body: &FusedMapBody<'n>) -> Self {
match map_body {
FusedMapBody::Extract { segments } => Self {
a: FieldCursor::new(segments),
b: None,
},
FusedMapBody::ArithVarLit { segments, .. } => Self {
a: FieldCursor::new(segments),
b: None,
},
FusedMapBody::ArithVarVar {
a_segments,
b_segments,
..
} => Self {
a: FieldCursor::new(a_segments),
b: Some(FieldCursor::new(b_segments)),
},
}
}
}
#[inline(always)]
fn mapped_number<'a>(
map_body: &FusedMapBody<'_>,
cursors: &mut FusedCursors<'_>,
item: &'a DataValue<'a>,
lit: NumberValue,
) -> Option<NumberValue> {
match map_body {
FusedMapBody::Extract { .. } => cursors.a.resolve(item)?.as_number().copied(),
FusedMapBody::ArithVarLit { op, var_is_lhs, .. } => {
let v = *cursors.a.resolve(item)?.as_number()?;
let (x, y) = if *var_is_lhs { (v, lit) } else { (lit, v) };
Some(arith_number(*op, x, y))
}
FusedMapBody::ArithVarVar { op, .. } => {
let a = *cursors.a.resolve(item)?.as_number()?;
let b = *cursors.b.as_mut()?.resolve(item)?.as_number()?;
Some(arith_number(*op, a, b))
}
}
}
pub(super) struct FoldShape<'a> {
op: ArithOp,
acc_is_lhs: bool,
current_segments: &'a [PathSegment],
}
pub(super) fn detect_fold_shape(body: &CompiledNode) -> Option<FoldShape<'_>> {
let CompiledNode::BuiltinOperator {
opcode,
args: body_args,
..
} = body
else {
return None;
};
let op = opcode.arith_op()?;
if body_args.len() != 2 {
return None;
}
let (current_arg, acc_is_lhs) = if is_bare_accumulator(&body_args[1]) {
(&body_args[0], false)
} else if is_bare_accumulator(&body_args[0]) {
(&body_args[1], true)
} else {
return None;
};
let current_segments = if let CompiledNode::Var {
segments,
reduce_hint,
..
} = current_arg
{
match reduce_hint {
ReduceHint::Current => &[][..],
ReduceHint::CurrentPath if segments.len() >= 2 => &segments[1..],
_ => return None,
}
} else {
return None;
};
Some(FoldShape {
op,
acc_is_lhs,
current_segments,
})
}
fn is_bare_accumulator(node: &CompiledNode) -> bool {
match node {
CompiledNode::Var {
reduce_hint: ReduceHint::Accumulator,
..
} => true,
CompiledNode::Var {
reduce_hint: ReduceHint::AccumulatorPath,
segments,
..
} => segments.len() == 1,
_ => false,
}
}
fn try_reduce_fast_path<'a>(
src: &IterSrc<'a>,
initial: &'a DataValue<'a>,
body: &CompiledNode,
arena: &'a Bump,
) -> Option<&'a DataValue<'a>> {
let FoldShape {
op,
acc_is_lhs,
current_segments,
} = detect_fold_shape(body)?;
let mut current_field = FieldCursor::new(current_segments);
let mut acc = initial.as_number().copied()?;
with_arith!(op, |f| {
if acc_is_lhs {
for item in src.0 {
acc = f(acc, *current_field.resolve(item)?.as_number()?);
}
} else {
for item in src.0 {
acc = f(*current_field.resolve(item)?.as_number()?, acc);
}
}
});
Some(alloc_number(arena, acc))
}