Skip to content

fix(Compiler/Expr): replace caseInteger list-indexing fallback with equalsInteger chain#7693

Merged
Unisay merged 3 commits intomasterfrom
yura/fix-caseInteger-sop-equalsInteger
Apr 2, 2026
Merged

fix(Compiler/Expr): replace caseInteger list-indexing fallback with equalsInteger chain#7693
Unisay merged 3 commits intomasterfrom
yura/fix-caseInteger-sop-equalsInteger

Conversation

@Unisay
Copy link
Copy Markdown
Contributor

@Unisay Unisay commented Mar 26, 2026

Summary

Closes IntersectMBO/plutus-private#2135.
Fixes the execution cost regression documented in #7691.

When compiling caseInteger in the default SumsOfProducts mode, the fallback used PlutusTx.List.!!, which built a linked list at runtime and indexed into it with a Z-combinator. This caused a 3-5x cost increase for unsafeFromBuiltinData on multi-constructor types.

This PR replaces the fallback with a lazy equalsInteger/ifThenElse chain in PIR.
Uses the (all dead. resTy) / (/\dead -> branch) encoding to avoid evaluating non-matching branches. No runtime allocation, no Y-combinator, no linked list.

How it works

The non-BuiltinCasing branch in Compiler/Expr.hs now generates PIR equivalent to:

ifThenElse {all dead. resTy} (equalsInteger scrut 0)
  (/\dead -> b0)
  (/\dead -> ifThenElse {all dead. resTy} (equalsInteger scrut 1)
    (/\dead -> b1)
    (/\dead -> b2)
    {resTy})
  {resTy}

Which erases to delay/force in UPLC, matching the pre-regression pattern.
The BuiltinCasing branch is unchanged (native case on the integer).

Budget comparison

data ABC = A Integer | B Integer | C Integer, same source compiled in both modes:

SumsOfProducts (default, the regressed mode)

Test Metric Baseline (if False) This PR Regressed (master)
decode A (index 0) CPU 1,244,851 1,292,851 2,214,190
Memory 4,662 4,962 9,964
decode C (index 2) CPU 2,050,823 1,746,441 4,054,578
Memory 7,468 6,366 17,974

Baseline is generated by bypassing caseInteger (if False in unsafeFromDataClause),
which falls back to the old tuple-pattern-matching TH code. The small remaining difference
(~4% for index 0) is from casePair continuation vs fst/snd let-bindings in the
pair extraction, which is inherent to the caseInteger TH code path.

BuiltinCasing (unaffected by this change)

Test Metric Budget
decode A (index 0) CPU 512,582
Memory 2,596
decode C (index 2) CPU 677,790
Memory 2,998

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 26, 2026

Execution Budget Golden Diff

0c08eee (master) vs 7638be5

output

This comment will get updated when changes are made.

@Unisay Unisay self-assigned this Mar 26, 2026
@Unisay Unisay marked this pull request as draft March 26, 2026 13:34
@Unisay Unisay force-pushed the yura/fix-caseInteger-sop-equalsInteger branch 2 times, most recently from 43759bb to f2f4424 Compare March 26, 2026 14:29
@Unisay Unisay marked this pull request as ready for review March 26, 2026 14:33
@Unisay Unisay requested a review from zliu41 March 26, 2026 14:37
@Unisay Unisay requested a review from SeungheonOh March 26, 2026 14:37
@Unisay Unisay force-pushed the yura/fix-caseInteger-sop-equalsInteger branch from f2f4424 to 8a53491 Compare March 26, 2026 18:41
Copy link
Copy Markdown
Member

@zliu41 zliu41 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be backported to the version the Hydra team is using.

Copy link
Copy Markdown
Collaborator

@SeungheonOh SeungheonOh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me

@Unisay Unisay force-pushed the yura/fix-caseInteger-sop-equalsInteger branch 4 times, most recently from 759d2f5 to 3f451f2 Compare March 27, 2026 16:05
@Unisay Unisay enabled auto-merge (squash) March 27, 2026 16:08
@Unisay Unisay force-pushed the yura/fix-caseInteger-sop-equalsInteger branch 2 times, most recently from 5f9e28d to 31f123c Compare March 31, 2026 14:00
@Unisay
Copy link
Copy Markdown
Contributor Author

Unisay commented Apr 1, 2026

Update: fixed Marlowe Semantics test failures

Running the Marlowe benchmark suite (cabal run plutus-benchmark:marlowe-validators) revealed that 53 out of 206 Semantics test cases were failing with a bare error from the CEK machine. The RolePayout tests (simpler validator) passed fine.

Root cause

