Skip to content

Spec.Data.Value.Budget — non-builtin valueOf evidence (NOT FOR MERGE)#7798

Draft
Unisay wants to merge 2 commits into
masterfrom
yura/issue-2242-valueof-evidence
Draft

Spec.Data.Value.Budget — non-builtin valueOf evidence (NOT FOR MERGE)#7798
Unisay wants to merge 2 commits into
masterfrom
yura/issue-2242-valueof-evidence

Conversation

@Unisay

@Unisay Unisay commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Not for merge. The purpose is to compare three lookup paths so that V3 guidance on which to prefer can be backed by concrete numbers rather than estimates:

  • BuiltinValue path (unsafeDataAsValue + lookupCoin): the new on-chain builtins introduced for Data-backed Values.
  • Typed valueOf path: plutus-ledger-api's PlutusLedgerApi.V1.Data.Value.valueOf (after the rewrite in #7797), entered through unsafeFromBuiltinData.
  • Raw BuiltinData walker: a hand-rolled valueOfRaw :: BuiltinByteString -> BuiltinByteString -> BuiltinData -> Integer that walks the Data structure directly via unsafeDataAsMap / unsafeDataAsB / unsafeDataAsI. This is the pre-BuiltinValue baseline that production Plinth code (e.g. Djed) and other compilers (Aiken, Pebble, Scalus) effectively produce.

Spec.Data.Value.Budget runs all three across four shapes (S1 / S3 / S8 / S100) at four lookup positions (ada / middle / last / miss). It also captures unsafeDataAsValue standalone per shape, so the decode overhead can be subtracted from the BuiltinValue column. 46 bundles × 3 files = 138 goldens.

This branch stacks on top of #7797.

For plutus-private#2242.

Lookup matrix, GHC 9.6, plutus-ledger-api 1.65.0.0

