Repository navigation
Internal RISC-V assembler fails on 4/7 bundled examples: missing B-type branch range relaxation #1
Description
(Filed by claude code)
Summary
cellc … --target riscv64-elf fails for 4 of the 7 bundled examples because the internal assembler rejects conditional branches whose displacement exceeds the RV64 B-type 13-bit signed immediate range (±4 KB). The failure propagates through cargo test --test examples, so bundled_examples_compile_to_elf is currently red on main.
The bug is independent of --target-profile — the default (spora), explicit --target-profile spora, and --target-profile ckb all trigger the same overflow on any example whose code reaches the internal assembler. CKB profile appears to "work" for some examples only because its target-profile policy gate rejects them before codegen runs.
Reproduction
git checkout main
for f in examples/{token,nft,vesting,amm_pool,multisig,timelock,launch}.cell; do
cellc "$f" --target riscv64-elf 2>&1 | tail -1 | sed "s|^|$f :: |"
doneexamples/token.cell :: error: line 0: immediate '5060' does not fit 13-bit signed field
examples/nft.cell :: error: line 0: immediate '4888' does not fit 13-bit signed field
examples/vesting.cell :: error: line 0: immediate '5672' does not fit 13-bit signed field
examples/amm_pool.cell :: error: line 0: immediate '11748' does not fit 13-bit signed field
examples/multisig.cell :: (ok)
examples/timelock.cell :: (ok)
examples/launch.cell :: (ok)
Same results with explicit --target-profile spora. Under --target-profile ckb, token.cell still fails with the same 5060 overflow; the other examples are rejected by the CKB policy gate in src/lib.rs:465-605 before reaching codegen, which masks (but does not fix) the assembler bug.
cargo test --test examples -- bundled_examples_compile_to_elfthread 'bundled_examples_compile_to_elf' panicked at tests/examples.rs:293:29:
amm_pool.cell should compile to ELF:
immediate '11748' does not fit 13-bit signed field
test result: FAILED. 9 passed; 1 failed
Expected vs. actual
- Expected: every example in
BUNDLED_EXAMPLES(tests/examples.rs:4) compiles to ELF, matching the assertion attests/examples.rs:293. - Actual: four examples fail in
src/codegen/mod.rs::encode_signed_bitswith the 13-bit overflow error.--target riscv64-asmstill works for all seven — only the ELF path is broken.
Root cause
RV64 conditional branches (beq / bne / blt / bge / bltu / bgeu, including the beqz / bnez aliases) are B-type: 13-bit signed PC-relative immediate, i.e. ±4096 bytes. The internal assembler encodes them directly with no fallback:
// src/codegen/mod.rs:5274-5282
Instruction::Beqz { rs, label } => {
let target = parsed.symbol_address(label, layout)?;
out.extend_from_slice(
&encode_b_type(0x63, 0b000, *rs, 0, relative_offset(pc, target)?)?
.to_le_bytes(),
);
}
Instruction::Bnez { rs, label } => { /* identical pattern */ }// src/codegen/mod.rs:5534-5538
fn encode_b_type(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i64) -> Result<u32> {
// ...
let imm = encode_signed_bits(imm, 13)?;Because codegen emits validation logic inline — each 32-byte type_hash comparison expands into 32 lbu/lbu/sub/beqz/<5-line fail epilogue> blocks, and every size/bounds/witness-magic check gets its own inline fail stub — the distance from a conditional branch to a shared exit label (.Lentry_witness_fail_*, .Lentry_witness_done_*, etc.) grows quickly. In amm_pool.cell this distance reaches 11748 bytes, ~3× the B-type limit.
The standard fix is branch range relaxation: when a B-type target is out of range, rewrite the branch as an inverted short branch plus a J-type jump (which has 21-bit ±1 MB range):
beqz rs, far_label
becomes
bnez rs, .Lskip_N
j far_label
.Lskip_N:
GAS, LLVM MC, and Keystone all do this automatically. The internal assembler is currently the only path without it.
Secondary issues uncovered
(1) Error message is missing span information
encode_signed_bits constructs the error with Span::default():
// src/codegen/mod.rs:5577-5580
return Err(CompileError::new(
format!("immediate '{}' does not fit {}-bit signed field", value, bits),
crate::error::Span::default(),
));Result: every failure prints line 0:, regardless of which assembly line or which source position the branch originated from. The assembler has per-line information available during parsing (ParsedAssembly::from_lines); it should be threaded down into encode_instruction.
(2) CI evidently isn't running cargo test --test examples (or isn't gating on it)
A failing panic!-based test in the default test suite has landed on main (commit 10d5762 Initial CellScript standalone release). Either CI isn't running this target or the failure is being ignored. Regardless of the branch-relaxation fix, the test-gating policy needs a look.
Suggested fix
Two options, in increasing scope:
Option A — minimum viable: implement branch range relaxation
Modify encode_instruction in src/codegen/mod.rs:5274-5282 to detect out-of-range B-type branches and emit an inverted-branch-plus-jump pair. Because this changes instruction stream length, SectionLayout needs a fixup pass (conservative first pass for symbol addresses, exact second pass for emission). Standard two-pass assembler shape.
Estimated effort: ~1 day. Fixes all four failing examples. No semantic change to generated code.
Option B — recommended: share fail-handler routines (eliminates the long branches at source)
The generated assembly currently has 302 copies of the li a0, <err>; ld ra; ld fp; addi sp; ret fail epilogue across token.s alone. Collapsing these into ~8 shared handlers (one per distinct error code) and replacing fail sites with single-instruction j __fail_<code> brings every conditional branch target within a few hundred bytes. Also eliminates the equivalent issue in the 128 inlined byte-compare blocks by factoring them into a __verify_32byte_eq(ptr1, ptr2, err_code) helper.
Side effects:
- The branch-range bug disappears without needing relaxation.
- Assembly text shrinks by ~40% (observed on
token.s: 4505 → ~2600 lines). - +~2 cycles per fail path due to the extra
j(negligible vs. CKB/Spora cycle budgets).
Estimated effort: ~1 week. Recommended as the actual fix because it addresses the underlying codegen smell rather than papering over it in the assembler.
Either way
- Plumb real spans into
encode_signed_bits/encode_b_typeso future assembler errors point at the offending line. - Add a regression test that generates a >8 KB synthetic assembly fragment with a long-distance branch and asserts it assembles.
- Verify CI is actually running
cargo test --test examplesand failing the build onbundled_examples_compile_to_elfregressions.
Impact assessment
- User-visible: the documented primary deliverable (
cellc <input> --target riscv64-elffor on-chain deployment) fails silently — there is no warning in the README orcellc --helpthat four of the seven example programs can't actually be produced as ELF.--target riscv64-asmstill works, which can mask the problem in casual testing. - Trust: the project's own test suite is red on
main. Any downstream consumer runningcargo testsees the failure immediately. - Difficulty: low — this is a missing standard assembler feature, not an architectural issue.
Environment
- Branch:
mainat10d5762 Initial CellScript standalone release - Platform: Linux
- Toolchain: rustc (stable),
cargo run --bin cellc - No external RISC-V toolchain present (so the internal assembler path is exercised;
try_external_elf_toolchainatsrc/codegen/mod.rs:4713returnsNone).
Activity
a19q3 commentedon Apr 21, 2026
thanks a lot for filing it jan :)
the diagnosis looks right to me: this is a real missing branch-relaxation case in the internal assembler, and the current inline codegen style is making it much easier to hit.
in this case i will likely treat branch relaxation as the immediate fix, then look at deduplicating fail handlers / compare helpers as the more structural cleanup.
also agreed on plumbing real spans and adding a regression test for long-distance branches.
a19q3 commentedon Apr 21, 2026
okay, fixes are in place now.
i’ve decided to take a deeper dive into this issue.
also moving toward a proper machine-level CFG/layout pass so future backend work is less reactive and more principled.
feat(v0.15): implement scoped invariants, cell identity, destruction …
refactor(codegen): split assembler backend into separate module
a19q3 commentedon Aug 27, 2026
Closure audit (current nightly-0.25)
The primary defect is still correctly closed.
Evidence:
CellScript/src/codegen/assembler.rs
Lines 340 to 433 in 8ae6dc4
CellScript/scripts/cellscript_gate.sh
Lines 580 to 595 in 8ae6dc4
One secondary diagnostic concern remains: assembler failures still commonly use Span::default() and can report line 0. That concern is now tracked separately in #21 so it does not obscure the fact that B-type range relaxation itself is fixed.
a19q3 commentedon Sep 19, 2026
Status correction from the 2026-09-19 issue audit: the secondary diagnostic follow-up mentioned in the previous comment is also complete under #21. Internal assembler errors now retain generated-assembly provenance without inventing a CellScript source span. The original branch-relaxation fix remains closed.
Candidate 74cff5fd passed the full compiler suites and backend audit, including branch relaxation and generated-assembly diagnostics. See the current issue audit; this is a follow-up to the historical comment, not a new unresolved assembler defect.