Skip to content

Experiment: && vs builtinAnd vs alternatives — boolean AND chaining budget#7583

Draft
Unisay wants to merge 3 commits intomasterfrom
yura/experiment-and-chaining-budget
Draft

Experiment: && vs builtinAnd vs alternatives — boolean AND chaining budget#7583
Unisay wants to merge 3 commits intomasterfrom
yura/experiment-and-chaining-budget

Conversation

@Unisay
Copy link
Contributor

@Unisay Unisay commented Feb 10, 2026

Note

Not for merge — this is an experiment to share findings with colleagues. Related to PR #7562.

Context

Philip DiSarro's PR #7562 introduces builtinAnd and builtinIf helpers as alternatives to standard && for boolean condition chaining in Plutus validators. The claim is that builtinAnd (using BI.ifThenElse + lambda/unit) avoids delay/force overhead and is cheaper than &&.

This experiment measures the actual execution budget across 6 patterns with 3 test scenarios each (18 golden tests total).

Patterns tested

Patterns returning Bool (1–4)

  1. Standard && — lazy, plugin-rewritten to delay/force
  2. builtinAnd — Philip's helper using BI.ifThenElse + lambda/unit
  3. Multi-way if — negated guards
  4. Direct BI.ifThenElse — fully hand-written lambda/unit chain

Patterns returning BuiltinUnit with traceError on failure (5–6)

  1. builtinAnd + if/error — Philip's exact claimed usage: if (builtinAnd a $ builtinAnd b c) then ok else error
  2. Nested builtinIf — Philip's actual validator pattern from PR feat: native builtin data vs th builtin data benchmark #7562

Fairness

Patterns 2, 4, 5, 6 are compiled in a separate module (BuiltinAndLib.hs) with Philip's exact GHC flags from ValidatorOptimized.hs:

  • -fno-full-laziness, -fno-strictness, -fno-specialise, -fno-spec-constr
  • -fno-ignore-interface-pragmas, -fno-omit-interface-pragmas
  • -fno-unbox-small-strict-fields, -fno-unbox-strict-fields
  • -fplugin-opt PlutusTx.Plugin:conservative-optimisation
  • -fplugin-opt PlutusTx.Plugin:datatypes=BuiltinCasing
  • {-# INLINE #-} pragmas (not INLINEABLE)

Results

Patterns 1–4 (return Bool)

Pattern AllTrue CPU EarlyFail CPU LateFail CPU Short-circuits?
1. && (lazy) 551,970 301,390 551,970 Yes
2. builtinAnd 839,970 839,970 839,970 No
3. Multi-way if 583,970 301,390 583,970 Yes
4. Direct BI.ifThenElse 647,970 349,390 647,970 Yes

Patterns 5–6 (return BuiltinUnit, traceError on failure)

Pattern AllTrue CPU EarlyFail LateFail Short-circuits?
5. builtinAnd + if/error 919,970 error error No
6. Nested builtinIf 727,970 error error Yes

(Fail cases produce traceError, so no budget is reported — this is the intended validator behavior.)

Key findings

  1. builtinAnd does not short-circuit. builtinAnd :: Bool -> Bool -> Bool takes strict arguments — both Bool values are computed before the function body runs. All 3 scenarios produce identical budgets (839,970 CPU). This holds even with Philip's exact GHC flags and INLINE pragmas.

  2. builtinAnd is the most expensive pattern. It costs 52% more CPU than standard && (839,970 vs 551,970) in the AllTrue case, and 178% more in the EarlyFail case (839,970 vs 301,390).

  3. Standard && is the cheapest pattern overall. It properly short-circuits and has the lowest CPU cost in all scenarios.

  4. Nested builtinIf works (pattern 6). When conditions are placed inside lambda bodies, short-circuiting is preserved. This is what Philip's actual validator code in PR feat: native builtin data vs th builtin data benchmark #7562 does — but it's a different pattern from builtinAnd.

  5. The UPLC confirms it. Pattern 2 compiles all lessThanInteger calls into let-bindings before any case expression. Pattern 6 nests lessThanInteger inside lambda bodies.

UPLC comparison

Pattern 2 (builtinAnd) — no short-circuiting:

(\x y z ->
    (\b ->         -- z < 100 computed here
       (\b ->      -- y < 100 computed here
          (\b ->   -- x < 100 computed here
             (\b -> case b [(\ds -> False), (\ds -> b)] ())
               (case b [(\ds -> False), (\ds -> b)] ()))
            (lessThanInteger z 100))
         (lessThanInteger y 100))
      (lessThanInteger x 100))

Pattern 6 (nested builtinIf) — proper short-circuiting:

(\x y z ->
    case (lessThanInteger x 100)
      [(\ds -> error)
      ,(\ds -> case (lessThanInteger y 100)      -- only if x < 100
                 [(\ds -> error)
                 ,(\ds -> case (lessThanInteger z 100)  -- only if y < 100
                            [(\ds -> error)
                            ,(\ds -> ())]
                            ())]
                 ())]
      ())

…udget

Compare 4 patterns for chaining boolean conditions in Plutus Tx:
1. Standard && (lazy, delay/force)
2. builtinAnd (lambda/unit, Philip DiSarro's pattern from PR #7562)
3. Multi-way if (negated guards)
4. Direct BI.ifThenElse chain (manual lambda/unit)

Each pattern tested with 3 scenarios: AllTrue, EarlyFail, LateFail.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

Execution Budget Golden Diff

7d37742 (master) vs b17516d

output

This comment will get updated when changes are made.

@Unisay Unisay self-assigned this Feb 10, 2026
Extract patterns 2 and 4 into Budget.BuiltinAndLib with the exact GHC
flags used in PR #7562's ValidatorOptimized.hs:
- All -fno-* optimisation flags
- conservative-optimisation plugin option
- INLINE pragmas (not INLINEABLE)

Results unchanged: builtinAnd still doesn't short-circuit (839,970 CPU
in all scenarios) because Bool arguments are evaluated eagerly before
the function body runs, regardless of INLINE/flags.
… experiment

Add two more patterns to the && vs builtinAnd comparison:

Pattern 5 (andBuiltinAndIfError): Philip's exact claimed usage -
  if (builtinAnd a $ builtinAnd b c) then ok else error
  Result: 919,970 CPU, no short-circuiting (all conditions evaluated)

Pattern 6 (andBuiltinIfNest): Nested builtinIf with conditions in lambda bodies
  (Philip's actual validator pattern from PR #7562)
  Result: 727,970 CPU AllTrue, with proper short-circuiting
@Unisay Unisay added the No Changelog Required Add this to skip the Changelog Check label Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Do not merge No Changelog Required Add this to skip the Changelog Check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant