Re-associates a chain of nested lets into multi-argument applications.
A let is [(lam x body) rhs], so a chain of them nests to the right. Bindings that do not depend on each other can be applied as one argument list instead:
// before: three nested lets — 3 Apply + 3 LamAbs = 6 machine steps
[(lam a [(lam b [(lam c body) ec]) eb]) ea]
// after: one three-argument application — same step count on its own...
[[[(lam a (lam b (lam c body))) ea] eb] ec]
// ...but [[CaseConstrApply]] then encodes it as Case + Constr + 3 LamAbs = 5 steps
(case (constr 0 [ea, eb, ec]) [(lam a (lam b (lam c body)))])
A group of N bindings therefore saves N - 2 machine steps per execution of the chain. This pass only re-associates — the case/constr encoding is left to CaseConstrApply, which already fires on any application chain with 3+ arguments and must stay the single place that decision is made (Case/Constr are illegal before Plutus V3).
==Why the threshold is five, not three==
Steps are not the whole fee: the encoding also changes the script's size, and the two pull in opposite directions below N = 7.
In the flat encoding an application chain is pure tags, 4N bits. The case-constr form pays fixed framing first — Case tag 4, Constr tag 4, constructor index 8, field-list framing N+1 (one bit per field plus a terminator), branch-list framing 2 — so 19 + N bits. The difference is therefore
Δbits = (19 + N) - 4N = 19 - 3N
which is '''larger''' below N = 7 and smaller from N = 7 up (verified against the encoder for N = 1..12). On mainnet a script byte costs 15 lovelace of reference-script fee in every transaction using the script, while a machine step costs 6.92 lovelace (100 mem * 0.0577 + 16,000 cpu * 0.0000721) per ''execution''. So the break-even depends on how often the chain runs: for a script executed once per transaction it sits just under N = 4; at N = 3 the grouping loses about 12 lovelace, and only turns positive from roughly three executions per transaction.
MinRunSize is 5 rather than 4 because the theoretical N = 4 margin (+0.72 lovelace) does not survive measurement: flat is bit-packed, so a group's 7 theoretical bits round up to a whole byte in the encoded script. Measured over the ten example validators: a threshold of 4 nets +511 lovelace for +19 bytes and still makes one of them worse, while 5 nets +533 for no extra bytes at all and makes none worse.
All of this is conservative for chains that run more than once per transaction (inside a loop, or a script spending several inputs), where the step saving multiplies but the bytes are paid once.
==Grouping rule==
The chain is split into '''maximal contiguous runs'''. A binding joins the current run when
- its right-hand side does not mention any binder already in the run, and
- its name does not repeat a binder already in the run.
Bindings are never reordered. Aiken's split_body_lambda moves bindings between groups to grow them, which changes the order effects happen in; Scalus preserves source evaluation order.
==Why no purity guard is needed==
The CEK machine evaluates [f a] function-first, then argument. So [[[F ea] eb] ec] evaluates F (a lambda — effect-free), then ea, then the beta-reduction yields the next lambda (effect-free), then eb, then ec — exactly the order of the nested form. Under the subsequent case (constr 0 [...]) encoding the fields are also evaluated left to right. A right-hand side that errors, traces or diverges therefore does so at the same point relative to the others in both forms, so this pass applies to effectful bindings (a require(...) lowers to a binding with no uses) as readily as to values.
Moving a right-hand side out of the scope of the earlier binders cannot capture or free a name either: in the input it sits under those binders, so a free occurrence of one of them refers to the let, which is exactly what the dependency test rejects.
That argument covers ordering '''within''' the chain. It does not by itself cover a chain in the function position of an enclosing Apply, where CaseConstrApply would otherwise merge the enclosing arguments into the same constr and hoist them ahead of the chain body; rebuild leaves the outermost run nested in that case, which keeps the two forms identical. See rebuild.
Value parameters
- logger
-
Logger for tracking regrouping operations
- minRunSize
-
smallest run worth grouping; see the threshold discussion above
Attributes
- See also
-
CaseConstrApply for the pass that turns the resulting chains into
case/constr - Companion
- object
- Graph
-
- Supertypes