The bug turned out to be in compileHaskellList, not in the equalsInteger chain generation itself.

compileHaskellList handles two GHC Core representations of list literals:

  1. Build form: build @ty (\con nil -> con e1 (con e2 ...)) — used when GHC can apply foldr/build fusion
  2. Cons chain: (:) @ty e1 ((:) @ty e2 (... ([] @ty))) — used when GHC inlines build

The old code had a pattern for case 2 that matched (:) @ty e _, silently discarding the tail. This worked fine when the tail was [] (single-element lists), but for types with many constructors — like Marlowe's Value (12 constructors) and Observation (11) — GHC chose the cons-chain form, and the pattern returned only the first branch. The resulting equalsInteger chain had 1 branch instead of 11/12, so any constructor with index > 0 fell through to error.

The fix adds a recursive consumeCons that walks the full (:) spine.

Why this wasn't caught earlier

  • The existing IsData tests use types with ≤ 3 constructors, which GHC represents in build form
  • Whether GHC uses build vs (:) depends on the surrounding code context and optimizer decisions — hard to reproduce in isolation
  • The bug was latent on master (the cons pattern existed before this PR), but only manifested now because our equalsInteger chain calls compileHaskellList on the SoP/Scott path, which master never did

Test results

  • 838/838 plugin tests pass
  • 2697/2697 plutus-core tests pass
  • 0/206 Marlowe Semantics failures (was 53)
  • 0/100 Marlowe RolePayout failures
  • Marlowe SoP flat file is ~0.7% smaller (57266 vs 57663 bytes on master)

Unisay added 3 commits April 2, 2026 11:43
Add golden budget tests for unsafeFromBuiltinData compilation in both
SumsOfProducts and BuiltinCasing modes.  Covers single-constructor,
pair, mixed-arity, and three-constructor types with PIR, UPLC, and
evaluation golden files for GHC 9.6 and 9.12.
…ger chain

Replace the PlutusTx.List.!! fallback for caseInteger in SoP/Scott mode
with an inline equalsInteger/ifThenElse chain that avoids runtime list
construction and indexing.

Also fix a latent bug in compileHaskellList: the old single-element
pattern silently discarded the list tail, so when GHC inlined `build`
into an explicit (:)-chain (as it does for recursive types with many
constructors like Marlowe's Value and Observation), only the first
branch was compiled. The fix adds a recursive consumeCons that walks
the full (:) spine.
Regenerate Marlowe flat files and golden budget TSVs to reflect the
equalsInteger chain compilation.  Semantics SoP validator shrinks from
57663 to 57266 bytes (~0.7%).
@Unisay Unisay force-pushed the yura/fix-caseInteger-sop-equalsInteger branch from 3609454 to 7638be5 Compare April 2, 2026 09:43
@Unisay Unisay merged commit 09680d7 into master Apr 2, 2026
9 of 10 checks passed
@Unisay Unisay deleted the yura/fix-caseInteger-sop-equalsInteger branch April 2, 2026 11:54
Unisay added a commit that referenced this pull request Apr 7, 2026
Replace runtime list-indexing via PlutusTx.List.!! with a compile-time
equalsInteger/ifThenElse chain for the SumsOfProducts caseInteger
fallback. This fixes a 3-5x cost regression in unsafeFromBuiltinData
for multi-constructor types (reported by Hydra team).

Also fixes a latent bug in compileHaskellList that silently dropped
list elements when GHC used (:)-chain representation instead of
build/foldr form.
@Unisay Unisay mentioned this pull request Apr 7, 2026
Unisay added a commit that referenced this pull request Apr 8, 2026
* test(IsData/Budget): add SoP and BuiltinCasing budget tests

Add golden budget tests for unsafeFromBuiltinData compilation in both
SumsOfProducts and BuiltinCasing modes.  Covers single-constructor,
pair, mixed-arity, and three-constructor types with PIR, UPLC, and
evaluation golden files for GHC 9.6 and 9.12.

* fix(Compiler/Expr): backport caseInteger SoP fallback fix from #7693

Replace runtime list-indexing via PlutusTx.List.!! with a compile-time
equalsInteger/ifThenElse chain for the SumsOfProducts caseInteger
fallback. This fixes a 3-5x cost regression in unsafeFromBuiltinData
for multi-constructor types (reported by Hydra team).

Also fixes a latent bug in compileHaskellList that silently dropped
list elements when GHC used (:)-chain representation instead of
build/foldr form.

* chore: regenerate Marlowe benchmark baselines

Updated Marlowe golden TSV files to reflect improved budgets from the
caseInteger SoP fix. CPU: -9..12%, Memory: -16..20%.

* Release 1.56.0.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants