Skip to content

feat: add more peephole optimisations - #458

Merged
rkalis merged 3 commits into
nextfrom
feat/more-peephole-optimisations
Sep 29, 2026
Merged

rkalis merged 3 commits into
nextfrom
feat/more-peephole-optimisations

Conversation

@mr-zwets

Copy link
Copy Markdown
Member

Opened on behalf of Mathieu G. (mr-zwets), written by Claude Opus 5.5.

Follow-up to #451, with peephole optimisations found in the pre-release audit of 0.14.0. The cashc test contracts get 59 bytes smaller in total (3,067 to 3,008), and randomly generated loop-heavy contracts about 3.6%.

Changes

New rules, appended to optimisations.ts in four groups:

  • Increments and decrements of a deeper variable (e.g. x = x + 1 inside a branch): OP_OVER OP_1ADD OP_ROT OP_DROP → OP_SWAP OP_1ADD, and OP_n OP_PICK OP_1ADD OP_(n+1) OP_ROLL OP_DROP → OP_n OP_ROLL OP_1ADD for n = 2 to 15, with the same rules for OP_1SUB. These come before the drop rules below, which would otherwise match the end of these patterns first.
  • Drops of deeper items (scope cleanup): OP_ROT OP_DROP OP_NIP → OP_NIP OP_NIP, OP_3 OP_ROLL OP_DROP OP_ROT → OP_2SWAP OP_NIP, then OP_ROT OP_ROT OP_DROP → OP_NIP OP_SWAP, and OP_3 OP_ROLL OP_DROP OP_NIP OP_NIP → OP_NIP OP_NIP OP_NIP.
  • Shorter stack shuffles: OP_SWAP OP_OVER → OP_TUCK, OP_2 OP_PICK OP_2 OP_PICK → OP_3DUP OP_DROP, OP_2 OP_PICK OP_OVER → OP_3DUP OP_NIP (with a note that OP_3DUP copies one more item, which raises the operation cost when that item is large), OP_2 OP_PICK OP_NIP → OP_DROP OP_OVER, OP_TOALTSTACK OP_ROT OP_ROT OP_FROMALTSTACK → OP_2SWAP OP_ROT, and OP_DUP OP_ROT OP_NUMEQUALVERIFY → OP_TUCK OP_NUMEQUALVERIFY (the operands end up swapped, so only for this order-independent comparison).
  • Extraneous OP_DUP: OP_DUP OP_SIZE OP_NIP → OP_SIZE.

Not included: OP_DUP OP_VERIFY OP_DROP → OP_VERIFY is VM-equivalent, but its OP_DROP is a scope cleanup. Merging that location into the OP_VERIFY makes the SDK report a failing require(x) as the whole enclosing block, and moves a later console.log before the check.

Release notes updated.

Tests

  • Every new rule gives the same final stack, alt stack and success or failure on libauth's BCH 2026 VM as its pattern, on structured and random stacks at every depth from 0 to 18 (including stack underflow, non-minimal numbers, negative zero, empty and large items).
  • 40,000 random sequences built from rule fragments, run through the full optimiseBytecode on 6 stacks each: no difference apart from stack-underflow cases and the documented OP_NOT and OP_CAT OP_DROP relaxations of existing rules. Every new rule shortens the script, so no new optimisation cycles are possible.
  • No new overlapping source tags, and the formatted BitAuth script still compiles to the contract's bytecode for all cashc test contracts and for 4,500 generated contracts.
  • Generation and BitAuth fixtures updated for the new bytecode; bytecode comments stay with the statements they compile.
  • New deep_increment_decrement.cash fixture for decrements and deeper variables, which no existing test contract reached: it exercises the OP_1SUB rules at depths 1, 2 and 5, the OP_1ADD rule at depth 4, and both OP_2SWAP rules (90 to 74 bytes).

yarn build, yarn test, yarn lint and yarn spellcheck pass.

🤖 Generated with Claude Code

Add optimisations for increments and decrements of a variable deeper in
the stack (e.g. x = x + 1 inside a branch), scope cleanup of deeper
items, stack shuffles that OP_TUCK, OP_3DUP or OP_2SWAP express shorter,
and OP_SIZE on a duplicated value. The increment and decrement rules come
before the deeper drop rules, which would otherwise match the end of
their patterns first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cashscript Ready Ready Preview Sep 29, 2026 8:59am UTC

Request Review

@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.33%. Comparing base (2a2e050) to head (cddbb9b).
⚠️ Report is 2 commits behind head on next.

Additional details and impacted files
@@            Coverage Diff             @@
##             next     #458      +/-   ##
==========================================
- Coverage   89.34%   89.33%   -0.01%     
==========================================
  Files          61       61              
  Lines        5048     5053       +5     
  Branches      942      946       +4     
==========================================
+ Hits         4510     4514       +4     
  Misses        413      413              
- Partials      125      126       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mr-zwets

Copy link
Copy Markdown
Member Author

On behalf of Mathieu G. (mr-zwets), written by Claude Opus 5.5.

A note for later, not for this PR: optimisations.ts now has four families that differ only in n, which together make up 57 of its 159 rules:

Family Rules
OP_n OP_NIP → OP_DROP OP_n 17
OP_n OP_PICK OP_ROT OP_SWAP OP_DROP → OP_SWAP 14
OP_n OP_PICK OP_1ADD OP_(n+1) OP_ROLL OP_DROP → OP_n OP_ROLL OP_1ADD (this PR) 13
The same with OP_1SUB (this PR) 13

This PR writes its families out like the existing ones, so the file stays one consistent list. A single regex can't express OP_n … OP_(n+1), so the alternative is generating each family with a small range helper, which would turn those 57 rules into about four lines. It would be a pure refactor: every compiled artifact stays byte-identical, which the generation fixtures check.

A generator would also rule out gaps, and there is already one: the OP_n OP_PICK OP_ROT OP_SWAP OP_DROP family goes from 2 to 14 and then 16, without OP_15. That looks like an oversight, a missed optimisation rather than a bug. OP_15 OP_PICK OP_ROT OP_SWAP OP_DROP → OP_SWAP behaves like the rest of the family on the BCH 2026 VM (the same final stacks wherever the original succeeds).

Happy to add the OP_15 rule here, or do both in a follow-up PR, whichever you prefer.

Added a note about the need for dynamic optimisations in future versions.
Consolidate multiple optimization entries and fix various bugs related to literals and bytecode.
@rkalis
rkalis merged commit 52c2b43 into next Sep 29, 2026
5 checks passed

This branch was successfully deployed

1 active deployment
Preview — cddbb9bf Deployed Sep 29, 2026 by vercel[bot]
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.

2 participants