sui-eval 0.1.147

Clean-room Nix language evaluator — lazy tree-walker + bytecode VM with construction-guaranteed Lazy<T>
Documentation
//! List builtins: length, head, tail, elemAt, elem, map, filter, sort, foldl',
//! genList, concatMap, concatLists, all, any, partition, groupBy.

use super::*;

pub(crate) fn register(builtins: &mut NixAttrs) {
    register_builtin(builtins, "length", |args| {
        Ok(Value::Int(args[0].as_list()?.len() as i64))
    });
    register_builtin(builtins, "head", |args| {
        let list = args[0].as_list()?;
        list.first()
            .cloned()
            .ok_or_else(|| EvalError::TypeError("head: empty list".to_string()))
    });
    register_builtin(builtins, "tail", |args| {
        let list = args[0].as_list()?;
        if list.is_empty() {
            return Err(EvalError::TypeError("tail: empty list".to_string()));
        }
        Ok(Value::List(Rc::new(NixList::new(list[1..].to_vec()))))
    });
    register_builtin(builtins, "elemAt", |args| {
        // Curried: builtins.elemAt list index
        let list = args[0].as_list()?.to_vec();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "elemAt<partial>",
            func: Rc::new(move |args2| {
                let idx = args2[0].as_int()? as usize;
                list.get(idx)
                    .cloned()
                    .ok_or_else(|| EvalError::TypeError(format!("elemAt: index {idx} out of bounds")))
            }),
        })))
    });
    register_builtin(builtins, "elem", |args| {
        let needle = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "elem<partial>",
            func: Rc::new(move |args2| {
                let haystack = args2[0].as_list()?;
                Ok(Value::Bool(haystack.contains(&needle)))
            }),
        })))
    });
    register_builtin(builtins, "genList", |args| {
        let func = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "genList<partial>",
            func: Rc::new(move |args2| {
                let n = args2[0].as_int()?;
                // CppNix rejects negative counts with "negative list length";
                // the prior sui behavior was to silently return `[]` because
                // `for i in 0..n` is an empty range when n < 0 on i64.
                // That's a silent-Ok bug — values CppNix rejects must not
                // succeed here. Mirror the CppNix error shape.
                if n < 0 {
                    return Err(EvalError::TypeError(format!(
                        "genList: negative list length {n}"
                    )));
                }
                let mut result = Vec::with_capacity(n as usize);
                for i in 0..n {
                    result.push(crate::eval::apply(func.clone(), Value::Int(i))?);
                }
                Ok(Value::List(Rc::new(NixList::new(result))))
            }),
        })))
    });

    // ── Higher-order list operations (critical for nixpkgs) ─────
    //
    // Clone cost analysis: all `.clone()` calls on `func`, `pred`, and
    // list element `v` in these loops are Rc reference-count bumps, NOT
    // deep copies.  The Value enum's heap variants are:
    //
    //   Lambda(Closure { env: Env(Rc<EnvInner>), .. })  → Rc bump
    //   Builtin(BuiltinFn { func: Rc<BuiltinFunc>, .. }) → Rc bump
    //   Attrs(NixAttrs)  → BTreeMap clone (but inner Values are Rc'd)
    //   List(Vec<Value>) → Vec clone (but inner Values are Rc'd)
    //   String(NixString) → String clone (typically interned/small)
    //   Thunk(Thunk(Rc<RefCell<ThunkRepr>>))  → Rc bump
    //
    // For the common case in nixpkgs — `map`, `filter`, `foldl'` over
    // lists of attrsets or lambdas — every clone is O(1).  The `apply`
    // function consumes its `arg: Value` by value, so we must clone once
    // per predicate/function call; matched elements are cloned a second
    // time to push into the result Vec.  This is inherent to the
    // ownership model and already minimal.

    register_builtin(builtins, "map", |args| {
        let func = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "map<partial>",
            func: Rc::new(move |args2| {
                let list = args2[0].as_list()?;
                // CppNix: map returns a lazy list where each element is a
                // thunk wrapping `f(element)`. Elements are only forced when
                // accessed. Eagerly applying `f` to all elements breaks
                // lazy list processing in nixpkgs.
                let result: Vec<Value> = list.iter()
                    .map(|v| {
                        let f = func.clone();
                        let val = v.clone();
                        Value::Thunk(Thunk::new_native(move || {
                            crate::eval::apply(f, val)
                        }))
                    })
                    .collect();
                Ok(Value::List(Rc::new(NixList::new(result))))
            }),
        })))
    });
    register_builtin(builtins, "filter", |args| {
        let pred = args[0].clone(); // Rc bump
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "filter<partial>",
            func: Rc::new(move |args2| {
                let list = args2[0].as_list()?;
                let mut result = Vec::new();
                for v in list {
                    if crate::eval::apply_and_force(pred.clone(), v.clone())?.as_bool()? {
                        result.push(v.clone());
                    }
                }
                Ok(Value::List(Rc::new(NixList::new(result))))
            }),
        })))
    });
    register_builtin(builtins, "foldl'", |args| {
        let func = args[0].clone(); // Rc bump
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "foldl'<p1>",
            func: Rc::new(move |args2| {
                let init = args2[0].clone();
                let func2 = func.clone();
                Ok(Value::Builtin(Box::new(BuiltinFn {
                    name: "foldl'<p2>",
                    func: Rc::new(move |args3| {
                        let list = args3[0].as_list()?;
                        let mut acc = init.clone();
                        for v in list {
                            let partial = crate::eval::apply(func2.clone(), acc)?;
                            // Force acc after each step — matches CppNix's
                            // foldl' which calls forceValue(vCur). Without
                            // this, acc becomes a nested thunk chain (20+
                            // levels for nixpkgs overlays) that cascades
                            // into 150K forces instead of CppNix's 96.
                            acc = crate::eval::force_concrete(
                                &crate::eval::apply(partial, v.clone())?
                            )?.into_value();
                        }
                        Ok(acc)
                    }),
                })))
            }),
        })))
    });
    register_builtin(builtins, "concatMap", |args| {
        let func = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "concatMap<partial>",
            func: Rc::new(move |args2| {
                let list = args2[0].as_list()?;
                let mut result = Vec::new();
                for v in list {
                    let mapped = crate::eval::apply_and_force(func.clone(), v.clone())?;
                    let inner = mapped.as_list()?;
                    if crate::perf::enabled() {
                        crate::perf::inc(crate::perf::Counter::ListConcatCalls);
                        crate::perf::add(
                            crate::perf::Counter::ListConcatElemsCopied,
                            inner.len() as u64,
                        );
                    }
                    result.extend_from_slice(inner);
                }
                Ok(Value::List(Rc::new(NixList::new(result))))
            }),
        })))
    });
    register_builtin(builtins, "concatLists", |args| {
        let lists = args[0].as_list()?;
        // Pre-compute total length to allocate once.
        let total_len: usize = lists.iter()
            .filter_map(|v| crate::eval::force_value(v).ok()
                .and_then(|fv| fv.as_list().ok().map(|l| l.len())))
            .sum();
        let mut result = Vec::with_capacity(total_len);
        for v in lists {
            let forced = crate::eval::force_value(v)?;
            // M2.6 ROOT #4 diagnostics (byte-neutral, env-gated, zero hot-path
            // cost): a non-list element in `concatLists`'s argument is the
            // `concatLists: expected list, got null` failure mode — the
            // module-system fixpoint softening (`in_promise_eval`) returned
            // `null` for a `config.<x>` select-miss that flowed into a
            // list-typed position.  `SUI_M26_CLTRACE` reports the offending
            // element's type + file stack; `SUI_M26_CLDUMP` (requires
            // `SUI_TRACE_EVAL=ring`) dumps the ring-buffer tail so the
            // deepest `mergeModules'` frame that produced the null is visible.
            if !matches!(forced, Value::List(_)) {
                if std::env::var_os("SUI_M26_CLTRACE").is_some() {
                    let stack = crate::eval::eval_file_stack_snapshot();
                    let tail: Vec<String> = stack.iter().rev().take(6).cloned().collect();
                    eprintln!("[M26 CONCATLISTS-null] elem_type={} outer_len={} filestack(top6)={:?}", forced.type_name(), lists.len(), tail);
                }
                if std::env::var_os("SUI_M26_CLDUMP").is_some() {
                    eprintln!("[M26 CONCATLISTS-null] elem_type={} outer_len={} — ring tail:", forced.type_name(), lists.len());
                    crate::trace::dump_ring_tail(12);
                }
            }
            let inner = forced.as_list()?;
            if crate::perf::enabled() {
                crate::perf::inc(crate::perf::Counter::ListConcatCalls);
                crate::perf::add(
                    crate::perf::Counter::ListConcatElemsCopied,
                    inner.len() as u64,
                );
            }
            result.extend(inner.iter().cloned());
        }
        Ok(Value::List(Rc::new(NixList::new(result))))
    });
    register_builtin(builtins, "sort", |args| {
        let cmp = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "sort<partial>",
            func: Rc::new(move |args2| {
                let mut list = args2[0].as_list()?.to_vec();
                if list.len() <= 1 {
                    return Ok(Value::List(Rc::new(NixList::new(list))));
                }
                // O(n log n) stable sort via Rust's merge sort.
                // Capture any comparator error and propagate after sort.
                let mut err: Option<EvalError> = None;
                list.sort_by(|a, b| {
                    if err.is_some() {
                        return std::cmp::Ordering::Equal;
                    }
                    match crate::eval::apply(cmp.clone(), a.clone())
                        .and_then(|partial| crate::eval::apply(partial, b.clone()))
                        .and_then(|v| crate::eval::force_value(&v))
                        .and_then(|v| v.as_bool().map_err(|_| {
                            EvalError::TypeError("sort comparator must return bool".into())
                        }))
                    {
                        Ok(true) => std::cmp::Ordering::Less,
                        Ok(false) => std::cmp::Ordering::Greater,
                        Err(e) => {
                            err = Some(e);
                            std::cmp::Ordering::Equal
                        }
                    }
                });
                if let Some(e) = err {
                    return Err(e);
                }
                Ok(Value::List(Rc::new(NixList::new(list))))
            }),
        })))
    });
    register_builtin(builtins, "all", |args| {
        let pred = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "all<partial>",
            func: Rc::new(move |args2| {
                let list = args2[0].as_list()?;
                for v in list {
                    if !crate::eval::apply_and_force(pred.clone(), v.clone())?.as_bool()? {
                        return Ok(Value::Bool(false));
                    }
                }
                Ok(Value::Bool(true))
            }),
        })))
    });
    register_builtin(builtins, "any", |args| {
        let pred = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "any<partial>",
            func: Rc::new(move |args2| {
                let list = args2[0].as_list()?;
                for v in list {
                    if crate::eval::apply_and_force(pred.clone(), v.clone())?.as_bool()? {
                        return Ok(Value::Bool(true));
                    }
                }
                Ok(Value::Bool(false))
            }),
        })))
    });

    // partition — split list by predicate into { right, wrong }
    register_builtin(builtins, "partition", |args| {
        let pred = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "partition<partial>",
            func: Rc::new(move |args2| {
                let list = args2[0].as_list()?;
                let mut right = Vec::new();
                let mut wrong = Vec::new();
                for v in list {
                    if crate::eval::apply_and_force(pred.clone(), v.clone())?.as_bool()? {
                        right.push(v.clone());
                    } else {
                        wrong.push(v.clone());
                    }
                }
                let mut result = NixAttrs::new();
                result.insert("right".to_string(), Value::List(Rc::new(NixList::new(right))));
                result.insert("wrong".to_string(), Value::List(Rc::new(NixList::new(wrong))));
                Ok(Value::Attrs(Rc::new(result)))
            }),
        })))
    });

    // groupBy — group list elements by key function
    register_builtin(builtins, "groupBy", |args| {
        let func = args[0].clone();
        Ok(Value::Builtin(Box::new(BuiltinFn {
            name: "groupBy<partial>",
            func: Rc::new(move |args2| {
                let list = args2[0].as_list()?;
                let mut groups: std::collections::BTreeMap<String, Vec<Value>> =
                    std::collections::BTreeMap::new();
                for v in list {
                    let key = crate::eval::apply_and_force(func.clone(), v.clone())?;
                    let key_str = key.as_string()?.to_string();
                    groups.entry(key_str).or_default().push(v.clone());
                }
                let mut result = NixAttrs::new();
                for (k, vs) in groups {
                    result.insert(k, Value::List(Rc::new(NixList::new(vs))));
                }
                Ok(Value::Attrs(Rc::new(result)))
            }),
        })))
    });
}