# Builtin Operators and Functions

`rpnbuiltin.h`/`rpnbuiltin.cpp` connect the [RPN evaluator](rpneval.md)'s untyped [`Value`](value.md) stack to the concretely-typed functors in [functors](rpnfunctor.md). The header holds template machinery that expands one functor into every `Value::Type` combination it needs to support, generating a runtime type-dispatch `switch` from a single template definition; the `.cpp` holds hand-written, non-templated builtins (vector/list construction, Monte Carlo call setup, ...) whose logic doesn't fit that pattern.

## The core per-element evaluators

`rpnMathFunc1<Tin, F>` and `rpnMathFunc2<Tin1, Tin2, F>` are the single- and dual-argument, in-place, element-wise workhorses behind almost everything else in this file. Both follow the same shape:

1. Derive the scalar result type from `decltype(functor(...))`, then the actual (possibly vectorized) result type via `Value::VectorType<...>` (see [Values](value.md)'s compile-time type-mapping templates).
2. Pick a destination `Value*`: reuse one of the argument slots in place if it is already tagged with the result type - avoiding an allocation for the common case (`Real op Real -> Real`, etc.) - otherwise allocate one temporary `Value`, sized once up front for vectors.
3. Run `functor.ok()` then `functor()` per element (once, for scalars), writing straight into the destination.
4. `swap()` the (possibly temporary) destination back into the original argument slot - which is why the argument `Value*` passed in doubles as the output, and callers never need to allocate a separate result.

`rpnAggregateFunc1<Tin, F>` follows the same destination-reuse idea for the aggregate functors (`FwAll`, `FwSum`, ...), whose result type doesn't depend on whether the input was scalar or vector.

This "compute in place, reusing the argument's own storage when the type already matches" strategy is the same one `Value`'s move-assignment operators and `RpnStack::pop()` already apply (see [Values](value.md), [the stack](rpnstack.md)) - worth reusing carefully here because expression evaluation runs once per instance, per Monte Carlo sample, per rebuild, and can happen very often over the course of a run.

## Type-dispatch wrappers

`mathFuncComp1`, `mathFuncComp2`, `mathAggregateFunc1`, `mathAggregateNumFunc1`, `mathRelOpComp2`, `mathBitOpComp1`, `mathBitOpComp2`, `mathLogicOp1`, `mathLogicOp2` are each a `switch` over `vp->type()` or `type_pair(v1p->type(), v2p->type())` (see [Values](value.md)'s `type_pair`) that selects the right `Tin`/`Tin1,Tin2` instantiation of `rpnMathFunc1`/`rpnMathFunc2`/`rpnAggregateFunc1` for a given functor `F`, falling through to `Status::Unsupported` for any combination the operator doesn't support. What's listed differs per wrapper: bitwise wrappers only list `Int`/`IntVec` combinations; relational wrappers additionally list `String`/`StringVec`; the plain math wrappers list `Int`/`Real` and their vector forms but not `String`.

These templates are shared between operators and named functions: `mathFuncComp2<FwPlus>` is what the [evaluator](rpneval.md)'s `TOp` handling calls for `Rpn::OpPlus`, and the exact same template (instantiated with different functors) backs `pow()`, `hypot()`, and the two-argument form of `min()`/`max()` in the [builtins table](context.md). There is exactly one implementation of "broadcast a binary math functor over Int/Real/vector combinations," reused everywhere that pattern is needed.

`scalarTypeCheck<Type>` and `typeConversion<Type>` are parameterized on the target `Value::Type` so `isint`/`isreal`/`isstring` and `int()`/`real()`/`string()` each share one implementation body, instantiated once per type instead of written out six times.

## Monte Carlo: `mcGenerator<F, narg>`

Shared implementation behind `gauss`/`agauss` (`narg=3`) and `unif`/`aunif` (`narg=2`, no `sigma` argument). Validates that the nominal/variation/(sigma) arguments are all scalar and convertible to `Real`, then - only when `ctx.ctxType()` is `Instance`, `Model`, or `Parameters` (see [the netlist context](rpnevalctx.md)) - retrieves a memoized uniform sample via `MCData::retrieveOrAdd()` and shapes it with the functor `F`; otherwise it returns the nominal value unchanged. `ctx.nextCallIndex()` is advanced unconditionally, so a later Monte Carlo call in the same expression still gets a distinct generator identity even when this one didn't draw.

## Hand-written builtins (`rpnbuiltin.cpp`)

- **`vectorCheck`/`listCheck`/`len`**: type/shape predicates, plus `len()` (the only one of the three that can fail, on a non-vector argument).
- **`vectorBuild(n[, value])`**: an `n`-element vector filled with a scalar (default `Int(0)`); the vector's element type follows the fill value's type.
- **`vectorRange(...)`**: implements `range(to)`, `range(from, to)`, `range(from, to, step)`. Promotes all arguments to a common `Int`/`Real` type first, rejects a zero step, and rounds `(to-from)/step` to get the element count so floating-point accumulation error can't silently drop or add a trailing element.
- **`vectorRandUnif(n)`**: an `n`-element vector of *ordinary* uniform randoms, drawn from one process-lifetime `std::mt19937_64` (`rng`) - unlike `MCData`'s per-generator, per-sample-reproducible randomness ([the netlist context](rpnevalctx.md)), this is netlist-authoring convenience (building a fixed random test vector), not statistical sampling.
- **`vectorInterleave(v1, ..., vn)`/`vectorSeparate(v, n, i)`**: a matched pair. `interleave` zips `n` equal-length vectors element by element; `separate` is the inverse, pulling out every `n`-th element of `v` starting at offset `i`.
- **`vectorPack`/`listPack`**: implement the netlist's two bracket syntaxes and correspond directly to `Rpn::TPackVec`/`TPackList` ([expression format](rpnexpr.md)):
  - `[a, b, c]` (`vectorPack`) builds a numeric/string *vector*, recursively flattening any nested list arguments (`vectorPackPreprocess`/`vectorPackCore`) and unifying to a common element type (`Int` widens to `Real` on mixing; an empty vector argument is compatible with anything).
  - `{a, b, c}` (`listPack`) builds a heterogeneous *list* (`ValueVector`) with no flattening or type unification - each argument becomes one list entry as-is.
- **`listFlatten`**: backs the `flatten()` builtin (one argument, must already be a list). One-level flatten: any element of the input list that is itself a list has its entries spliced directly into the result; every other element is moved across unchanged. Unlike `vectorPack`'s recursive flattening, nested lists more than one level deep are not fully unpacked.
- **`minWrapper`/`maxWrapper`**: the one-argument (aggregate over a vector) vs. two-argument (component-wise) dispatch behind `min`/`max`, matching their `{1, 2, ...}` arity range in the [builtins table](context.md).
- **`mathFuncSelector2`/`rpnSelectorFunc2<Tin, Tsel>`**: backs `Rpn::OpSelect` (`x[i]`), dispatched over every vector `Tin` (`Int`/`Real`/`String`/`Value` vector) crossed with a scalar or vector `Int` selector via `FwSelector` ([functors](rpnfunctor.md)) - a vector selector gathers a vector of elements (fancy indexing), a scalar selector extracts one element.