CPU in thousands. Columns:

  • BuiltinValue = unsafeDataAsValue + lookupCoin.
  • typed valueOf = unsafeFromBuiltinData + PlutusLedgerApi.V1.Data.Value.valueOf (the rewrite from #7797).
  • raw walker = hand-rolled valueOfRaw :: BuiltinByteString -> BuiltinByteString -> BuiltinData -> Integer over unsafeDataAsMap / unsafeDataAsB / unsafeDataAsI; the pre-BuiltinValue baseline.

Shape legend: S1 = ada only (1 token); S3 = ada plus 2 single-token policies (3 tokens); S8 = ada plus 7 single-token policies (8 tokens); S100 = ada plus 10 policies of 10 tokens each (101 tokens). Lookup position is the row index of the matching (currency, token) inside the outer/inner maps: ada = first, middle = around the centre, last = final entry, miss = absent key.

shape position BuiltinValue typed valueOf raw walker
S1 ada 896 3 772 3 468
S1 miss 896 2 500 2 212
S3 ada 1 673 3 772 3 468
S3 middle 1 673 5 010 4 706
S3 last 1 673 6 247 5 943
S3 miss 1 673 4 974 4 686
S8 ada 3 611 3 772 3 468
S8 middle 3 611 8 721 8 417
S8 last 3 611 12 432 12 128
S8 miss 3 611 11 159 10 871
S100 ada 22 108 3 772 3 468
S100 middle 22 108 15 162 14 410
S100 last 22 108 27 852 26 540
S100 miss 22 108 14 870 14 582

Note on the S100 last vs miss asymmetry for typed and raw: last walks the outer fully (11 entries to reach cs10) and then the inner of cs10 (10 entries to reach tn10), while miss only walks the outer (11 entries, no inner decoding and walk), so last costs roughly 21 step-equivalents and miss 11 — a ratio matching the observed CPU split. The BuiltinValue path is position-independent: unsafeDataAsValue decodes the whole value up front.

Standalone unsafeDataAsValue:

shape CPU Mem
S1 577 0.8
S3 1 344 0.8
S8 3 264 1.1
S100 21 733 3.2

Typed valueOf vs raw walker

PlutusLedgerApi.V1.Data.Value is Data-backed: Value is a newtype over Data.AssocMap.Map, which is itself a newtype over BuiltinList (BuiltinPair BuiltinData BuiltinData). So unsafeFromBuiltinData :: BuiltinData -> Value does not decode the structure; it coerces through the newtype layers to the underlying BuiltinList. The Plinth plugin strips this to zero-cost coercions.

The typed valueOf and the raw walker traverse identically once compiled, so the gap between them is small: ~300 K CPU flat on most rows, growing modestly on the larger linear scans (S100 middle / last). The single-percent overhead reflects only the constant newtype-unwrap on the caller boundary, not any per-element work.

The typed valueOf API is not appreciably slower than a raw BuiltinData walker. Callers who already hold a BuiltinData and prefer the raw signature can write one inline, but there's no order-of-magnitude reason to expose valueOfRaw publicly.

V3 lookup guidance

Headline: for the values seen on mainnet today (S1–S8, ≤8 assets), the BuiltinValue path is the cheaper choice. It wins at every position by 2×–4× on S1, 1.5×–3× on S3, and stays within 5% of the typed/raw walkers even on S8 ada (the crossover point). Since the vast majority of on-chain values fall in this range, this is the default recommendation.

More detail by size and access pattern:

  • Up to ~8 currencies (typical mainnet values): use the BuiltinValue path. The fixed unsafeDataAsValue overhead is the whole cost, and it stays small enough that walking (typed or raw) can't beat it.
  • Large values, first-position lookup: use the typed valueOf or a raw walker. The short-circuit beats the linear unsafeDataAsValue decode on any size, and the gap widens (S100 ada: ~5.9× cheaper than the BuiltinValue path).
  • Large values, last-position or full scan: use the BuiltinValue path once you cross ~100 entries. The decode cost is paid once and amortises across the scan.
  • One unwrap, many lookups in the same validator: BuiltinValue wins everywhere except trivially small values. The decode is amortised; subsequent lookupCoin calls are cheap.

The "non-builtin is 2.5–12× slower" claim from #7738 no longer applies for first-position lookups on large values, but the headline for typical-size mainnet values (S1–S8) is now firmly in favour of the BuiltinValue path.

@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Execution Budget Golden Diff

b4d1df4 (master) vs 79a5fde

output

This comment will get updated when changes are made.

@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.6.3

🚀 View preview at
https://IntersectMBO.github.io/plutus/pr-preview/docs/pr-7798/

Built to branch gh-pages at 2026-05-27 12:10 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

This comment was marked as low quality.

@Unisay Unisay force-pushed the yura/issue-2242-optimal-valueof branch from 2d0ecc7 to ae21ea2 Compare May 27, 2026 13:07
@Unisay Unisay force-pushed the yura/issue-2242-valueof-evidence branch from 152f7a0 to 2e8512e Compare May 27, 2026 13:07
@Unisay Unisay force-pushed the yura/issue-2242-optimal-valueof branch from ae21ea2 to 0ac70fa Compare May 27, 2026 13:22
@Unisay Unisay force-pushed the yura/issue-2242-valueof-evidence branch 2 times, most recently from 58a65a7 to 52a8bb6 Compare May 27, 2026 14:22
@Unisay Unisay force-pushed the yura/issue-2242-optimal-valueof branch from 0ac70fa to cd7c252 Compare May 28, 2026 11:28
@Unisay Unisay force-pushed the yura/issue-2242-valueof-evidence branch from 52a8bb6 to ad744d7 Compare May 28, 2026 11:29
@Unisay Unisay force-pushed the yura/issue-2242-optimal-valueof branch from cd7c252 to 8271c9e Compare May 28, 2026 11:43
@Unisay Unisay force-pushed the yura/issue-2242-valueof-evidence branch from ad744d7 to 804a090 Compare May 28, 2026 11:43
@Unisay Unisay force-pushed the yura/issue-2242-optimal-valueof branch from 8271c9e to 613670c Compare May 28, 2026 11:50
@Unisay Unisay force-pushed the yura/issue-2242-valueof-evidence branch from 804a090 to ea05f7b Compare May 28, 2026 11:50
Rewrites `PlutusLedgerApi.V1.Data.Value.valueOf` so the non-builtin lookup
path walks the underlying `BuiltinList` directly via `unsafeDataAsMap` /
`unsafeDataAsB` / `unsafeDataAsI`, compares keys with `equalsByteString`,
and short-circuits on the first match. No `Maybe` is materialised: the
"absent" answer is `0`, returned in-place by the `nilCase` of each
traversal. Avoids `withCurrencySymbol`'s continuation + `Map.lookup`'s
`Maybe`-wrapping, and bypasses the `ToData k`/`UnsafeFromData a` dictionary
work that `AssocMap.lookup` does per element. Semantics preserved.

Adds `Spec.Data.Value.test_valueOf`: a QuickCheck property that compiles
`valueOf` via TH, evaluates it on the CEK machine, and compares the result
against the host-Haskell `valueOf` for the same inputs. Differential test
against the Plinth compiler — any divergence is a compilation bug, not a
semantics bug.

Budget evidence (lookup matrix, `unsafeDataAsValue` baseline) lives on the
companion experimental branch `yura/issue-2242-valueof-evidence`, kept out
of this PR to avoid carrying ~96 golden files that would only ever regenerate
on upstream plugin/cost-model changes.

For IntersectMBO/plutus-private#2242.
@Unisay Unisay force-pushed the yura/issue-2242-optimal-valueof branch from 613670c to b4d1df4 Compare May 29, 2026 10:35
Adds `Spec.Data.Value.Budget` with the lookup matrix for `valueOf` across
S1 / S3 / S8 / S100 value shapes at ada / middle / last / miss positions,
plus the standalone `unsafeDataAsValue` baseline per shape. Generated 96
golden files (eval / pir / uplc per bundle, 32 bundles total) capture CPU
and Memory cost evidence.

Companion to the merge PR for `valueOf` rewrite. This branch is kept
separate so the goldens — which only regenerate on upstream plugin or
cost-model changes, not on actual regressions — don't bloat the main
PR's diff or CI.

For IntersectMBO/plutus-private#2242.
@Unisay Unisay force-pushed the yura/issue-2242-valueof-evidence branch from ea05f7b to 79a5fde Compare May 29, 2026 10:36
Base automatically changed from yura/issue-2242-optimal-valueof to master June 2, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants