Conversation
Replace the rule families that only differed in a stack depth or push with matcher functions: an in-place update of a variable at any depth with any pure expression (including copies of other variables), OP_NIP after any push, and a dropped pick at any depth. Optimisation rules can now be either [pattern, replacement] pairs or matchers that return their match as parts, so opcodes a match keeps also keep their source locations and positions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture from #463 predates the OP_2 OP_PICK OP_2 OP_PICK => OP_3DUP OP_DROP optimisation from #458, which makes it fail on next. The regenerated fixture also picks up the per-opcode source location of the in-place loop counter update, and the overlapping tag test now uses the shifted indices of the same scope cleanups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## next #468 +/- ##
=======================================
Coverage ? 90.69%
=======================================
Files ? 61
Lines ? 5158
Branches ? 979
=======================================
Hits ? 4678
Misses ? 367
Partials ? 113 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #458, which left a note in
optimisations.tsthat its hardcoded rule families "should become dynamic". This replaces the families that only differ in a stack depth or push with matcher functions, and generalises the in-place update to any expression.Opened as a draft because of the two open decisions below.
Open decisions
1. For-loop update annotation in the BitAuth output. Opcodes that an optimisation keeps now keep their own source locations (see Optimiser engine below). For
i = i + 1loop updates the assignment's location was on the removedOP_NIP, so the annotation now reads>>> for-loop update (i + 1)instead of(i = i + 1).i++,i += 1andi -= 3still show the full statement, and the multi-line update in theNestedForWithDoWhilefixture now shows(i + 1)instead of an empty().x = x + 1is the most common form in the docs and tests (16 of 22 loops).Restoring the old text means handing the location of a removed part to the opcode before it. But the removed drop can also be a scope or function cleanup: in
compound_assign, that would give the finalrequirecheck the whole function body's location and display it at the closing}. Options:(i + 1).2.
<pick n> OP_ROT OP_SWAP OP_DROP→OP_SWAPnever fires. The earlierOP_SWAP OP_DROP→OP_NIPrule always rewrites its tail first within the same pass, so this family (and the missingOP_15rule mentioned in #458) has never had an effect. This PR generalises it as-is. Options: retarget it to the reachable<pick n> OP_ROT OP_NIP→OP_SWAP(same semantics, changes no current contract), or delete it.Changes
Rules can be matcher functions (
packages/utils/src/optimisations.ts)The rule list holds
[pattern, replacement]pairs and matcher functions, and went from 159 static rules to 94 static rules plus 3 matchers:updateInPlace:<pick n> <expression> <drop n+1>→<roll n> <expression>OP_1ADD/OP_1SUBrules from #458,OP_DUP OP_1ADD OP_NIP,OP_DUP OP_1SUB OP_NIPmoveDropBeforePush:<push> OP_NIP→OP_DROP <push>OP_n OP_NIPrulesOP_1NEGATEdropPickedCopy:<pick n> OP_ROT OP_SWAP OP_DROP→OP_SWAPOP_nrules<pick n>,<roll n>and<drop n>are the forms left after the hardcoded stack op rules, e.g.OP_OVER,OP_ROTandOP_NIPfor small depths.The expression of an in-place update can be any sequence of opcodes that replace the items they pop with results that only depend on those items and the transaction, as long as it never consumes items below its own. A table of about 75 opcodes and their stack effects defines these (arithmetic, comparisons, hashes, splice, bitwise and shift ops, introspection, stack shuffles). The expression can also copy other variables. Copies of items below the updated variable are rewritten one shallower, because the roll removes it from the stack. This optimises e.g.
total = total + tx.outputs[i].valueinside a branch or loop.It does not match when the expression:
x = x * x), since the expression might already have consumed its copyOP_VERIFY,OP_IF,OP_ROLL, alt stack ops,OP_DEPTHorOP_INVOKEOptimiser engine (
packages/utils/src/script.ts)[pick → roll], one part per expression opcode, and[drop → nothing], so the opcodes it keeps keep their source locations and positions. Log data still adjusts against the whole match, because the roll changes the stack below the expression.Merging one location over a whole long match broke debugging: for
x -= 3; require(x == 12), the finalrequire's ip ended up one opcode early, so the SDK could no longer attribute the failedrequire. Every opcode also got the whole function body's location from the cleanupOP_NIP.tuple_reassignmentthat attributed an unrelatedOP_ROTto an inlinedswapthat was optimised away.Fixtures
OverlappingScopeCleanupBitAuth fixture from fix: fix failing debug() when scope cleanup tags overlapped #463. It predates theOP_2 OP_PICK OP_2 OP_PICK→OP_3DUP OP_DROPrule from feat: add more peephole optimisations #458, and currently fails CI onnext. The overlapping-tag test inbitauth-script.test.tsnow uses the shifted indices of the same scope cleanups.Results
6 contracts get smaller, by 14 bytes in total. The
cashctest contracts go from 3,157 to 3,143 bytes.complex_loop(outputSum = outputSum + tx.outputs[i].valuein a loop branch)for_loop_stack_itemscompound_assignglobal_function_in_control_flowincrement_decrementtuple_modifiers13 more contracts only change in debug information: the per-opcode locations of in-place updates.
Verification
yarn test(957 passed),yarn lintandyarn spellcheckpass.packages/utils/test/optimisations.test.tscovers each matcher, including cases that must not match. It checks VM equivalence on libauth's BCH 2026 VM with a stack of distinct numbers, so reading the wrong item changes the result. It also covers the debug information: kept locations, the finalrequireip, and tag ends.optimiseBytecodewith 6 random stacks per script (the harness is not part of this PR):OP_NOT OP_IFandOP_NOT OP_NOT OP_VERIFYrelaxations.OP_ACTIVEBYTECODE,OP_UTXOBYTECODEandOP_OUTPOINTTXHASHwere left out of the fuzz, because in libauth's test programs their values depend on the script bytes that the optimisation changes.Possible follow-ups
OP_DUP OP_ROT OP_DROP,OP_OVER OP_ROT OP_DROPandOP_2 OP_PICK OP_ROT OP_DROPlook like another depth family (<pick n> OP_ROT OP_DROPequalsOP_NIP <pick n-1>for n ≥ 2, one opcode shorter). Not measured.🤖 Generated with Claude Code