Skip to main content

javm_transpiler/
linker.rs

1//! ELF → PVM2 (raw RV+C+custom-0 bytes) linker.
2//!
3//! Pipeline:
4//! 1. **Concatenate code sections** at their ELF vaddr offsets (typical
5//!    LLD PIE output places each function in its own `.text.<sym>`).
6//! 2. **Resolve AUIPC pairs.** *Data* references (an `auipc` paired with
7//!    a load/store/addi of a low-memory address) fold to absolute
8//!    `lui`+lo12 — data is relocated to its runtime address in
9//!    `[DATA_BASE, …)` (the ELF's `[0, extent)` data layout shifted up
10//!    by `DATA_BASE`), unrelated to where code maps. *Code* references
11//!    (`R_RISCV_CALL_PLT` and
12//!    code-targeting `PCREL_HI20`) stay native `auipc`+`jalr`/`addi`:
13//!    code is mapped at [`CODE_BASE`], so the
14//!    PC-relative computation lands on the right code VA. Kept pairs are
15//!    re-encoded after step 4 if fallthrough injection shifts the layout
16//!    (LUI is absolute, so injection-stable, and needs no fixup).
17//! 3. **Replace standard ECALL markers**. The guest convention is
18//!    `csrrw x0, 0x800/0x801, x0` followed by `ecall`; the marker slot
19//!    becomes a NOP and the `ecall` a custom-0 `ecall.jar` / `ecalli`.
20//! 4. **Inject fallthrough markers** before branch/jal/endpoint targets
21//!    that aren't already post-terminator, so the predecoder's strict
22//!    basic-block-start set — derived purely from the instruction stream
23//!    — covers every reachable jump target. `jalr` targets are validated
24//!    against that set at *runtime*; the linker never emits a trusted
25//!    target table (the recompiler runs untrusted code).
26//! 5. **Validate producer output**: no x3/x4 use, no remaining standard
27//!    `ecall` / `ebreak`, no CSR / atomic / FP / custom-1 / privileged
28//!    encodings (see `~/docs/pvm-isa/05-pvm2-rv-diff.md`). `auipc`/`jalr`
29//!    are standard PVM2 instructions and are accepted; x3/x4 are valid
30//!    runtime GPRs but outside the hot-register ABI this linker emits.
31//! 6. **Emit Image** with the raw code bytes in [`Image::code`], mapped
32//!    read-only at the fixed `CODE_BASE` by the runtime. The recompiler
33//!    consumes the raw bytes directly.
34
35use crate::TranspileError;
36use crate::elf::parse_linked_elf;
37use crate::layout::{
38    CODE_BASE, DATA_BASE, HEAP_CAP_INDEX, MAX_CODE_SIZE, PVM_PAGE_SIZE, ProgramLayout,
39    RO_CAP_INDEX, RW_CAP_INDEX, STACK_CAP_INDEX,
40};
41use javm_cap::Key;
42use javm_cap::abi::BARE_YIELD_RECEIVER_SLOT;
43use javm_cap::image::{EndpointDef, Image, ImageBuilder, MemoryMapping};
44use javm_cap::slot::SlotPath;
45use std::collections::BTreeMap;
46
47/// PVM register index for the RISC-V stack pointer (φ[1] = x2).
48const SP_REG: u8 = 1;
49
50/// RV opcode major (low 7 bits) for AUIPC.
51const OP_AUIPC: u32 = 0b001_0111;
52/// RV opcode major for LUI.
53const OP_LUI: u32 = 0b011_0111;
54/// RV opcode major for SYSTEM (CSR ops, ECALL, EBREAK).
55const OP_SYSTEM: u32 = 0b111_0011;
56/// RV opcode major for OP-IMM (addi etc.). Used by the test module.
57#[cfg(test)]
58const OP_OP_IMM: u32 = 0b001_0011;
59/// RV opcode major for custom-0 (PVM2 host ops).
60const OP_CUSTOM_0: u32 = 0b000_1011;
61/// RV opcode major for custom-1 (PVM2 `callf`).
62const OP_CUSTOM_1: u32 = 0b010_1011;
63/// RV opcode major for JAL.
64const OP_JAL: u32 = 0b110_1111;
65/// RV opcode major for JALR.
66const OP_JALR: u32 = 0b110_0111;
67
68/// 32-bit canonical NOP: `addi x0, x0, 0`.
69const NOP_BYTES: [u8; 4] = [0x13, 0x00, 0x00, 0x00];
70
71/// PVM ecall-marker CSR numbers (custom range).
72const CSR_ECALL_JAR: u32 = 0x800;
73const CSR_ECALLI: u32 = 0x801;
74
75/// Link an RV ELF into a PVM2 [`Image`]. [`Image::code`] holds the raw
76/// RV+C+custom-0 bytes, mapped read-only at [`CODE_BASE`] by the runtime.
77pub fn link_elf(elf_data: &[u8]) -> Result<Image, TranspileError> {
78    let elf = parse_linked_elf(elf_data)?;
79
80    // ---- 1. Concatenate code sections ------------------------------
81    //
82    // Multi-section ELFs from lld place each function in its own
83    // `.text.<symbol>` section so dead-code elimination can drop
84    // unused ones. To keep all reloc data working unchanged, we
85    // preserve the original RV vaddr layout in the output: take the
86    // minimum vaddr as the base, allocate a buffer spanning to
87    // `max_vaddr + max_section_size`, and copy each section in at
88    // its vaddr offset. Gaps stay zero (RVC `c.illegal`); the
89    // predecoder records them as Reserved and codegen emits a panic
90    // — fine because gaps shouldn't be reached during execution.
91    if elf.code_sections.is_empty() {
92        return Err(TranspileError::InvalidSection(
93            "link_elf: ELF has no code sections".into(),
94        ));
95    }
96    let mut sections_by_vaddr: Vec<&(u64, u64, Vec<u8>)> = elf.code_sections.iter().collect();
97    sections_by_vaddr.sort_by_key(|(_, v, _)| *v);
98    let base_vaddr = sections_by_vaddr[0].1;
99    let mut code_end_vaddr = base_vaddr;
100    for (_, v, d) in &sections_by_vaddr {
101        let end = v.saturating_add(d.len() as u64);
102        if end > code_end_vaddr {
103            code_end_vaddr = end;
104        }
105    }
106    let span = (code_end_vaddr - base_vaddr) as usize;
107    let mut code: Vec<u8> = vec![0u8; span];
108    for (_, v, d) in &sections_by_vaddr {
109        let off = (v - base_vaddr) as usize;
110        code[off..off + d.len()].copy_from_slice(d);
111    }
112    let code_len = code.len();
113
114    let vaddr_to_offset = |v: u64| -> Option<usize> {
115        if v < base_vaddr {
116            return None;
117        }
118        let o = (v - base_vaddr) as usize;
119        if o >= code_len { None } else { Some(o) }
120    };
121
122    let is_code_addr = |addr: u64| -> bool {
123        elf.code_ranges
124            .iter()
125            .any(|(start, end)| addr >= *start && addr < *end)
126    };
127
128    // ---- 2. Resolve AUIPC pairs ------------------------------------
129    //
130    // lld emits each `auipc rd, hi20` with `hi20` chosen so that
131    //   anchor := auipc_pc + sext(hi20 << 12)
132    // sits within ±2 KiB of the symbol; the paired LO12 instruction
133    // (load/store/addi/jalr) carries `lo12 = target - anchor`.
134    //
135    // *Data* references address the runtime `[DATA_BASE, …)` mapping
136    // absolutely, so we fold them to `lui rd, hi; <op> rd, lo12`
137    // (loading `target + DATA_BASE`; the +0x800 carry compensates
138    // lo12's sign extension). LUI is absolute — unaffected by later
139    // fallthrough injection.
140    //
141    // *Code* references stay native `auipc`/`jalr`/`addi`: code maps at
142    // `CODE_BASE`, so `auipc`'s PC-relative result is already the right
143    // code VA. We only record them here; their displacement is
144    // re-encoded in step 4b after injection settles the final offsets.
145    //
146    // `code_auipc`: auipc byte-offset → target byte-offset.
147    // `code_lo12`:  (lo12 offset, anchor-auipc offset, target offset).
148    let mut code_auipc: BTreeMap<usize, usize> = BTreeMap::new();
149    let mut code_lo12: Vec<(usize, usize, usize)> = Vec::new();
150
151    // CALL_PLT — always a code target: `auipc` + `jalr` at +4.
152    for (&call_v, &target) in &elf.call_targets {
153        let auipc_off = vaddr_to_offset(call_v).ok_or_else(|| {
154            TranspileError::InvalidSection(format!(
155                "link_elf: CALL_PLT AUIPC at vaddr {call_v:#x} outside code section"
156            ))
157        })?;
158        let target_off = vaddr_to_offset(target).ok_or_else(|| {
159            TranspileError::InvalidSection(format!(
160                "link_elf: CALL_PLT target {target:#x} (from {call_v:#x}) outside code section"
161            ))
162        })?;
163        expect_auipc(&code, auipc_off, call_v)?;
164        code_auipc.insert(auipc_off, target_off);
165        if let Some(jalr_off) = vaddr_to_offset(call_v + 4) {
166            code_lo12.push((jalr_off, auipc_off, target_off));
167        }
168    }
169
170    // PCREL_HI20 — code target stays native `auipc`; data folds to LUI.
171    for (&hi20_v, &target) in &elf.hi20_targets {
172        let auipc_off = vaddr_to_offset(hi20_v).ok_or_else(|| {
173            TranspileError::InvalidSection(format!(
174                "link_elf: PCREL_HI20 AUIPC at vaddr {hi20_v:#x} outside code section"
175            ))
176        })?;
177        if is_code_addr(target) {
178            let target_off = vaddr_to_offset(target).ok_or_else(|| {
179                TranspileError::InvalidSection(format!(
180                    "link_elf: PCREL_HI20 code target {target:#x} (from {hi20_v:#x}) out of range"
181                ))
182            })?;
183            expect_auipc(&code, auipc_off, hi20_v)?;
184            code_auipc.insert(auipc_off, target_off);
185        } else {
186            // Data target: relocate from the ELF's `[0, extent)` data
187            // layout to the runtime `[DATA_BASE, …)` mapping.
188            let data_target = target.wrapping_add(u64::from(DATA_BASE));
189            fold_auipc_to_lui(
190                &mut code,
191                auipc_off,
192                hi20_v,
193                (data_target & 0xFFFF_FFFF) as u32,
194            )?;
195        }
196    }
197
198    // PCREL_LO12 — code lo12 re-encoded in step 4b; data lo12 patched
199    // to the absolute target's low 12 bits now.
200    for (&lo_v, &target) in &elf.lo12_targets {
201        let Some(lo_off) = vaddr_to_offset(lo_v) else {
202            continue;
203        };
204        if lo_off + 4 > code.len() {
205            continue;
206        }
207        if is_code_addr(target) {
208            if let Some(&hi20_v) = elf.lo12_to_hi20.get(&lo_v)
209                && let (Some(auipc_off), Some(target_off)) =
210                    (vaddr_to_offset(hi20_v), vaddr_to_offset(target))
211            {
212                code_lo12.push((lo_off, auipc_off, target_off));
213            }
214        } else {
215            // Data target: relocate to the runtime `[DATA_BASE, …)`
216            // mapping (DATA_BASE is page-aligned, so the low 12 bits the
217            // LO12 carries are unchanged — kept explicit for clarity).
218            let data_target = target.wrapping_add(u64::from(DATA_BASE));
219            patch_lo12_abs(&mut code, lo_off, (data_target & 0xFFFF_FFFF) as u32);
220        }
221    }
222
223    // ---- 3. ECALL marker replacement -------------------------------
224    //
225    // The guest emits the same CSRRW(0x800/0x801) + ECALL sequence as
226    // the PVM path. We scan the code for those exact two-instruction
227    // sequences and rewrite them in-place:
228    //
229    //  - CSRRW x0, 0x800, x0 → NOP, then ECALL → custom-0 ecall.jar.
230    //  - CSRRW x0, 0x801, x0 → NOP, then ECALL → custom-0 ecalli imm.
231    //
232    // For ecalli, the host-call selector is in x5 (t0); we leave that
233    // intact and use ecalli with imm=0 (the actual selector flows
234    // through x5 at runtime — matching today's PVM ecalli behaviour).
235    rewrite_ecall_markers(&mut code)?;
236
237    // ---- 4. Fallthrough injection ----------------------------------
238    //
239    // Branch / jal / endpoint / rodata-code-pointer targets aren't
240    // necessarily post-terminator. Inject `fallthrough` (4 bytes,
241    // custom-0, terminator no-op) before each such target so the
242    // predecode's strict basic-block-start set — derived purely from
243    // the instruction stream, never trusted from linker metadata —
244    // covers everything reachable. `jalr` targets are validated against
245    // that set at runtime.
246    //
247    // A statically-known jalr target must be a block start too. Resume
248    // PCs (the instruction after a call's jalr) already are — jalr is a
249    // terminator, so its successor is post-terminator. But the *call
250    // targets* (CALL_PLT and code-`hi20` function entries reached via
251    // `auipc`+`jalr`) are not static jal edges, so they're fed as
252    // `extra_targets` alongside endpoint entries and `.rodata`
253    // code-pointer targets. `align_branch_targets` only injects where a
254    // target isn't already post-terminator, so entries that already
255    // follow a `ret`/`j` cost nothing.
256    let endpoint_entries_pre: Vec<usize> = {
257        match crate::elf::find_all_section_bytes(elf_data, ".subsoil.endpoints") {
258            Ok(sections) => sections
259                .iter()
260                .flat_map(|s| s.chunks(16))
261                .filter_map(|chunk| {
262                    if chunk.len() < 8 {
263                        return None;
264                    }
265                    let fn_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
266                    if fn_ptr < base_vaddr {
267                        return None;
268                    }
269                    Some((fn_ptr - base_vaddr) as usize)
270                })
271                .collect(),
272            Err(_) => Vec::new(),
273        }
274    };
275    let rodata_targets_pre: Vec<usize> = elf
276        .abs_code_ptrs
277        .iter()
278        .filter_map(|&(_, rv_target, _)| {
279            if is_code_addr(rv_target) {
280                Some(rv_target.wrapping_sub(base_vaddr) as usize)
281            } else {
282                None
283            }
284        })
285        .collect();
286    let mut extra_targets: Vec<usize> = endpoint_entries_pre;
287    extra_targets.extend_from_slice(&rodata_targets_pre);
288    extra_targets.extend(code_auipc.values().copied());
289    let offset_map = align_branch_targets(&mut code, &extra_targets)?;
290
291    // ---- 4b. Re-encode kept code-relative pairs --------------------
292    //
293    // Injection may have shifted offsets between an `auipc` and its
294    // target, invalidating the original displacement. Recompute each
295    // kept pair's PC-relative split against the post-injection layout.
296    fixup_code_pcrel(&mut code, &offset_map, &code_auipc, &code_lo12)?;
297
298    // ---- 5. Validation pass ----------------------------------------
299    //
300    // Walk every 2- or 4-byte instruction boundary (RV+C self-describes
301    // length via op[1:0]) and reject anything that PVM2 forbids.
302    validate_pvm2(&code)?;
303
304    // ---- 5b. Rewrite code pointers in .rodata -----------------------
305    //
306    // Function pointer tables (e.g. LLVM jump tables, vtables) store
307    // code addresses as raw u32/u64 values in .rodata. The original
308    // values are ELF vaddrs; at runtime a `jalr` through such a pointer
309    // validates the target VA against the basic-block-start set, so each
310    // pointer must become `CODE_BASE + post-injection byte offset`.
311    //
312    // SUB32-based relative jump tables (entries `target - base`) are
313    // left as-is: their base register is loaded from a *data* address
314    // (the table lives in `.rodata`), so `base + delta` reconstructs the
315    // ELF vaddr, not `CODE_BASE + offset`. Such a `jalr` target fails the
316    // runtime block-start check and faults loudly rather than corrupting
317    // state — relocating relative tables into the CODE_BASE model is a
318    // follow-up (TODO). The absolute-pointer path below is correct.
319    let mut ro_data_rewritten = elf.ro_data.clone();
320    let ro_base = elf.stack_size as u64;
321    {
322        // Build a set of vaddrs handled via sub32 (so we skip them in
323        // the absolute-rewrite pass).
324        let sub32_data_vaddrs: std::collections::HashSet<u64> =
325            elf.sub32_relocs.iter().map(|(v, _)| *v).collect();
326
327        // Translate a code address (RV vaddr) to its guest VA:
328        // `CODE_BASE + post-injection byte offset within the region`.
329        let translate_code_addr = |rv_target: u64| -> u32 {
330            let pre = rv_target.wrapping_sub(base_vaddr) as usize;
331            let off = offset_map.get(&pre).copied().unwrap_or(pre);
332            CODE_BASE.wrapping_add(off as u32)
333        };
334
335        for &(data_vaddr, rv_target, size) in &elf.abs_code_ptrs {
336            if sub32_data_vaddrs.contains(&data_vaddr) {
337                // Relative entry — uniform shift preserves the diff.
338                continue;
339            }
340            if !is_code_addr(rv_target) {
341                continue;
342            }
343            if data_vaddr < ro_base {
344                continue;
345            }
346            let off = (data_vaddr - ro_base) as usize;
347            let new_val = translate_code_addr(rv_target);
348            match size {
349                4 if off + 4 <= ro_data_rewritten.len() => {
350                    ro_data_rewritten[off..off + 4].copy_from_slice(&new_val.to_le_bytes());
351                }
352                8 if off + 8 <= ro_data_rewritten.len() => {
353                    ro_data_rewritten[off..off + 8]
354                        .copy_from_slice(&(new_val as u64).to_le_bytes());
355                }
356                _ => {}
357            }
358        }
359
360        // Heuristic: 8-byte values in .rodata that look like code
361        // pointers but aren't covered by an explicit reloc.
362        let mut off = 0;
363        let already_covered: std::collections::HashSet<u64> =
364            elf.abs_code_ptrs.iter().map(|&(v, _, _)| v).collect();
365        while off + 8 <= ro_data_rewritten.len() {
366            let val = u64::from_le_bytes(ro_data_rewritten[off..off + 8].try_into().unwrap());
367            if is_code_addr(val) {
368                let vaddr = ro_base + off as u64;
369                if !already_covered.contains(&vaddr) {
370                    let new_val = translate_code_addr(val);
371                    ro_data_rewritten[off..off + 8]
372                        .copy_from_slice(&(new_val as u64).to_le_bytes());
373                }
374            }
375            off += 8;
376        }
377    }
378
379    // ---- 5c. Relocate absolute data pointers ------------------------
380    //
381    // Pointers stored in data that point *into data* (e.g. `&'static`
382    // constants in `.data.rel.ro`) hold ELF data vaddrs (the `[0,
383    // extent)` layout). The runtime maps data at `[DATA_BASE, …)`, so
384    // shift each by `+DATA_BASE`. Data-targeting abs relocs the parser
385    // captured but the code-pointer pass above ignored. A pointer that
386    // lands in neither the RO nor RW blob is unrelocatable — error
387    // loudly rather than emit a silently-wrong pointer.
388    let mut rw_data_rewritten = elf.rw_data.clone();
389    {
390        let ro_base = elf.stack_size as u64;
391        let rw_base = elf.rw_base;
392        for &(data_vaddr, target, size) in &elf.abs_data_ptrs {
393            let new_val = target.wrapping_add(u64::from(DATA_BASE));
394            let n = size as usize;
395            let bytes = new_val.to_le_bytes();
396            if data_vaddr >= ro_base
397                && (data_vaddr - ro_base) as usize + n <= ro_data_rewritten.len()
398            {
399                let off = (data_vaddr - ro_base) as usize;
400                ro_data_rewritten[off..off + n].copy_from_slice(&bytes[..n]);
401            } else if data_vaddr >= rw_base
402                && (data_vaddr - rw_base) as usize + n <= rw_data_rewritten.len()
403            {
404                let off = (data_vaddr - rw_base) as usize;
405                rw_data_rewritten[off..off + n].copy_from_slice(&bytes[..n]);
406            } else {
407                return Err(TranspileError::InvalidSection(format!(
408                    "link_elf: absolute data pointer at vaddr {data_vaddr:#x} (→ {target:#x}) \
409                     falls outside the RO/RW data blobs; cannot relocate to DATA_BASE"
410                )));
411            }
412        }
413    }
414
415    // ---- 6. Endpoints -----------------------------------------------
416    //
417    // `entry_pc` stays a code-region byte offset; the runtime adds
418    // `CODE_BASE` when it seeds the PC. Remap through `offset_map` to
419    // account for any fallthrough injection.
420    let mut endpoints = read_subsoil_endpoints_rv(elf_data, base_vaddr, code.len())?;
421    for def in endpoints.values_mut() {
422        let pre = def.entry_pc as usize;
423        if let Some(&new) = offset_map.get(&pre) {
424            def.entry_pc = new as u64;
425        }
426    }
427
428    // ---- 7. Memory layout + Image construction ----------------------
429    let ro_data = ro_data_rewritten;
430    let rw_data = rw_data_rewritten;
431
432    let stack_pages = elf.stack_size / PVM_PAGE_SIZE;
433    let ro_pages = (ro_data.len() as u32).div_ceil(PVM_PAGE_SIZE);
434    let rw_pages = (rw_data.len() as u32).div_ceil(PVM_PAGE_SIZE);
435    let layout = ProgramLayout::compute(stack_pages, ro_pages, rw_pages, elf.heap_pages);
436    let stack_top = layout.stack_top();
437
438    for def in endpoints.values_mut() {
439        def.initial_regs.insert(SP_REG, stack_top);
440    }
441
442    // The data regions are handed to `ImageBuilder` as the same contiguous
443    // `ro_data`/`rw_data` buffers as before; `build()` page-splits them,
444    // elides all-zero pages (the leading rw gap and trailing `.bss` stop
445    // being serialized), and deduplicates identical pages into one shared
446    // page-granular `arena`. `MemoryMapping`s and geometry are unchanged —
447    // each region's `size` is still `page_count * PAGE_SIZE`, so the
448    // resulting `DataCap`s are byte- and hash-identical to the old inline
449    // form; only the wire encoding shrinks.
450    let mut memory_mappings: Vec<MemoryMapping> = Vec::new();
451    let page_bytes = u64::from(PVM_PAGE_SIZE);
452    let mut builder = ImageBuilder::new();
453
454    let stack_slot = Key::from(STACK_CAP_INDEX);
455    let stack_size = u64::from(layout.stack.page_count) * page_bytes;
456    memory_mappings.push(MemoryMapping {
457        start: u64::from(layout.stack.base_page) * page_bytes,
458        size: stack_size,
459        source: SlotPath::root(stack_slot.clone()),
460    });
461    builder = builder.initial_data(stack_slot, Vec::new(), stack_size);
462
463    if let Some(ro) = &layout.ro {
464        let ro_slot = Key::from(RO_CAP_INDEX);
465        let size = u64::from(ro.page_count) * page_bytes;
466        memory_mappings.push(MemoryMapping {
467            start: u64::from(ro.base_page) * page_bytes,
468            size,
469            source: SlotPath::root(ro_slot.clone()),
470        });
471        builder = builder.pinned_data(ro_slot, ro_data, size);
472    }
473
474    if let Some(rw) = &layout.rw {
475        let rw_slot = Key::from(RW_CAP_INDEX);
476        let size = u64::from(rw.page_count) * page_bytes;
477        memory_mappings.push(MemoryMapping {
478            start: u64::from(rw.base_page) * page_bytes,
479            size,
480            source: SlotPath::root(rw_slot.clone()),
481        });
482        builder = builder.initial_data(rw_slot, rw_data, size);
483    }
484
485    if let Some(heap) = &layout.heap {
486        let heap_slot = Key::from(HEAP_CAP_INDEX);
487        let size = u64::from(heap.page_count) * page_bytes;
488        memory_mappings.push(MemoryMapping {
489            start: u64::from(heap.base_page) * page_bytes,
490            size,
491            source: SlotPath::root(heap_slot.clone()),
492        });
493        builder = builder.initial_data(heap_slot, Vec::new(), size);
494    }
495
496    // Layout geometry: code occupies `[CODE_BASE, CODE_BASE +
497    // code_size)` and must stay below DATA_BASE (i.e. `code_size ≤
498    // MAX_CODE_SIZE`); data occupies `[DATA_BASE, DATA_BASE +
499    // data_extent)` and must stay within the 4 GiB guest range.
500    let code_base = u64::from(CODE_BASE);
501    let code_size = (code.len() as u64).div_ceil(page_bytes) * page_bytes;
502    if code_base + code_size > u64::from(DATA_BASE) {
503        return Err(TranspileError::InvalidSection(format!(
504            "link_elf: code size {code_size:#x} exceeds MAX_CODE_SIZE {:#x} (would overlap DATA_BASE {:#x})",
505            MAX_CODE_SIZE, DATA_BASE,
506        )));
507    }
508    let data_end = u64::from(DATA_BASE) + u64::from(layout.total_data_pages()) * page_bytes;
509    if data_end > (1u64 << 32) {
510        return Err(TranspileError::InvalidSection(format!(
511            "link_elf: data end {data_end:#x} exceeds the 4 GiB guest range"
512        )));
513    }
514    // Code is mapped RO at the fixed `CODE_BASE` by the runtime — not
515    // via a declarative mapping, so an untrusted Image cannot relocate
516    // it. `memory_mappings` describes data/slot regions only.
517
518    builder = builder.code(code);
519    for (key, ep) in endpoints {
520        builder = builder.endpoint(key, ep);
521    }
522    for mapping in memory_mappings {
523        builder = builder.mapping(mapping);
524    }
525    Ok(builder
526        .yield_receiver_slot(Some(Key::from(BARE_YIELD_RECEIVER_SLOT)))
527        .build())
528}
529
530/// Verify the 4 bytes at `off` decode to an `auipc`; error otherwise.
531/// Used before recording a code reference whose AUIPC we keep native.
532fn expect_auipc(code: &[u8], off: usize, v: u64) -> Result<(), TranspileError> {
533    if off + 4 > code.len() {
534        return Err(TranspileError::InvalidSection(format!(
535            "link_elf: AUIPC reloc at vaddr {v:#x} truncated by section end"
536        )));
537    }
538    let word = u32::from_le_bytes([code[off], code[off + 1], code[off + 2], code[off + 3]]);
539    if word & 0x7F != OP_AUIPC {
540        return Err(TranspileError::InvalidSection(format!(
541            "link_elf: reloc at vaddr {v:#x} not an AUIPC (opcode {:#x})",
542            word & 0x7F
543        )));
544    }
545    Ok(())
546}
547
548/// Fold a *data* `auipc rd, hi20` at `off` into `lui rd, hi` loading the
549/// absolute 4 KiB-aligned base of `eff` (the paired lo12 supplies the
550/// rest). The +0x800 carry compensates the lo12's sign extension.
551fn fold_auipc_to_lui(code: &mut [u8], off: usize, v: u64, eff: u32) -> Result<(), TranspileError> {
552    if off + 4 > code.len() {
553        return Err(TranspileError::InvalidSection(format!(
554            "link_elf: AUIPC reloc at vaddr {v:#x} truncated by section end"
555        )));
556    }
557    let word = u32::from_le_bytes([code[off], code[off + 1], code[off + 2], code[off + 3]]);
558    if word & 0x7F != OP_AUIPC {
559        return Err(TranspileError::InvalidSection(format!(
560            "link_elf: reloc at vaddr {v:#x} not an AUIPC (opcode {:#x})",
561            word & 0x7F
562        )));
563    }
564    let rd = (word >> 7) & 0x1F;
565    let new_word = (eff.wrapping_add(0x800) & 0xFFFF_F000) | (rd << 7) | OP_LUI;
566    code[off..off + 4].copy_from_slice(&new_word.to_le_bytes());
567    Ok(())
568}
569
570/// Patch a *data* LO12 instruction (I- or S-type) with the absolute
571/// low 12 bits of `eff` (sign-extended).
572fn patch_lo12_abs(code: &mut [u8], off: usize, eff: u32) {
573    let new_lo12 = ((eff as i32) << 20) >> 20;
574    match code[off] & 0x7F {
575        // I-type (load, addi, jalr) — imm in [31:20].
576        0b0000011 | 0b0010011 | 0b1100111 => patch_imm_i(&mut code[off..off + 4], new_lo12),
577        // S-type (store) — imm[11:5] in [31:25], imm[4:0] in [11:7].
578        0b0100011 => patch_imm_s(&mut code[off..off + 4], new_lo12),
579        _ => {}
580    }
581}
582
583/// Re-encode the displacement of every kept code-relative `auipc` pair
584/// against the post-injection layout. The `auipc` carries the high 20
585/// bits (with the +0x800 carry) and the paired `jalr`/`addi`/load/store
586/// the low 12 (sign-extended), both relative to the *AUIPC's* PC.
587fn fixup_code_pcrel(
588    code: &mut [u8],
589    offset_map: &BTreeMap<usize, usize>,
590    code_auipc: &BTreeMap<usize, usize>,
591    code_lo12: &[(usize, usize, usize)],
592) -> Result<(), TranspileError> {
593    let remap = |o: usize| -> Result<usize, TranspileError> {
594        offset_map.get(&o).copied().ok_or_else(|| {
595            TranspileError::InvalidSection(format!(
596                "fixup_code_pcrel: offset {o:#x} not in offset_map"
597            ))
598        })
599    };
600    for (&auipc_off, &target_off) in code_auipc {
601        let na = remap(auipc_off)?;
602        let nt = remap(target_off)?;
603        if na + 4 > code.len() {
604            continue;
605        }
606        let word = u32::from_le_bytes([code[na], code[na + 1], code[na + 2], code[na + 3]]);
607        if word & 0x7F != OP_AUIPC {
608            return Err(TranspileError::InvalidSection(format!(
609                "fixup_code_pcrel: expected AUIPC at offset {na:#x} (opcode {:#x})",
610                word & 0x7F
611            )));
612        }
613        let disp = nt as i64 - na as i64;
614        let rd = (word >> 7) & 0x1F;
615        let new_word = ((disp as u32).wrapping_add(0x800) & 0xFFFF_F000) | (rd << 7) | OP_AUIPC;
616        code[na..na + 4].copy_from_slice(&new_word.to_le_bytes());
617    }
618    for &(lo12_off, auipc_off, target_off) in code_lo12 {
619        let nl = remap(lo12_off)?;
620        let na = remap(auipc_off)?;
621        let nt = remap(target_off)?;
622        if nl + 4 > code.len() {
623            continue;
624        }
625        let disp = nt as i64 - na as i64;
626        let new_lo12 = ((disp as i32) << 20) >> 20;
627        match code[nl] & 0x7F {
628            0b0000011 | 0b0010011 | 0b1100111 => patch_imm_i(&mut code[nl..nl + 4], new_lo12),
629            0b0100011 => patch_imm_s(&mut code[nl..nl + 4], new_lo12),
630            _ => {}
631        }
632    }
633    Ok(())
634}
635
636/// Walk `code` and rewrite ECALL-related sequences:
637///
638/// - `CSRRW(0x800) + ECALL` → `NOP + custom-0 ecall.jar`.
639/// - `CSRRW(0x801) + ECALL` → `NOP + custom-0 ecalli imm=0`.
640/// - Bare standard `ECALL` (not preceded by a marker) → custom-0
641///   `ecalli imm=0`. This mirrors the legacy fallback in the PVM
642///   transpiler (`riscv.rs`: "No marker (legacy) — treat as ecalli for
643///   backward compat").
644fn rewrite_ecall_markers(code: &mut [u8]) -> Result<(), TranspileError> {
645    let n = code.len();
646    let mut i = 0;
647    while i + 2 <= n {
648        // RVC slots have op[1:0] != 11; skip them.
649        let lo = u16::from_le_bytes([code[i], code[i + 1]]);
650        if lo & 0b11 != 0b11 {
651            i += 2;
652            continue;
653        }
654        if i + 4 > n {
655            break;
656        }
657        let word = u32::from_le_bytes([code[i], code[i + 1], code[i + 2], code[i + 3]]);
658        let opcode = word & 0x7F;
659        let funct3 = (word >> 12) & 0x7;
660        if opcode == OP_SYSTEM && funct3 == 0b001 {
661            // CSRRW. Check csr field.
662            let csr = (word >> 20) & 0xFFF;
663            if csr == CSR_ECALL_JAR || csr == CSR_ECALLI {
664                code[i..i + 4].copy_from_slice(&NOP_BYTES);
665                let j = i + 4;
666                if j + 4 <= n {
667                    let nxt = u32::from_le_bytes([code[j], code[j + 1], code[j + 2], code[j + 3]]);
668                    if is_full_length(nxt) && is_standard_ecall(nxt) {
669                        let new_word = if csr == CSR_ECALL_JAR {
670                            encode_custom0_ecall_jar()
671                        } else {
672                            encode_custom0_ecalli(0)
673                        };
674                        code[j..j + 4].copy_from_slice(&new_word.to_le_bytes());
675                        i = j + 4;
676                        continue;
677                    }
678                }
679                // Marker without follow-up ECALL — pass through as NOP,
680                // keep scanning.
681                i += 4;
682                continue;
683            }
684        }
685        if opcode == OP_SYSTEM && funct3 == 0 && is_standard_ecall(word) {
686            // Bare ECALL with no preceding marker → custom-0 ecalli imm=0.
687            let new_word = encode_custom0_ecalli(0);
688            code[i..i + 4].copy_from_slice(&new_word.to_le_bytes());
689        }
690        i += 4;
691    }
692    Ok(())
693}
694
695/// Which 5-bit fields of a 4-byte RV instruction encode registers
696/// (vs. parts of an immediate). Used by [`validate_pvm2`] so we don't
697/// flag S/B-type immediates that happen to match x3/x4 as "register
698/// use".
699#[derive(Clone, Copy)]
700struct RegFields {
701    rd: bool,
702    rs1: bool,
703    rs2: bool,
704}
705const REG_NONE: RegFields = RegFields {
706    rd: false,
707    rs1: false,
708    rs2: false,
709};
710
711/// Return which fields of `w` carry register numbers, given the
712/// 7-bit major opcode.
713fn reg_fields_for(opcode: u32) -> RegFields {
714    match opcode {
715        // R-type: rd, rs1, rs2 (OP, OP-32).
716        0b011_0011 | 0b011_1011 => RegFields {
717            rd: true,
718            rs1: true,
719            rs2: true,
720        },
721        // I-type loads (LOAD).
722        0b000_0011 => RegFields {
723            rd: true,
724            rs1: true,
725            rs2: false,
726        },
727        // I-type ALU (OP-IMM, OP-IMM-32) and JALR — rd + rs1 are
728        // registers; the I-type slot holds the immediate.
729        0b001_0011 | 0b001_1011 | 0b110_0111 => RegFields {
730            rd: true,
731            rs1: true,
732            rs2: false,
733        },
734        // S-type stores: rs1, rs2 are regs; rd slot is imm[4:0].
735        0b010_0011 => RegFields {
736            rd: false,
737            rs1: true,
738            rs2: true,
739        },
740        // B-type branches: rs1, rs2 are regs; rd slot is imm.
741        0b110_0011 => RegFields {
742            rd: false,
743            rs1: true,
744            rs2: true,
745        },
746        // U-type (LUI, AUIPC): rd is reg; rs1/rs2 slots are imm.
747        0b011_0111 | 0b001_0111 => RegFields {
748            rd: true,
749            rs1: false,
750            rs2: false,
751        },
752        // J-type (JAL): rd is reg; rs1/rs2 slots are imm.
753        0b110_1111 => RegFields {
754            rd: true,
755            rs1: false,
756            rs2: false,
757        },
758        // MISC-MEM (FENCE): no registers in scope.
759        0b000_1111 => REG_NONE,
760        // custom-0 (PVM2 host ops): trap/ecall.jar/ecalli — all reg
761        // fields are zero. ecalli's imm lives in the I-type slot,
762        // so we treat it as I-type for safety (rd = x0 always).
763        0b000_1011 => RegFields {
764            rd: true,
765            rs1: true,
766            rs2: false,
767        },
768        _ => REG_NONE,
769    }
770}
771
772/// Validate that `code` contains only the producer-emitted PVM2 subset.
773///
774/// Reject: standard ECALL (not preceded by a marker — so any remaining
775/// ECALL after the rewrite pass is unaccounted for), EBREAK, CSR ops,
776/// atomics, FP/V, privileged, x16..x31, and producer-forbidden references
777/// to x3/x4. The runtime still treats x3/x4 as valid spilled RV64E GPRs.
778fn validate_pvm2(code: &[u8]) -> Result<(), TranspileError> {
779    let n = code.len();
780    let mut i = 0;
781    while i < n {
782        if i + 2 > n {
783            break;
784        }
785        let lo16 = u16::from_le_bytes([code[i], code[i + 1]]);
786        if lo16 & 0b11 != 0b11 {
787            // RVC. RVC reg fields use x8..x15 (3-bit encoding), which
788            // can't reference x3/x4. RVC `c.ebreak` is allowed by RV
789            // but PVM2 wants standard ebreak rejected; c.ebreak is
790            // encoding 0x9002 — reject it explicitly.
791            if lo16 == 0x9002 {
792                return Err(TranspileError::InvalidSection(format!(
793                    "link_elf: c.ebreak at offset {:#x} (forbidden)",
794                    i
795                )));
796            }
797            i += 2;
798            continue;
799        }
800        if i + 4 > n {
801            break;
802        }
803        let w = u32::from_le_bytes([code[i], code[i + 1], code[i + 2], code[i + 3]]);
804        let opcode = w & 0x7F;
805        match opcode {
806            OP_CUSTOM_1 => {
807                return Err(TranspileError::InvalidSection(format!(
808                    "link_elf: custom-1 opcode at offset {:#x} is reserved in PVM2",
809                    i
810                )));
811            }
812            OP_SYSTEM => {
813                let funct3 = (w >> 12) & 0x7;
814                let csr_or_imm = (w >> 20) & 0xFFF;
815                if funct3 == 0 {
816                    return Err(TranspileError::InvalidSection(format!(
817                        "link_elf: standard ECALL/EBREAK at offset {:#x} (imm={:#x})",
818                        i, csr_or_imm
819                    )));
820                }
821                return Err(TranspileError::InvalidSection(format!(
822                    "link_elf: CSR op at offset {:#x} (funct3={})",
823                    i, funct3
824                )));
825            }
826            0b010_1111 => {
827                return Err(TranspileError::InvalidSection(format!(
828                    "link_elf: atomic op at offset {:#x}",
829                    i
830                )));
831            }
832            0b000_0111 | 0b010_0111 => {
833                return Err(TranspileError::InvalidSection(format!(
834                    "link_elf: FP load/store at offset {:#x}",
835                    i
836                )));
837            }
838            0b101_0011 => {
839                return Err(TranspileError::InvalidSection(format!(
840                    "link_elf: FP arithmetic at offset {:#x}",
841                    i
842                )));
843            }
844            _ => {}
845        }
846        // Check register fields based on the instruction encoding type.
847        let rf = reg_fields_for(opcode);
848        let rd = (w >> 7) & 0x1F;
849        let rs1 = (w >> 15) & 0x1F;
850        let rs2 = (w >> 20) & 0x1F;
851        // Producer-forbidden registers: x3/x4 are valid runtime GPRs but
852        // outside the 13-hot-register ABI this linker emits. x16..x31 do not
853        // exist in RV64E and are reserved/illegal at runtime too. Kept local
854        // so the transpiler need not depend on the executor crate.
855        let check = |name: &str, r: u32| -> Result<(), TranspileError> {
856            if r == 3 || r == 4 || r >= 16 {
857                return Err(TranspileError::InvalidSection(format!(
858                    "link_elf: forbidden register x{} ({}) at offset {:#x}",
859                    r, name, i
860                )));
861            }
862            Ok(())
863        };
864        if rf.rd {
865            check("rd", rd)?;
866        }
867        if rf.rs1 {
868            check("rs1", rs1)?;
869        }
870        if rf.rs2 {
871            check("rs2", rs2)?;
872        }
873        i += 4;
874    }
875    Ok(())
876}
877
878/// Identify the `.subsoil.endpoints` section, parse its 16-byte
879/// descriptors, and resolve `fn_ptr` (RV vaddr) into an RV-byte-offset
880/// PC. The identity map `(rv_vaddr - base_vaddr) -> pc` works because
881/// the rewritten code keeps each instruction at its original offset.
882fn read_subsoil_endpoints_rv(
883    elf_data: &[u8],
884    base_vaddr: u64,
885    code_len: usize,
886) -> Result<BTreeMap<Key, EndpointDef>, TranspileError> {
887    let sections = crate::elf::find_all_section_bytes(elf_data, ".subsoil.endpoints")?;
888    const DESCRIPTOR_SIZE: usize = 16;
889    // Endpoints are keyed by a Key selector (the V1 single-byte ABI maps the
890    // descriptor's u8 index to the one-byte key `[index]`).
891    let mut endpoints: BTreeMap<Key, EndpointDef> = BTreeMap::new();
892    for section_bytes in &sections {
893        if section_bytes.len() % DESCRIPTOR_SIZE != 0 {
894            return Err(TranspileError::InvalidSection(format!(
895                ".subsoil.endpoints size {} is not a multiple of {}",
896                section_bytes.len(),
897                DESCRIPTOR_SIZE
898            )));
899        }
900        for chunk in section_bytes.chunks(DESCRIPTOR_SIZE) {
901            let fn_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
902            let index = chunk[8];
903            let arg_registers = chunk[9];
904            let arg_cnode_size = chunk[10];
905            if fn_ptr < base_vaddr || fn_ptr >= base_vaddr + code_len as u64 {
906                return Err(TranspileError::InvalidSection(format!(
907                    "subsoil endpoint {} fn_ptr {:#x} outside code section",
908                    index, fn_ptr
909                )));
910            }
911            let rv_pc = fn_ptr - base_vaddr;
912            if endpoints
913                .insert(
914                    Key::from(index),
915                    EndpointDef {
916                        entry_pc: rv_pc,
917                        arg_registers,
918                        arg_cnode_size,
919                        initial_regs: BTreeMap::new(),
920                    },
921                )
922                .is_some()
923            {
924                return Err(TranspileError::InvalidSection(format!(
925                    "duplicate #[subsoil::endpoint({})] declaration",
926                    index
927                )));
928            }
929        }
930    }
931    if endpoints.is_empty() {
932        return Err(TranspileError::InvalidSection(
933            ".subsoil.endpoints section is absent or empty: \
934             the guest must declare at least one #[subsoil::endpoint(N)]"
935                .into(),
936        ));
937    }
938    Ok(endpoints)
939}
940
941/// True iff the 32-bit RV word is a "full-length" (4-byte) instruction
942/// (bits[1:0] == 11). For 16-bit RVC instructions the same byte
943/// position has bits[1:0] != 11 in the low 16 bits.
944#[inline]
945fn is_full_length(word: u32) -> bool {
946    word & 0b11 == 0b11
947}
948
949/// Patch an I-type instruction's 12-bit imm (bits [31:20]) in place.
950/// `imm` is the signed 12-bit value; only the low 12 bits are used.
951fn patch_imm_i(slot: &mut [u8], imm: i32) {
952    let w = u32::from_le_bytes([slot[0], slot[1], slot[2], slot[3]]);
953    let cleared = w & 0x000F_FFFF;
954    let imm12 = (imm as u32) & 0xFFF;
955    let patched = cleared | (imm12 << 20);
956    slot[0..4].copy_from_slice(&patched.to_le_bytes());
957}
958
959/// Patch an S-type instruction's 12-bit imm (bits [31:25] | [11:7]).
960fn patch_imm_s(slot: &mut [u8], imm: i32) {
961    let w = u32::from_le_bytes([slot[0], slot[1], slot[2], slot[3]]);
962    let cleared = w & 0x01FF_F07F;
963    let imm12 = (imm as u32) & 0xFFF;
964    let hi7 = (imm12 >> 5) & 0x7F;
965    let lo5 = imm12 & 0x1F;
966    let patched = cleared | (hi7 << 25) | (lo5 << 7);
967    slot[0..4].copy_from_slice(&patched.to_le_bytes());
968}
969
970/// True for the standard RV `ECALL` encoding `0x00000073`.
971#[inline]
972fn is_standard_ecall(word: u32) -> bool {
973    word == 0x0000_0073
974}
975
976/// Encode custom-0 `ecall.jar`: `(funct3=001)(rest=0)`.
977#[inline]
978fn encode_custom0_ecall_jar() -> u32 {
979    // funct3 = 001 in bits [14:12]; opcode in [6:0].
980    (0b001 << 12) | OP_CUSTOM_0
981}
982
983/// Encode custom-0 `ecalli imm`: `(funct3=010)(imm[19:0])`.
984/// imm placed in bits [31:20] (12-bit signed I-type slot).
985#[inline]
986fn encode_custom0_ecalli(imm: i32) -> u32 {
987    let imm12 = (imm as u32) & 0xFFF;
988    (imm12 << 20) | (0b010 << 12) | OP_CUSTOM_0
989}
990
991/// Encode custom-0 `fallthrough` (funct3 = 100; all other fields zero).
992/// A 4-byte terminator no-op that creates a bb_start at the next byte.
993#[inline]
994fn encode_custom0_fallthrough() -> u32 {
995    (0b100 << 12) | OP_CUSTOM_0
996}
997
998/// Decode J-type immediate (sign-extended 21-bit).
999fn imm_j(w: u32) -> i32 {
1000    let b20 = (w >> 31) & 1;
1001    let b10_1 = (w >> 21) & 0x3FF;
1002    let b11 = (w >> 20) & 1;
1003    let b19_12 = (w >> 12) & 0xFF;
1004    let raw = (b20 << 20) | (b19_12 << 12) | (b11 << 11) | (b10_1 << 1);
1005    ((raw as i32) << 11) >> 11
1006}
1007
1008/// Decode B-type immediate (sign-extended 13-bit).
1009fn imm_b(w: u32) -> i32 {
1010    let b12 = (w >> 31) & 1;
1011    let b11 = (w >> 7) & 1;
1012    let b10_5 = (w >> 25) & 0x3F;
1013    let b4_1 = (w >> 8) & 0xF;
1014    let raw = (b12 << 12) | (b11 << 11) | (b10_5 << 5) | (b4_1 << 1);
1015    ((raw as i32) << 19) >> 19
1016}
1017
1018/// Encode B-type immediate into an existing branch instruction word.
1019fn encode_b_imm(opcode_and_regs: u32, imm: i32) -> u32 {
1020    let v = imm as u32;
1021    let b12 = (v >> 12) & 0x1;
1022    let b11 = (v >> 11) & 0x1;
1023    let b10_5 = (v >> 5) & 0x3F;
1024    let b4_1 = (v >> 1) & 0xF;
1025    // Clear the imm-bearing bits, then OR in the new ones.
1026    let cleared = opcode_and_regs & 0x01FF_F07F;
1027    cleared | (b12 << 31) | (b10_5 << 25) | (b4_1 << 8) | (b11 << 7)
1028}
1029
1030/// Walk the code and inject a `fallthrough` (4 bytes) before every
1031/// JAL / branch target that isn't already preceded by a terminator
1032/// instruction. After injection, all reachable static targets are
1033/// guaranteed to be in the strict bb_starts set the predecode computes.
1034///
1035/// Mutates `code` in place. Returns `old_pc → new_pc` map so the
1036/// caller can remap PC values stored elsewhere (endpoint entries,
1037/// `.rodata` code-pointers) consistently.
1038///
1039/// `extra_targets` lets the caller mark additional PCs (e.g. endpoint
1040/// entries, `.rodata` code-pointer targets) as required bb_starts so
1041/// they get fallthrough injection too.
1042fn align_branch_targets(
1043    code: &mut Vec<u8>,
1044    extra_targets: &[usize],
1045) -> Result<BTreeMap<usize, usize>, TranspileError> {
1046    // ---- Pass 1: scan instructions, identify terminators by PC ----
1047    // We need to know which PCs follow a terminator (= legitimate
1048    // bb_starts) so we can skip injection where it isn't needed.
1049
1050    // Decode each instruction at its byte offset; record:
1051    //  - The set of all instruction-start byte offsets (`inst_starts`).
1052    //  - The set of terminator instruction END offsets (their next_pc).
1053    //  - The list of (branch_or_jal_pc, target_pc) static edges.
1054    let n = code.len();
1055    let mut inst_starts: Vec<usize> = Vec::with_capacity(n / 4);
1056    let mut post_terminator: std::collections::HashSet<usize> = std::collections::HashSet::new();
1057    post_terminator.insert(0); // PC=0 is always a bb_start.
1058    let mut static_edges: Vec<(usize, usize)> = Vec::new(); // (instruction_pc, target_pc)
1059
1060    let mut pc: usize = 0;
1061    while pc < n {
1062        inst_starts.push(pc);
1063        let lo = u16::from_le_bytes([code[pc], code[pc + 1]]);
1064        let inst_len: usize;
1065        let is_terminator: bool;
1066        let target: Option<i64>;
1067        if lo & 0b11 != 0b11 {
1068            // Compressed (2 bytes).
1069            inst_len = 2;
1070            // RVC encodings that are terminators in PVM2:
1071            //   c.j imm    (op=01, f3=101)        — static jump
1072            //   c.beqz / c.bnez (op=01, f3=110/111) — conditional branches
1073            //   c.jr (op=10, f3=100) — `jalr x0, rs1, 0` (return /
1074            //     indirect jump): a terminator. c.jalr is a call (also a
1075            //     terminator); c.ebreak is Reserved (panics, terminator).
1076            //   c.illegal  (= 0x0000)             — reserved (terminator)
1077            // Other RVC ops are non-terminators.
1078            let op = lo & 0b11;
1079            let f3 = (lo >> 13) & 0b111;
1080            if lo == 0 {
1081                // c.illegal: terminator.
1082                is_terminator = true;
1083                target = None;
1084            } else if op == 0b01 && f3 == 0b101 {
1085                // c.j imm — terminator, has a static target.
1086                let imm = decompress_cj_imm(lo);
1087                is_terminator = true;
1088                target = Some(pc as i64 + imm as i64);
1089            } else if op == 0b01 && (f3 == 0b110 || f3 == 0b111) {
1090                // c.beqz / c.bnez — terminators with static targets.
1091                let imm = decompress_cb_imm(lo);
1092                is_terminator = true;
1093                target = Some(pc as i64 + imm as i64);
1094            } else if op == 0b10 && f3 == 0b100 {
1095                // (op=10, f3=100) family. Discriminate by bit12 / rdrs1 / rs2:
1096                //   (0, r, 0) r!=0  → c.jr     (= retf, terminator)
1097                //   (0, r, s) both!=0 → c.mv  (NOT a terminator)
1098                //   (1, 0, 0)        → c.ebreak (Reserved, terminator)
1099                //   (1, r, 0) r!=0   → c.jalr (= callf, terminator)
1100                //   (1, r, s) both!=0 → c.add (NOT a terminator)
1101                let bit12 = (lo >> 12) & 1;
1102                let rdrs1 = (lo >> 7) & 0x1F;
1103                let rs2 = (lo >> 2) & 0x1F;
1104                // c.jr (bit12=0, rdrs1!=0, rs2=0)
1105                // c.ebreak (bit12=1, rdrs1=0, rs2=0)
1106                // c.jalr (bit12=1, rdrs1!=0, rs2=0)
1107                let is_jr_like = rs2 == 0 && (bit12 == 1 || rdrs1 != 0);
1108                is_terminator = is_jr_like;
1109                target = None;
1110            } else {
1111                is_terminator = false;
1112                target = None;
1113            }
1114        } else {
1115            // 4-byte instruction.
1116            if pc + 4 > n {
1117                break;
1118            }
1119            inst_len = 4;
1120            let w = u32::from_le_bytes([code[pc], code[pc + 1], code[pc + 2], code[pc + 3]]);
1121            let opcode = w & 0x7F;
1122            let funct3 = (w >> 12) & 0x7;
1123            match opcode {
1124                OP_JAL => {
1125                    let imm = imm_j(w);
1126                    is_terminator = true;
1127                    target = Some(pc as i64 + imm as i64);
1128                }
1129                OP_JALR => {
1130                    // jalr — return / indirect call. A terminator; its
1131                    // successor comes via the runtime dispatch table, not
1132                    // a static immediate.
1133                    is_terminator = true;
1134                    target = None;
1135                }
1136                0b110_0011 => {
1137                    // B-type branch (BEQ/BNE/etc.).
1138                    let imm = imm_b(w);
1139                    is_terminator = true;
1140                    target = Some(pc as i64 + imm as i64);
1141                }
1142                OP_CUSTOM_0 => {
1143                    // trap / ecalli / ecall.jar / fallthrough — all
1144                    // terminators with no statically-embedded successor.
1145                    is_terminator = true;
1146                    target = None;
1147                    let _ = funct3;
1148                }
1149                _ => {
1150                    is_terminator = false;
1151                    target = None;
1152                }
1153            }
1154        }
1155        let next_pc = pc + inst_len;
1156        if is_terminator && next_pc < n {
1157            post_terminator.insert(next_pc);
1158        }
1159        if let Some(t) = target
1160            && t >= 0
1161            && (t as usize) < n
1162        {
1163            static_edges.push((pc, t as usize));
1164        }
1165        pc = next_pc;
1166    }
1167
1168    // ---- Pass 2: identify targets needing fallthrough injection ----
1169    let inst_starts_set: std::collections::HashSet<usize> = inst_starts.iter().copied().collect();
1170    let mut needs_inject: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
1171    for &(_, target) in &static_edges {
1172        if !post_terminator.contains(&target) && inst_starts_set.contains(&target) {
1173            needs_inject.insert(target);
1174        }
1175    }
1176    // Also: endpoint entries / .rodata code-pointer targets — any PC
1177    // the host (or future indirect-dispatch lowering) might enter at.
1178    // The caller passes these via `extra_targets`.
1179    for &t in extra_targets {
1180        if t < n && !post_terminator.contains(&t) && inst_starts_set.contains(&t) {
1181            needs_inject.insert(t);
1182        }
1183    }
1184
1185    if needs_inject.is_empty() {
1186        // No injections needed — old offsets are identity.
1187        let identity: BTreeMap<usize, usize> = inst_starts.into_iter().map(|p| (p, p)).collect();
1188        return Ok(identity);
1189    }
1190
1191    // ---- Pass 3: build new code with fallthrough injected ----
1192    let new_len = n + needs_inject.len() * 4;
1193    let mut new_code: Vec<u8> = Vec::with_capacity(new_len);
1194    // old_pc → new_pc (only for old instruction-start positions; mid-
1195    // instruction bytes don't get mapped).
1196    let mut offset_map: BTreeMap<usize, usize> = BTreeMap::new();
1197    let fallthrough_word = encode_custom0_fallthrough();
1198    let fallthrough_bytes = fallthrough_word.to_le_bytes();
1199
1200    let mut next_inject_iter = needs_inject.iter().peekable();
1201    let mut old_idx = 0;
1202    while old_idx < inst_starts.len() {
1203        let old_pc = inst_starts[old_idx];
1204        // If any injection is scheduled at this old_pc, emit fallthrough first.
1205        while let Some(&&inject_pc) = next_inject_iter.peek() {
1206            if inject_pc == old_pc {
1207                new_code.extend_from_slice(&fallthrough_bytes);
1208                next_inject_iter.next();
1209            } else {
1210                break;
1211            }
1212        }
1213        offset_map.insert(old_pc, new_code.len());
1214        let next_pc = inst_starts.get(old_idx + 1).copied().unwrap_or(n);
1215        let inst_len = next_pc - old_pc;
1216        new_code.extend_from_slice(&code[old_pc..old_pc + inst_len]);
1217        old_idx += 1;
1218    }
1219
1220    // ---- Pass 4: re-encode branch / jal / callf offsets in new_code ----
1221    // Iterate over OLD instruction starts (not new_code) so we never
1222    // encounter the injected fallthrough instructions during this pass.
1223    for &old_pc in &inst_starts {
1224        let new_pc = offset_map[&old_pc];
1225        let lo = u16::from_le_bytes([new_code[new_pc], new_code[new_pc + 1]]);
1226        if lo & 0b11 != 0b11 {
1227            // RVC. c.j and c.beqz/c.bnez have static targets.
1228            let op = lo & 0b11;
1229            let f3 = (lo >> 13) & 0b111;
1230            if op == 0b01 && f3 == 0b101 {
1231                let old_imm = decompress_cj_imm(lo);
1232                let old_target = (old_pc as i64 + old_imm as i64) as usize;
1233                let new_target = *offset_map.get(&old_target).ok_or_else(|| {
1234                    TranspileError::InvalidSection(format!(
1235                        "align_branch_targets: c.j old target {:#x} not in offset_map",
1236                        old_target
1237                    ))
1238                })?;
1239                let new_imm = new_target as i64 - new_pc as i64;
1240                if new_imm != old_imm as i64 {
1241                    let new_h = encode_cj_imm(lo, new_imm as i32).ok_or_else(|| {
1242                        TranspileError::InvalidSection(format!(
1243                            "align_branch_targets: c.j at new_pc {:#x} new_imm {} \
1244                             out of ±2 KiB range",
1245                            new_pc, new_imm
1246                        ))
1247                    })?;
1248                    new_code[new_pc..new_pc + 2].copy_from_slice(&new_h.to_le_bytes());
1249                }
1250            } else if op == 0b01 && (f3 == 0b110 || f3 == 0b111) {
1251                let old_imm = decompress_cb_imm(lo);
1252                let old_target = (old_pc as i64 + old_imm as i64) as usize;
1253                let new_target = *offset_map.get(&old_target).ok_or_else(|| {
1254                    TranspileError::InvalidSection(format!(
1255                        "align_branch_targets: c.beqz/c.bnez old target {:#x} not in offset_map",
1256                        old_target
1257                    ))
1258                })?;
1259                let new_imm = new_target as i64 - new_pc as i64;
1260                if new_imm != old_imm as i64 {
1261                    let new_h = encode_cb_imm(lo, new_imm as i32).ok_or_else(|| {
1262                        TranspileError::InvalidSection(format!(
1263                            "align_branch_targets: c.beqz/c.bnez at new_pc {:#x} new_imm {} \
1264                             out of ±256 byte range",
1265                            new_pc, new_imm
1266                        ))
1267                    })?;
1268                    new_code[new_pc..new_pc + 2].copy_from_slice(&new_h.to_le_bytes());
1269                }
1270            }
1271        } else {
1272            let w = u32::from_le_bytes([
1273                new_code[new_pc],
1274                new_code[new_pc + 1],
1275                new_code[new_pc + 2],
1276                new_code[new_pc + 3],
1277            ]);
1278            let opcode = w & 0x7F;
1279            match opcode {
1280                OP_JAL => {
1281                    let old_imm = imm_j(w);
1282                    let old_target = (old_pc as i64 + old_imm as i64) as usize;
1283                    if let Some(&new_target) = offset_map.get(&old_target) {
1284                        let new_imm = new_target as i64 - new_pc as i64;
1285                        if !(-(1 << 20)..(1 << 20)).contains(&new_imm) {
1286                            return Err(TranspileError::InvalidSection(format!(
1287                                "align_branch_targets: JAL at new_pc {:#x} out of ±1 MiB \
1288                                 range after injection (new_imm = {})",
1289                                new_pc, new_imm
1290                            )));
1291                        }
1292                        let rd = (w >> 7) & 0x1F;
1293                        let v = new_imm as u32;
1294                        let b20 = (v >> 20) & 0x1;
1295                        let b10_1 = (v >> 1) & 0x3FF;
1296                        let b11 = (v >> 11) & 0x1;
1297                        let b19_12 = (v >> 12) & 0xFF;
1298                        let imm_field = (b20 << 31) | (b10_1 << 21) | (b11 << 20) | (b19_12 << 12);
1299                        let new_w = imm_field | (rd << 7) | OP_JAL;
1300                        new_code[new_pc..new_pc + 4].copy_from_slice(&new_w.to_le_bytes());
1301                    }
1302                }
1303                0b110_0011 => {
1304                    let old_imm = imm_b(w);
1305                    let old_target = (old_pc as i64 + old_imm as i64) as usize;
1306                    if let Some(&new_target) = offset_map.get(&old_target) {
1307                        let new_imm = new_target as i64 - new_pc as i64;
1308                        if !(-(1 << 12)..(1 << 12)).contains(&new_imm) {
1309                            return Err(TranspileError::InvalidSection(format!(
1310                                "align_branch_targets: B-type branch at new_pc {:#x} out of ±4 KiB \
1311                                 range after injection (new_imm = {})",
1312                                new_pc, new_imm
1313                            )));
1314                        }
1315                        let new_w = encode_b_imm(w, new_imm as i32);
1316                        new_code[new_pc..new_pc + 4].copy_from_slice(&new_w.to_le_bytes());
1317                    }
1318                }
1319                _ => {}
1320            }
1321        }
1322    }
1323
1324    *code = new_code;
1325    Ok(offset_map)
1326}
1327
1328/// Decompress a c.j (compressed jump) into a signed byte offset.
1329/// CJ-type immediate encoding (RV unprivileged spec).
1330fn decompress_cj_imm(h: u16) -> i32 {
1331    let h = h as u32;
1332    let b11 = (h >> 12) & 0x1;
1333    let b4 = (h >> 11) & 0x1;
1334    let b9_8 = (h >> 9) & 0x3;
1335    let b10 = (h >> 8) & 0x1;
1336    let b6 = (h >> 7) & 0x1;
1337    let b7 = (h >> 6) & 0x1;
1338    let b3_1 = (h >> 3) & 0x7;
1339    let b5 = (h >> 2) & 0x1;
1340    let raw = (b11 << 11)
1341        | (b10 << 10)
1342        | (b9_8 << 8)
1343        | (b7 << 7)
1344        | (b6 << 6)
1345        | (b5 << 5)
1346        | (b4 << 4)
1347        | (b3_1 << 1);
1348    ((raw as i32) << 20) >> 20
1349}
1350
1351/// Decompress a c.beqz / c.bnez (compressed branch) into a signed byte offset.
1352fn decompress_cb_imm(h: u16) -> i32 {
1353    let h = h as u32;
1354    let b8 = (h >> 12) & 0x1;
1355    let b4_3 = (h >> 10) & 0x3;
1356    let b7_6 = (h >> 5) & 0x3;
1357    let b2_1 = (h >> 3) & 0x3;
1358    let b5 = (h >> 2) & 0x1;
1359    let raw = (b8 << 8) | (b7_6 << 6) | (b5 << 5) | (b4_3 << 3) | (b2_1 << 1);
1360    ((raw as i32) << 23) >> 23
1361}
1362
1363/// Encode a new imm into a c.beqz / c.bnez instruction, preserving
1364/// funct3 / rs1' / opcode fields. `imm` must fit in 9 bits signed
1365/// (range ±256 bytes); returns None on overflow.
1366fn encode_cb_imm(h: u16, imm: i32) -> Option<u16> {
1367    if !(-(1 << 8)..(1 << 8)).contains(&imm) {
1368        return None;
1369    }
1370    if imm & 1 != 0 {
1371        return None;
1372    }
1373    let v = imm as u32;
1374    let b8 = (v >> 8) & 0x1;
1375    let b7_6 = (v >> 6) & 0x3;
1376    let b5 = (v >> 5) & 0x1;
1377    let b4_3 = (v >> 3) & 0x3;
1378    let b2_1 = (v >> 1) & 0x3;
1379    // Preserve: bits 15:13 (funct3), bits 9:7 (rs1'), bits 1:0 (opcode).
1380    let preserved = (h as u32) & 0b1110_0011_1000_0011;
1381    let new_imm = (b8 << 12) | (b4_3 << 10) | (b7_6 << 5) | (b2_1 << 3) | (b5 << 2);
1382    Some((preserved | new_imm) as u16)
1383}
1384
1385/// Encode a new imm into a c.j instruction, preserving funct3 / opcode.
1386/// `imm` must fit in 12 bits signed (range ±2 KiB); returns None on overflow.
1387fn encode_cj_imm(h: u16, imm: i32) -> Option<u16> {
1388    if !(-(1 << 11)..(1 << 11)).contains(&imm) {
1389        return None;
1390    }
1391    if imm & 1 != 0 {
1392        return None;
1393    }
1394    let v = imm as u32;
1395    let b11 = (v >> 11) & 0x1;
1396    let b10 = (v >> 10) & 0x1;
1397    let b9_8 = (v >> 8) & 0x3;
1398    let b7 = (v >> 7) & 0x1;
1399    let b6 = (v >> 6) & 0x1;
1400    let b5 = (v >> 5) & 0x1;
1401    let b4 = (v >> 4) & 0x1;
1402    let b3_1 = (v >> 1) & 0x7;
1403    // Preserve: bits 15:13 (funct3), bits 1:0 (opcode).
1404    let preserved = (h as u32) & 0b1110_0000_0000_0011;
1405    let new_imm = (b11 << 12)
1406        | (b4 << 11)
1407        | (b9_8 << 9)
1408        | (b10 << 8)
1409        | (b6 << 7)
1410        | (b7 << 6)
1411        | (b3_1 << 3)
1412        | (b5 << 2);
1413    Some((preserved | new_imm) as u16)
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418    use super::*;
1419
1420    #[test]
1421    fn nop_encoding_matches_addi_x0_x0_0() {
1422        let w = u32::from_le_bytes(NOP_BYTES);
1423        assert_eq!(w & 0x7F, OP_OP_IMM, "opcode must be OP-IMM");
1424        assert_eq!((w >> 7) & 0x1F, 0, "rd must be x0");
1425        assert_eq!((w >> 15) & 0x1F, 0, "rs1 must be x0");
1426        assert_eq!((w >> 20) & 0xFFF, 0, "imm must be 0");
1427        assert_eq!((w >> 12) & 0x7, 0, "funct3 must be 0 (ADDI)");
1428    }
1429
1430    #[test]
1431    fn custom0_ecall_jar_decodes() {
1432        let w = encode_custom0_ecall_jar();
1433        assert_eq!(w & 0x7F, OP_CUSTOM_0);
1434        assert_eq!((w >> 12) & 0x7, 0b001);
1435        // Other fields zero.
1436        assert_eq!((w >> 7) & 0x1F, 0);
1437        assert_eq!((w >> 15) & 0x1F, 0);
1438    }
1439
1440    #[test]
1441    fn custom0_ecalli_decodes() {
1442        let w = encode_custom0_ecalli(42);
1443        assert_eq!(w & 0x7F, OP_CUSTOM_0);
1444        assert_eq!((w >> 12) & 0x7, 0b010);
1445        assert_eq!((w >> 20) & 0xFFF, 42);
1446    }
1447
1448    #[test]
1449    fn custom0_fallthrough_decodes() {
1450        let w = encode_custom0_fallthrough();
1451        assert_eq!(w & 0x7F, OP_CUSTOM_0);
1452        assert_eq!((w >> 12) & 0x7, 0b100);
1453    }
1454
1455    #[test]
1456    fn cb_imm_round_trips() {
1457        // (op=01, f3=110 = beqz, rs1'=8, imm=0 placeholder) — start with a real beqz.
1458        // c.beqz x8 (rs1'=0), imm=0: f3=110, op=01, rs1'=0, all imm=0.
1459        let base = (0b110u16 << 13) | (0b01u16);
1460        for &imm in &[0, 2, -2, 4, -4, 128, -128, 254, -256] {
1461            let h = encode_cb_imm(base, imm).expect("in range");
1462            assert_eq!(
1463                decompress_cb_imm(h),
1464                imm,
1465                "round-trip failed for imm={}",
1466                imm
1467            );
1468        }
1469        assert!(encode_cb_imm(base, 256).is_none());
1470        assert!(encode_cb_imm(base, -258).is_none());
1471    }
1472
1473    #[test]
1474    fn cj_imm_round_trips() {
1475        // c.j with f3=101, op=01.
1476        let base = (0b101u16 << 13) | (0b01u16);
1477        for &imm in &[0, 2, -2, 4, -4, 512, -512, 2046, -2048] {
1478            let h = encode_cj_imm(base, imm).expect("in range");
1479            assert_eq!(
1480                decompress_cj_imm(h),
1481                imm,
1482                "round-trip failed for imm={}",
1483                imm
1484            );
1485        }
1486        assert!(encode_cj_imm(base, 2048).is_none());
1487    }
1488
1489    #[test]
1490    fn rewrite_ecall_marker_jar() {
1491        // CSRRW x0, 0x800, x0 = csr=0x800, rs1=0, funct3=1, rd=0, op=SYSTEM
1492        let csrrw = (0x800u32 << 20) | (0b001 << 12) | OP_SYSTEM;
1493        let ecall: u32 = 0x0000_0073;
1494        let mut code = Vec::new();
1495        code.extend_from_slice(&csrrw.to_le_bytes());
1496        code.extend_from_slice(&ecall.to_le_bytes());
1497        rewrite_ecall_markers(&mut code).unwrap();
1498        let w0 = u32::from_le_bytes(code[0..4].try_into().unwrap());
1499        assert_eq!(w0, u32::from_le_bytes(NOP_BYTES));
1500        let w1 = u32::from_le_bytes(code[4..8].try_into().unwrap());
1501        assert_eq!(w1, encode_custom0_ecall_jar());
1502    }
1503
1504    #[test]
1505    fn rewrite_ecall_marker_ecalli() {
1506        let csrrw = (0x801u32 << 20) | (0b001 << 12) | OP_SYSTEM;
1507        let ecall: u32 = 0x0000_0073;
1508        let mut code = Vec::new();
1509        code.extend_from_slice(&csrrw.to_le_bytes());
1510        code.extend_from_slice(&ecall.to_le_bytes());
1511        rewrite_ecall_markers(&mut code).unwrap();
1512        let w1 = u32::from_le_bytes(code[4..8].try_into().unwrap());
1513        assert_eq!(w1, encode_custom0_ecalli(0));
1514    }
1515
1516    #[test]
1517    fn validate_accepts_auipc_and_jalr() {
1518        // PVM2 now uses native RISC-V control flow: AUIPC computes a
1519        // code VA, JALR jumps to it (validated against bb_starts at
1520        // runtime). Both are accepted by the linker.
1521        let auipc = (0x1000u32 << 12) | (1 << 7) | OP_AUIPC; // auipc x1, 0x1000
1522        validate_pvm2(&auipc.to_le_bytes()).unwrap();
1523        let jalr = (1u32 << 15) | (1 << 7) | OP_JALR; // jalr x1, x1, 0
1524        validate_pvm2(&jalr.to_le_bytes()).unwrap();
1525        // The producer ABI still forbids x3/x4 even though the runtime
1526        // executes them through the spilled-register path.
1527        let jalr_x3 = (3u32 << 15) | (1 << 7) | OP_JALR; // jalr x1, x3, 0
1528        assert!(validate_pvm2(&jalr_x3.to_le_bytes()).is_err());
1529    }
1530
1531    #[test]
1532    fn validate_rejects_standard_ecall() {
1533        let code = 0x0000_0073u32.to_le_bytes().to_vec();
1534        let err = validate_pvm2(&code).unwrap_err();
1535        assert!(matches!(err, TranspileError::InvalidSection(_)));
1536    }
1537
1538    #[test]
1539    fn validate_rejects_producer_x3_use() {
1540        // addi x3, x0, 0  (rd=3, rs1=0, imm=0, funct3=0, op=OP-IMM)
1541        let w = (3u32 << 7) | OP_OP_IMM;
1542        let code = w.to_le_bytes().to_vec();
1543        let err = validate_pvm2(&code).unwrap_err();
1544        let TranspileError::InvalidSection(msg) = err else {
1545            panic!();
1546        };
1547        assert!(msg.contains("x3"));
1548    }
1549
1550    #[test]
1551    fn validate_accepts_clean_addi() {
1552        // addi x1, x0, 5  (rd=1, rs1=0, imm=5, funct3=0, op=OP-IMM)
1553        let w = (5u32 << 20) | (1 << 7) | OP_OP_IMM;
1554        let code = w.to_le_bytes().to_vec();
1555        validate_pvm2(&code).unwrap();
1556    }
1557
1558    #[test]
1559    fn validate_accepts_rvc() {
1560        // c.li x10, 5 = 0x4515 (h = 0x4515, low 2 bits = 01, RVC)
1561        let cli = 0x4515u16;
1562        let code = cli.to_le_bytes().to_vec();
1563        validate_pvm2(&code).unwrap();
1564    }
1565}