Skip to content

fix: for-loop init and slice() miscompiles, and other compiler findings from the 0.14 audit - #459

Closed
mr-zwets wants to merge 5 commits into
nextfrom
fix/compiler-audit-findings
Closed

mr-zwets wants to merge 5 commits into
nextfrom
fix/compiler-audit-findings

Conversation

@mr-zwets

Copy link
Copy Markdown
Member

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

Fixes the remaining compiler findings from the 0.14 pre-release audit, apart from the time-unit items in #455. Two of them silently compiled to the wrong value.

Changes

  • for init reassigning a variable: int i = 7; for (i = 0; i < 3; i++) {} left i at 7 after the loop, because the init pushed a new stack slot that the loop's cleanup dropped. The init now replaces the value in place, in contracts and global functions.
  • slice() with one variable in both arguments: the final use was recorded in the order start, end, but the code evaluates end first. The variable was rolled before its last read, so b.slice(v, v + 1) either failed to compile or, after v = v + 1, computed slice(0, 2). The symbol table now uses the code generation order.
  • Stale stack slots: after a reassignment outside a loop or branch, the old slot is made anonymous, so a mix-up like the ones above is a compile error rather than a stale read. console.log after a variable's final use now shows its current value. No bytecode changes.
  • LockingBytecodeNullData: a runtime chunk over 255 bytes lost its size's high byte (6a 4c 2c … for 300 bytes). The high byte is now checked to be zero, so such chunks make the contract fail (2 more bytes for runtime-sized chunks).
  • Overlapping scope cleanup tags: when the optimiser merged the cleanups of nested scopes (OP_2DROP OP_DROP), the BitAuth script showed an opcode twice, and debug()/send() rejected valid spends. The tags are now merged, and the rendering never repeats an opcode, which also covers existing artifacts.
  • Compiler options: an option passed as undefined disabled its check, and unknown keys (and files) were stored in the artifact. Options now fall back to their defaults, and only known options are stored.
  • Tuple destructuring: bytes a, bytes b = a.split(1); crashed with an internal error; it is now an UndefinedReferenceError.
  • Docs and release notes.

Tests

  • cashc: contracts executed on the BCH VM for the for init (contract and global function), slice() and NullData chunk sizes. New for_loop_reassign_init.cash and slice_same_variable.cash fixtures. A test that no variable is read after its final use, across all valid contract files. Compiler option tests and a tuple error fixture.
  • utils: a nested-scope BitAuth fixture that must reassemble to the bytecode, and old overlapping tags rendering each opcode once.
  • SDK: console.log of a reassigned variable after its final use.

Each of these tests fails on the current next. Recompiling every contract in the repo only changes locking_bytecode_nulldata_chunks and the two new fixtures.

Merges cleanly with #457 and #458; with #455 and the SDK findings PR only the release notes need their lines combined.

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

🤖 Generated with Claude Code

rkalis and others added 5 commits September 25, 2026 16:47
- Fix LockingBytecodeNullData push opcodes for empty chunks and chunks of
  128-255 bytes, which did not match the SDK's OP_RETURN encoding. Literal
  chunks are now pushed together with their push opcode, and literal chunks
  larger than 255 bytes are a compile error
- Fix tuple reassignment bypassing bytes length narrowing, e.g.
  `x, bytes rest = y.split(5);` after `require(x.length == 20);`
- Make hex literals with an odd number of digits a compile error
- Remove the early return TODO in EnsureFinalRequireTraversal (tracked in #416 and #424)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odes

String literal chunks (e.g. `bytes('memo')`) are pushed together with their
push opcode like hex literals, and chunks with a known length (e.g. bytes20)
are preceded by a constant push opcode. The push opcode is only computed at
runtime for chunks with an unknown length. Chunks whose size is known to be
larger than 255 bytes are a compile error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <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 28, 2026 10:11pm UTC

Request Review

@mr-zwets

Copy link
Copy Markdown
Member Author

On behalf of Mathieu G. (mr-zwets), written by Claude Opus 5.5. Opened from the wrong branch by mistake (the name collided with the branch of #452), please ignore; replaced by a new PR.

This branch was successfully deployed

1 active deployment
Preview — 768508da Deployed Sep 25, 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