Skip to main content

asm_rs/
linker.rs

1//! Label resolution, branch relaxation, and final layout.
2//!
3//! The linker collects fragments (fixed code/data, alignment padding, and
4//! relaxable branches), resolves labels, performs Szymanski-style branch
5//! relaxation (monotonic growth, guaranteed convergence), applies
6//! relocations, and emits the final machine code.
7
8use alloc::collections::BTreeMap;
9#[allow(unused_imports)]
10use alloc::format;
11use alloc::string::String;
12use alloc::string::ToString;
13#[allow(unused_imports)]
14use alloc::vec;
15use alloc::vec::Vec;
16
17use crate::encoder::{InstrBytes, RelaxInfo, RelocKind, Relocation};
18use crate::error::{AsmError, Span};
19
20// ─── FragmentBytes ─────────────────────────────────────────
21
22/// Compact byte storage for fragment payloads.
23///
24/// Instructions are stored inline as [`InstrBytes`], whose 32-byte capacity
25/// covers every encodable instruction on every supported target — zero heap
26/// allocations on the hot encoding path.  Data directives, which have no such
27/// bound, fall back to a heap-allocated `Vec<u8>`.
28#[derive(Debug, Clone)]
29pub enum FragmentBytes {
30    /// Inline storage — no heap allocation.
31    Inline(InstrBytes),
32    /// Heap-allocated storage for data of unbounded size.
33    Heap(Vec<u8>),
34}
35
36impl core::ops::Deref for FragmentBytes {
37    type Target = [u8];
38    #[inline]
39    fn deref(&self) -> &[u8] {
40        match self {
41            FragmentBytes::Inline(ib) => ib,
42            FragmentBytes::Heap(v) => v,
43        }
44    }
45}
46
47impl core::ops::DerefMut for FragmentBytes {
48    #[inline]
49    fn deref_mut(&mut self) -> &mut [u8] {
50        match self {
51            FragmentBytes::Inline(ib) => ib,
52            FragmentBytes::Heap(v) => v,
53        }
54    }
55}
56
57/// Maximum number of relaxation iterations before giving up.
58const MAX_RELAXATION_ITERS: usize = 100;
59
60/// Read a little-endian u32 from `bytes` at `offset`, with bounds checking.
61/// Returns a descriptive error instead of panicking on out-of-bounds access.
62#[cfg(any(feature = "arm", feature = "aarch64", feature = "riscv"))]
63fn read_le32(bytes: &[u8], offset: usize, label: &str, span: Span) -> Result<u32, AsmError> {
64    if offset + 4 > bytes.len() {
65        return Err(AsmError::Syntax {
66            msg: alloc::format!(
67                "relocation offset {offset} out of bounds (buffer len {}) for label '{label}'",
68                bytes.len()
69            ),
70            span,
71        });
72    }
73    // The bounds check above guarantees the slice is exactly 4 bytes,
74    // so the try_into conversion is infallible.
75    let arr: [u8; 4] = match bytes[offset..offset + 4].try_into() {
76        Ok(a) => a,
77        Err(_) => {
78            return Err(AsmError::Syntax {
79                msg: alloc::format!(
80                    "relocation offset {offset} out of bounds (buffer len {}) for label '{label}'",
81                    bytes.len()
82                ),
83                span,
84            });
85        }
86    };
87    Ok(u32::from_le_bytes(arr))
88}
89
90/// Read a little-endian u16 from `bytes` at `offset`, with bounds checking.
91#[cfg(any(feature = "arm", feature = "riscv"))]
92fn read_le16(bytes: &[u8], offset: usize, label: &str, span: Span) -> Result<u16, AsmError> {
93    if offset + 2 > bytes.len() {
94        return Err(AsmError::Syntax {
95            msg: alloc::format!(
96                "relocation offset {offset} out of bounds (buffer len {}) for label '{label}'",
97                bytes.len()
98            ),
99            span,
100        });
101    }
102    let arr: [u8; 2] = match bytes[offset..offset + 2].try_into() {
103        Ok(a) => a,
104        Err(_) => {
105            return Err(AsmError::Syntax {
106                msg: alloc::format!(
107                    "relocation offset {offset} out of bounds (buffer len {}) for label '{label}'",
108                    bytes.len()
109                ),
110                span,
111            });
112        }
113    };
114    Ok(u16::from_le_bytes(arr))
115}
116
117/// Check that a PC-relative displacement is representable at the granularity
118/// its encoding scales by, and return it shifted.
119///
120/// Branch encodings store their displacement pre-scaled — AArch64 and ARM by
121/// 4, Thumb and RISC-V by 2 — so the low bits of a misaligned target simply
122/// have nowhere to go. Shifting them off produces a perfectly well-formed
123/// instruction that branches to the wrong address, which is exactly the kind
124/// of failure a decoder will not flag. GNU `as` diagnoses it, and so does this.
125#[cfg(any(feature = "arm", feature = "aarch64", feature = "riscv"))]
126fn scaled_displacement(rel: i64, alignment: u8, label: &str, span: Span) -> Result<i64, AsmError> {
127    let mask = i64::from(alignment - 1);
128    if rel & mask != 0 {
129        return Err(AsmError::MisalignedBranchTarget {
130            label: String::from(label),
131            disp: rel,
132            alignment,
133            span,
134        });
135    }
136    Ok(rel >> alignment.trailing_zeros())
137}
138
139/// Split a halfword-scaled branch offset into the `S`, `J1`, `J2`, `imm10`,
140/// `imm11` fields shared by the Thumb-2 `BL` (encoding T1) and `B.W`
141/// (encoding T4) instructions.
142///
143/// Per the Arm ARM, both encodings reconstruct the offset as
144/// `SignExtend(S:I1:I2:imm10:imm11:'0')` with
145/// `I1 = NOT(J1 EOR S)` and `I2 = NOT(J2 EOR S)`.
146/// Inverting that gives `J1 = NOT(I1 EOR S)` and `J2 = NOT(I2 EOR S)`,
147/// where — for a 24-bit signed `offset` — `S` is bit 23, `I1` is bit 22 and
148/// `I2` is bit 21.
149///
150/// Getting the `I1`/`I2` bit positions wrong only shows up for targets more
151/// than ±4 MiB away (closer targets have `I1 == I2 == S` by sign extension),
152/// so this is covered by a dedicated far-branch test.
153#[cfg(feature = "arm")]
154fn thumb_t1_t4_fields(offset: i64) -> (u16, u16, u16, u16, u16) {
155    let imm = offset as u32;
156    let s = ((imm >> 23) & 1) as u16;
157    let i1 = (imm >> 22) & 1;
158    let i2 = (imm >> 21) & 1;
159    let j1 = (!(i1 ^ u32::from(s)) & 1) as u16;
160    let j2 = (!(i2 ^ u32::from(s)) & 1) as u16;
161    let imm10 = ((imm >> 11) & 0x3FF) as u16;
162    let imm11 = (imm & 0x7FF) as u16;
163    (s, j1, j2, imm10, imm11)
164}
165
166/// ARM modified-immediate encoder for the linker: find (imm8, rot) such that
167/// `value == imm8.rotate_right(rot * 2)`.
168#[cfg(feature = "arm")]
169fn encode_arm_imm_for_linker(value: u32) -> Option<(u8, u8)> {
170    for rot in 0..16u8 {
171        let shift = rot * 2;
172        let rotated = value.rotate_left(shift as u32);
173        if rotated <= 0xFF {
174            return Some((rotated as u8, rot));
175        }
176    }
177    None
178}
179
180/// The resolved output: (machine code bytes, label→address table, applied relocations, fragment offsets).
181type ResolveOutput = (
182    Vec<u8>,
183    Vec<(String, u64)>,
184    Vec<AppliedRelocation>,
185    Vec<u64>,
186);
187
188/// An applied relocation in the final output — describes where a label
189/// reference was patched. Useful for tooling, debugging, and re-linking.
190#[derive(Debug, Clone, PartialEq, Eq)]
191#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
192pub struct AppliedRelocation {
193    /// Offset in the output byte stream where the value was written.
194    pub offset: usize,
195    /// Size of the relocated value in bytes (1, 2, 4, or 8).
196    pub size: u8,
197    /// Target label name.
198    pub label: String,
199    /// How the linker patches the target address into the instruction bytes.
200    pub kind: RelocKind,
201    /// Addend.
202    pub addend: i64,
203}
204
205// ─── Fragment ──────────────────────────────────────────────
206
207/// A fragment of assembled output.
208///
209/// The linker operates on an ordered list of fragments.  During branch
210/// relaxation the sizes of [`Fragment::Relaxable`] and [`Fragment::Align`]
211/// fragments may change, but they only ever *grow* (monotonic), which
212/// guarantees convergence.
213#[derive(Debug, Clone)]
214pub enum Fragment {
215    /// Fixed-size bytes with optional relocation.
216    Fixed {
217        /// The raw assembled bytes (inline for instructions, heap for large data).
218        bytes: FragmentBytes,
219        /// Optional relocation to apply to these bytes.
220        relocation: Option<Relocation>,
221        /// Source span of the originating instruction or directive.
222        span: Span,
223    },
224    /// Alignment padding — size depends on preceding layout.
225    ///
226    /// When `use_nop` is true (x86/x86-64 code alignment with no explicit
227    /// fill byte), pad with Intel-recommended multi-byte NOP sequences
228    /// instead of repeating a single fill byte.
229    Align {
230        /// Required byte alignment (must be a power of two).
231        alignment: u32,
232        /// Byte value used for padding when `use_nop` is false.
233        fill: u8,
234        /// If set, skip this alignment entirely when the required padding
235        /// exceeds this many bytes.
236        max_skip: Option<u32>,
237        /// Use multi-byte NOP sequences instead of repeating `fill`.
238        use_nop: bool,
239        /// Source span of the alignment directive.
240        span: Span,
241    },
242    /// A relaxable branch instruction.
243    ///
244    /// Starts in short form (rel8).  If the linker determines the target is
245    /// beyond ±127 bytes it promotes to the long form (rel32) and re-lays
246    /// out.  Promotion is irreversible (Szymanski monotonic growth).
247    Relaxable {
248        /// Short-form bytes (e.g. `[0xEB, 0x00]` for JMP rel8).
249        short_bytes: InstrBytes,
250        /// Offset of the rel8 displacement within `short_bytes`.
251        short_reloc_offset: usize,
252        /// Optional relocation for the short form.  When `Some`, the linker
253        /// applies this relocation to `short_bytes` instead of raw byte-patching.
254        /// Used for RISC-V B-type branches.
255        short_relocation: Option<Relocation>,
256        /// Long-form bytes (e.g. `[0xE9, 0,0,0,0]` for JMP rel32).
257        long_bytes: InstrBytes,
258        /// Relocation for the long form (contains label, offset, etc.).
259        long_relocation: Relocation,
260        /// Whether this fragment has been promoted to long form.
261        is_long: bool,
262        /// Source span.
263        span: Span,
264    },
265    /// Advance the location counter to an absolute address, padding with
266    /// `fill` bytes.  If the target is behind the current position, an
267    /// error is raised during emission.
268    Org {
269        /// Absolute target address for the location counter.
270        target: u64,
271        /// Byte value used to fill the gap.
272        fill: u8,
273        /// Source span of the `.org` directive.
274        span: Span,
275    },
276}
277
278// ─── Linker internals ──────────────────────────────────────
279
280/// A label definition tracking which fragment it precedes.
281#[derive(Debug, Clone)]
282struct LabelDef {
283    fragment_index: usize,
284    span: Span,
285}
286
287/// Numeric label tracking (supports forward/backward references like `1:` / `1b` / `1f`).
288#[derive(Debug, Clone, Default)]
289struct NumericLabels {
290    defs: BTreeMap<u32, Vec<usize>>,
291}
292
293// ─── Public API ────────────────────────────────────────────
294
295/// The linker: collects fragments and labels, resolves everything.
296#[derive(Debug)]
297pub struct Linker {
298    fragments: Vec<Fragment>,
299    labels: BTreeMap<String, LabelDef>,
300    externals: BTreeMap<String, u64>,
301    numeric: NumericLabels,
302    constants: BTreeMap<String, i128>,
303    base_address: u64,
304    /// Set once `resolve()` has patched relocations into the fragments.
305    resolved: bool,
306    /// Hard ceiling on the laid-out image size.
307    ///
308    /// `.org` and `.align` can request an arbitrary amount of padding that is
309    /// not proportional to the source text, so the ceiling is enforced during
310    /// *layout* — before any buffer is reserved — rather than after the bytes
311    /// have already been materialised.
312    max_output_bytes: usize,
313}
314
315impl Default for Linker {
316    fn default() -> Self {
317        Self::new()
318    }
319}
320
321impl Linker {
322    /// Create a new, empty linker with base address 0.
323    pub fn new() -> Self {
324        Self {
325            fragments: Vec::new(),
326            labels: BTreeMap::new(),
327            externals: BTreeMap::new(),
328            numeric: NumericLabels::default(),
329            constants: BTreeMap::new(),
330            base_address: 0,
331            resolved: false,
332            max_output_bytes: usize::MAX,
333        }
334    }
335
336    /// Set the hard ceiling on the laid-out image size, in bytes.
337    ///
338    /// Exceeding it during layout aborts with
339    /// [`AsmError::ResourceLimitExceeded`] before any output buffer is
340    /// reserved.
341    pub fn set_max_output_bytes(&mut self, max: usize) {
342        self.max_output_bytes = max;
343    }
344
345    /// Set the base (origin) address for the assembled output.
346    pub fn set_base_address(&mut self, addr: u64) {
347        self.base_address = addr;
348    }
349
350    /// Get the base address.
351    pub fn base_address(&self) -> u64 {
352        self.base_address
353    }
354
355    /// The number of fragments currently added.
356    pub fn fragment_count(&self) -> usize {
357        self.fragments.len()
358    }
359
360    /// Define an external label at a known absolute address.
361    pub fn define_external(&mut self, name: &str, addr: u64) {
362        self.externals.insert(String::from(name), addr);
363    }
364
365    /// Define a constant value (`.equ` / `.set`).
366    pub fn define_constant(&mut self, name: &str, value: i128) {
367        self.constants.insert(String::from(name), value);
368    }
369
370    /// Look up a constant value by name.
371    pub fn get_constant(&self, name: &str) -> Option<&i128> {
372        self.constants.get(name)
373    }
374
375    /// Add a label definition at the current position (before the next fragment).
376    pub fn add_label(&mut self, name: &str, span: Span) -> Result<(), AsmError> {
377        // Numeric labels (e.g. `1:`) can be redefined.
378        if let Ok(n) = name.parse::<u32>() {
379            self.numeric
380                .defs
381                .entry(n)
382                .or_default()
383                .push(self.fragments.len());
384            return Ok(());
385        }
386
387        if let Some(existing) = self.labels.get(name) {
388            return Err(AsmError::DuplicateLabel {
389                label: String::from(name),
390                span,
391                first_span: existing.span,
392            });
393        }
394        self.labels.insert(
395            String::from(name),
396            LabelDef {
397                fragment_index: self.fragments.len(),
398                span,
399            },
400        );
401        Ok(())
402    }
403
404    /// Add a pre-built fragment.
405    pub fn add_fragment(&mut self, fragment: Fragment) {
406        self.fragments.push(fragment);
407    }
408
409    /// Convenience: add fixed bytes (data, non-branch instructions, etc.).
410    pub fn add_bytes(&mut self, bytes: Vec<u8>, span: Span) {
411        self.fragments.push(Fragment::Fixed {
412            bytes: FragmentBytes::Heap(bytes),
413            relocation: None,
414            span,
415        });
416    }
417
418    /// Add an encoded instruction, automatically choosing `Fixed` or `Relaxable`.
419    pub fn add_encoded(
420        &mut self,
421        bytes: InstrBytes,
422        relocation: Option<Relocation>,
423        relax: Option<RelaxInfo>,
424        span: Span,
425    ) -> Result<(), AsmError> {
426        if let Some(ri) = relax {
427            let long_relocation = relocation.ok_or_else(|| AsmError::Syntax {
428                msg: String::from("internal: relaxable instruction missing relocation"),
429                span,
430            })?;
431            self.fragments.push(Fragment::Relaxable {
432                short_bytes: ri.short_bytes,
433                short_reloc_offset: ri.short_reloc_offset,
434                short_relocation: ri.short_relocation,
435                long_bytes: bytes,
436                long_relocation,
437                is_long: false,
438                span,
439            });
440        } else {
441            self.fragments.push(Fragment::Fixed {
442                bytes: FragmentBytes::Inline(bytes),
443                relocation,
444                span,
445            });
446        }
447        Ok(())
448    }
449
450    /// Add alignment padding.
451    pub fn add_alignment(
452        &mut self,
453        alignment: u32,
454        fill: u8,
455        max_skip: Option<u32>,
456        use_nop: bool,
457        span: Span,
458    ) {
459        self.fragments.push(Fragment::Align {
460            alignment,
461            fill,
462            max_skip,
463            use_nop,
464            span,
465        });
466    }
467
468    /// Add an `.org` directive: advance the location counter to `target`,
469    /// padding with `fill` bytes.
470    pub fn add_org(&mut self, target: u64, fill: u8, span: Span) {
471        self.fragments.push(Fragment::Org { target, fill, span });
472    }
473
474    // ── resolve ────────────────────────────────────────────
475
476    /// Resolve all labels, perform branch relaxation, and return
477    /// the final bytes together with a label→address table and applied relocations.
478    ///
479    /// # Note
480    ///
481    /// Relocations are patched into the fragments **in place**, so `resolve()`
482    /// is single-shot: calling it a second time would apply every relocation
483    /// on top of the already-patched bytes and produce garbage. To re-link,
484    /// build a fresh `Linker` and re-add the fragments.
485    pub fn resolve(&mut self) -> Result<ResolveOutput, AsmError> {
486        // Guard the in-place patching: a second pass would apply every
487        // relocation on top of already-patched bytes and silently emit
488        // garbage, so refuse rather than corrupt.
489        if self.resolved {
490            return Err(AsmError::Syntax {
491                msg: String::from(
492                    "linker already resolved: relocations are patched in place, \
493                     so build a fresh Linker to re-link",
494                ),
495                span: Span::new(0, 0, 0, 0),
496            });
497        }
498        self.resolved = true;
499
500        // Phase 1: branch relaxation (Szymanski monotonic growth)
501        let offsets = self.relax()?;
502
503        // Phase 2: emit final bytes with patched relocations (reuse offsets)
504        self.emit_final(offsets)
505    }
506
507    // ── branch relaxation ──────────────────────────────────
508
509    /// Iteratively grow short branches that cannot reach their targets.
510    /// Returns the final computed offsets on success so callers can reuse them.
511    fn relax(&mut self) -> Result<Vec<u64>, AsmError> {
512        let mut offsets = Vec::with_capacity(self.fragments.len() + 1);
513        let mut to_expand: Vec<usize> = Vec::new();
514
515        for _iter in 0..MAX_RELAXATION_ITERS {
516            self.compute_offsets_into(&mut offsets);
517            self.check_layout_size(&offsets)?;
518            to_expand.clear();
519
520            for (i, frag) in self.fragments.iter().enumerate() {
521                if let Fragment::Relaxable {
522                    short_bytes,
523                    short_relocation,
524                    long_relocation,
525                    is_long,
526                    ..
527                } = frag
528                {
529                    if !is_long {
530                        let frag_end = offsets[i].wrapping_add(short_bytes.len() as u64);
531                        match self.resolve_label_with_offsets(&long_relocation.label, i, &offsets) {
532                            Ok(target) => {
533                                // Label addresses are caller-supplied and may
534                                // sit anywhere in the 64-bit space, so this
535                                // difference can exceed `i64`. Wrapping keeps
536                                // the range check total; a displacement that
537                                // wraps is out of range for every encoding and
538                                // is rejected below, then diagnosed properly
539                                // when the relocation is applied.
540                                let disp = (target as i64)
541                                    .wrapping_sub(frag_end as i64)
542                                    .wrapping_add(long_relocation.addend);
543                                let in_range = if let Some(ref sr) = short_relocation {
544                                    // Architecture-specific short form range check
545                                    match sr.kind {
546                                        #[cfg(feature = "riscv")]
547                                        RelocKind::RvBranch12 => {
548                                            // B-type: PC-relative from instruction start
549                                            let pc_offset = disp + (short_bytes.len() as i64);
550                                            (-(1i64 << 12)..(1i64 << 12)).contains(&pc_offset)
551                                        }
552                                        #[cfg(feature = "riscv")]
553                                        RelocKind::RvCBranch8 => {
554                                            // CB-type c.beqz/c.bnez: ±256 B (9-bit signed)
555                                            let pc_offset = disp + (short_bytes.len() as i64);
556                                            (-(1i64 << 8)..(1i64 << 8)).contains(&pc_offset)
557                                        }
558                                        #[cfg(feature = "riscv")]
559                                        RelocKind::RvCJump11 => {
560                                            // CJ-type c.j: ±2 KB (12-bit signed)
561                                            let pc_offset = disp + (short_bytes.len() as i64);
562                                            (-(1i64 << 11)..(1i64 << 11)).contains(&pc_offset)
563                                        }
564                                        #[cfg(feature = "aarch64")]
565                                        RelocKind::Aarch64Branch19 => {
566                                            // B.cond / CBZ / CBNZ: ±1 MB (19-bit signed × 4)
567                                            let pc_offset = disp + (short_bytes.len() as i64);
568                                            (-(1i64 << 20)..(1i64 << 20)).contains(&pc_offset)
569                                        }
570                                        #[cfg(feature = "aarch64")]
571                                        RelocKind::Aarch64Branch14 => {
572                                            // TBZ / TBNZ: ±32 KB (14-bit signed × 4)
573                                            let pc_offset = disp + (short_bytes.len() as i64);
574                                            (-(1i64 << 15)..(1i64 << 15)).contains(&pc_offset)
575                                        }
576                                        #[cfg(feature = "aarch64")]
577                                        RelocKind::Aarch64Adr21 => {
578                                            // ADR: ±1 MB (21-bit signed)
579                                            let pc_offset = disp + (short_bytes.len() as i64);
580                                            (-(1i64 << 20)..(1i64 << 20)).contains(&pc_offset)
581                                        }
582                                        #[cfg(feature = "arm")]
583                                        RelocKind::ThumbBranch8 => {
584                                            // B<cond> narrow: signed 8-bit offset >> 1 → ±256 B
585                                            // PC = instr + 4 in Thumb
586                                            let pc_offset = disp + (short_bytes.len() as i64);
587                                            (-(1i64 << 8)..(1i64 << 8)).contains(&pc_offset)
588                                        }
589                                        #[cfg(feature = "arm")]
590                                        RelocKind::ThumbBranch11 => {
591                                            // B narrow: signed 11-bit offset >> 1 → ±2 KB
592                                            let pc_offset = disp + (short_bytes.len() as i64);
593                                            (-(1i64 << 11)..(1i64 << 11)).contains(&pc_offset)
594                                        }
595                                        _ => (-128..=127).contains(&disp),
596                                    }
597                                } else {
598                                    // x86 default: rel8 ±127 from frag_end
599                                    (-128..=127).contains(&disp)
600                                };
601                                if !in_range {
602                                    to_expand.push(i);
603                                }
604                            }
605                            Err(_) => {
606                                // Undefined label — conservatively assume long form.
607                                // The real error will surface during emit_final.
608                                to_expand.push(i);
609                            }
610                        }
611                    }
612                }
613            }
614
615            if to_expand.is_empty() {
616                return Ok(offsets);
617            }
618
619            for &idx in &to_expand {
620                if let Fragment::Relaxable {
621                    ref mut is_long, ..
622                } = self.fragments[idx]
623                {
624                    *is_long = true;
625                }
626            }
627        }
628
629        Err(AsmError::RelaxationLimit {
630            max: MAX_RELAXATION_ITERS,
631        })
632    }
633
634    /// Reject a layout whose end address exceeds the configured ceiling.
635    ///
636    /// Called after every offset computation so that a `.org`/`.align`
637    /// requesting terabytes of padding fails fast instead of being handed to
638    /// the allocator.
639    fn check_layout_size(&self, offsets: &[u64]) -> Result<(), AsmError> {
640        let end = offsets.last().copied().unwrap_or(self.base_address);
641        let size = end.saturating_sub(self.base_address);
642        if size > self.max_output_bytes as u64 {
643            return Err(AsmError::ResourceLimitExceeded {
644                resource: String::from("output bytes"),
645                limit: self.max_output_bytes,
646            });
647        }
648        Ok(())
649    }
650
651    // ── offset computation ─────────────────────────────────
652
653    /// Build an offset table: `offsets[i]` is the absolute address of fragment `i`.
654    ///
655    /// `offsets[fragments.len()]` is a sentinel for the total end address.
656    /// Reuses the provided vector to avoid repeated allocation.
657    fn compute_offsets_into(&self, offsets: &mut Vec<u64>) {
658        offsets.clear();
659        let mut current = self.base_address;
660        for frag in &self.fragments {
661            offsets.push(current);
662            match frag {
663                Fragment::Fixed { bytes, .. } => {
664                    current += bytes.len() as u64;
665                }
666                Fragment::Align {
667                    alignment,
668                    max_skip,
669                    ..
670                } => {
671                    let a = *alignment as u64;
672                    if a > 1 {
673                        let aligned = current.div_ceil(a) * a;
674                        let padding = aligned - current;
675                        if max_skip.map_or(true, |ms| padding <= ms as u64) {
676                            current = aligned;
677                        }
678                    }
679                }
680                Fragment::Relaxable {
681                    short_bytes,
682                    long_bytes,
683                    is_long,
684                    ..
685                } => {
686                    if *is_long {
687                        current += long_bytes.len() as u64;
688                    } else {
689                        current += short_bytes.len() as u64;
690                    }
691                }
692                Fragment::Org { target, .. } => {
693                    if *target > current {
694                        current = *target;
695                    }
696                    // If target <= current, no advancement (error at emit time)
697                }
698            }
699        }
700        offsets.push(current);
701    }
702
703    // ── final emit ─────────────────────────────────────────
704
705    fn emit_final(&mut self, offsets: Vec<u64>) -> Result<ResolveOutput, AsmError> {
706        // Re-checked here (and not only in `relax`) so that `emit_final` is
707        // safe to reach from any future caller: the reservation below is sized
708        // directly from the layout.
709        self.check_layout_size(&offsets)?;
710        let total_size = offsets
711            .last()
712            .copied()
713            .unwrap_or(self.base_address)
714            .saturating_sub(self.base_address);
715        let mut output = Vec::with_capacity(total_size as usize);
716        let mut applied_relocs = Vec::new();
717
718        // Take fragments out so `self` is free for apply_relocation / resolve_label calls.
719        // This avoids cloning every fragment's byte buffer on the final emit path.
720        let mut fragments = core::mem::take(&mut self.fragments);
721
722        for (i, frag) in fragments.iter_mut().enumerate() {
723            match frag {
724                Fragment::Fixed {
725                    bytes,
726                    relocation,
727                    span,
728                } => {
729                    if let Some(ref mut reloc) = relocation {
730                        let frag_output_offset = output.len();
731                        // Patch in-place — no heap clone needed.
732                        self.apply_relocation(bytes, reloc, offsets[i], &offsets, i, *span)?;
733                        applied_relocs.push(AppliedRelocation {
734                            offset: frag_output_offset + reloc.offset,
735                            size: reloc.size,
736                            // Take ownership — emit_final is terminal, labels won't be read again.
737                            label: reloc.label.to_string(),
738                            kind: reloc.kind,
739                            addend: reloc.addend,
740                        });
741                        output.extend_from_slice(bytes);
742                    } else {
743                        output.extend_from_slice(bytes);
744                    }
745                }
746
747                Fragment::Align {
748                    alignment,
749                    fill,
750                    max_skip,
751                    use_nop,
752                    ..
753                } => {
754                    let a = *alignment as u64;
755                    if a > 1 {
756                        let current = offsets[i];
757                        let aligned = current.div_ceil(a) * a;
758                        let padding = (aligned - current) as usize;
759                        // skip if max_skip is exceeded
760                        if max_skip.is_some_and(|ms| padding > ms as usize) {
761                            // no padding emitted
762                        } else if *use_nop {
763                            emit_nop_padding(&mut output, padding);
764                        } else {
765                            output.extend(core::iter::repeat(*fill).take(padding));
766                        }
767                    }
768                }
769
770                Fragment::Relaxable {
771                    short_bytes,
772                    short_reloc_offset,
773                    short_relocation,
774                    long_bytes,
775                    long_relocation,
776                    is_long,
777                    span,
778                } => {
779                    if *is_long {
780                        let frag_output_offset = output.len();
781                        // Patch long_bytes in-place — no heap clone needed.
782                        self.apply_relocation(
783                            long_bytes,
784                            long_relocation,
785                            offsets[i],
786                            &offsets,
787                            i,
788                            *span,
789                        )?;
790                        applied_relocs.push(AppliedRelocation {
791                            offset: frag_output_offset + long_relocation.offset,
792                            size: long_relocation.size,
793                            label: (*long_relocation.label).into(),
794                            kind: long_relocation.kind,
795                            addend: long_relocation.addend,
796                        });
797                        output.extend_from_slice(long_bytes);
798                    } else if let Some(ref mut sr) = short_relocation {
799                        // Short form with architecture-specific relocation
800                        // (e.g. RISC-V B-type branch).
801                        let frag_output_offset = output.len();
802                        // Patch short_bytes in-place — no heap clone needed.
803                        self.apply_relocation(short_bytes, sr, offsets[i], &offsets, i, *span)?;
804                        applied_relocs.push(AppliedRelocation {
805                            offset: frag_output_offset + sr.offset,
806                            size: sr.size,
807                            label: (*sr.label).into(),
808                            kind: sr.kind,
809                            addend: sr.addend,
810                        });
811                        output.extend_from_slice(short_bytes);
812                    } else {
813                        // Short form — patch rel8 displacement (x86)
814                        let frag_output_offset = output.len();
815                        let target =
816                            self.resolve_label_with_offsets(&long_relocation.label, i, &offsets)?;
817                        let frag_end = offsets[i].wrapping_add(short_bytes.len() as u64);
818                        let disp = (target as i64)
819                            .wrapping_sub(frag_end as i64)
820                            .wrapping_add(long_relocation.addend);
821                        if !(-128..=127).contains(&disp) {
822                            return Err(AsmError::BranchOutOfRange {
823                                label: long_relocation.label.to_string(),
824                                disp,
825                                max: 127,
826                                span: *span,
827                            });
828                        }
829                        // Patch short_bytes in-place — no heap clone needed.
830                        short_bytes[*short_reloc_offset] = disp as i8 as u8;
831                        applied_relocs.push(AppliedRelocation {
832                            offset: frag_output_offset + *short_reloc_offset,
833                            size: 1,
834                            label: (*long_relocation.label).into(),
835                            kind: RelocKind::X86Relative,
836                            addend: long_relocation.addend,
837                        });
838                        output.extend_from_slice(short_bytes);
839                    }
840                }
841
842                Fragment::Org {
843                    target, fill, span, ..
844                } => {
845                    let current = offsets[i];
846                    if *target < current {
847                        return Err(AsmError::Syntax {
848                            msg: alloc::format!(
849                                ".org target 0x{:X} is behind current position 0x{:X}",
850                                target,
851                                current
852                            ),
853                            span: *span,
854                        });
855                    }
856                    let padding = (*target - current) as usize;
857                    output.extend(core::iter::repeat(*fill).take(padding));
858                }
859            }
860        }
861
862        // Restore fragments (now patched in-place, but structure intact).
863        self.fragments = fragments;
864
865        // Collect label addresses
866        let label_table: Vec<(String, u64)> = self
867            .labels
868            .iter()
869            .map(|(name, def)| (name.clone(), offsets[def.fragment_index]))
870            .collect();
871
872        Ok((output, label_table, applied_relocs, offsets))
873    }
874
875    // ── relocation patching ────────────────────────────────
876
877    fn apply_relocation(
878        &self,
879        bytes: &mut [u8],
880        reloc: &Relocation,
881        frag_abs: u64,
882        offsets: &[u64],
883        from_fragment: usize,
884        span: Span,
885    ) -> Result<(), AsmError> {
886        let target_addr = self.resolve_label_with_offsets(&reloc.label, from_fragment, offsets)?;
887        let reloc_abs = frag_abs + reloc.offset as u64;
888
889        match reloc.kind {
890            RelocKind::X86Relative => {
891                // RIP = address past the entire instruction, not just past the reloc field.
892                // trailing_bytes accounts for any immediate bytes following the displacement.
893                let rip = reloc_abs + reloc.size as u64 + reloc.trailing_bytes as u64;
894                let rel = (target_addr as i64)
895                    .wrapping_sub(rip as i64)
896                    .wrapping_add(reloc.addend);
897                match reloc.size {
898                    1 => {
899                        if rel < i8::MIN as i64 || rel > i8::MAX as i64 {
900                            return Err(AsmError::BranchOutOfRange {
901                                label: reloc.label.to_string(),
902                                disp: rel,
903                                max: 127,
904                                span,
905                            });
906                        }
907                        bytes[reloc.offset] = rel as i8 as u8;
908                    }
909                    4 => {
910                        if rel < i32::MIN as i64 || rel > i32::MAX as i64 {
911                            return Err(AsmError::BranchOutOfRange {
912                                label: reloc.label.to_string(),
913                                disp: rel,
914                                max: i32::MAX as i64,
915                                span,
916                            });
917                        }
918                        let b = (rel as i32).to_le_bytes();
919                        bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&b);
920                    }
921                    other => {
922                        return Err(AsmError::Syntax {
923                            msg: alloc::format!(
924                                "unsupported RIP-relative relocation size: {other}"
925                            ),
926                            span,
927                        });
928                    }
929                }
930            }
931            RelocKind::Absolute => {
932                let addr = target_addr.wrapping_add(reloc.addend as u64);
933                match reloc.size {
934                    1 => {
935                        if addr > u8::MAX as u64 {
936                            return Err(AsmError::Syntax {
937                                msg: alloc::format!(
938                                    "absolute address 0x{addr:X} exceeds 8-bit relocation range for '{}'",
939                                    reloc.label
940                                ),
941                                span,
942                            });
943                        }
944                        bytes[reloc.offset] = addr as u8;
945                    }
946                    2 => {
947                        if addr > u16::MAX as u64 {
948                            return Err(AsmError::Syntax {
949                                msg: alloc::format!(
950                                    "absolute address 0x{addr:X} exceeds 16-bit relocation range for '{}'",
951                                    reloc.label
952                                ),
953                                span,
954                            });
955                        }
956                        bytes[reloc.offset..reloc.offset + 2]
957                            .copy_from_slice(&(addr as u16).to_le_bytes());
958                    }
959                    4 => {
960                        if addr > u32::MAX as u64 {
961                            return Err(AsmError::Syntax {
962                                msg: alloc::format!(
963                                    "absolute address 0x{addr:X} exceeds 32-bit relocation range for '{}'",
964                                    reloc.label
965                                ),
966                                span,
967                            });
968                        }
969                        bytes[reloc.offset..reloc.offset + 4]
970                            .copy_from_slice(&(addr as u32).to_le_bytes());
971                    }
972                    8 => {
973                        bytes[reloc.offset..reloc.offset + 8].copy_from_slice(&addr.to_le_bytes());
974                    }
975                    other => {
976                        return Err(AsmError::Syntax {
977                            msg: alloc::format!("unsupported absolute relocation size: {other}"),
978                            span,
979                        });
980                    }
981                }
982            }
983            #[cfg(feature = "arm")]
984            RelocKind::ArmBranch24 => {
985                // ARM32 B/BL: PC = instr_addr + 8, offset = (target - PC) >> 2, packed bits 23:0
986                let pc = reloc_abs + 8;
987                let rel = (target_addr as i64)
988                    .wrapping_sub(pc as i64)
989                    .wrapping_add(reloc.addend);
990                let offset = scaled_displacement(rel, 4, &reloc.label, span)?;
991                if !(-(1 << 23)..(1 << 23)).contains(&offset) {
992                    return Err(AsmError::BranchOutOfRange {
993                        label: reloc.label.to_string(),
994                        disp: rel,
995                        max: (1 << 25) - 4,
996                        span,
997                    });
998                }
999                let imm24 = (offset as u32) & 0x00FF_FFFF;
1000                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1001                word = (word & 0xFF00_0000) | imm24;
1002                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1003            }
1004            #[cfg(feature = "arm")]
1005            RelocKind::ArmLdrLit => {
1006                // ARM32 LDR Rd, label: PC = instr_addr + 8, 12-bit offset, U-bit (bit 23)
1007                let pc = reloc_abs + 8;
1008                let rel = (target_addr as i64)
1009                    .wrapping_sub(pc as i64)
1010                    .wrapping_add(reloc.addend);
1011                let abs_rel = rel.unsigned_abs();
1012                if abs_rel > 4095 {
1013                    return Err(AsmError::BranchOutOfRange {
1014                        label: reloc.label.to_string(),
1015                        disp: rel,
1016                        max: 4095,
1017                        span,
1018                    });
1019                }
1020                let u_bit = if rel >= 0 { 1u32 } else { 0u32 };
1021                let imm12 = (abs_rel as u32) & 0xFFF;
1022                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1023                word = (word & 0xFF7F_F000) | (u_bit << 23) | imm12;
1024                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1025            }
1026            #[cfg(feature = "arm")]
1027            RelocKind::ArmAdr => {
1028                // ARM32 ADR Rd, label → ADD/SUB Rd, PC, #rotated_imm
1029                // PC = instr_addr + 8 (ARM pipeline)
1030                // The data-processing immediate format uses 8-bit imm + 4-bit rotation
1031                let pc = reloc_abs + 8;
1032                let rel = (target_addr as i64)
1033                    .wrapping_sub(pc as i64)
1034                    .wrapping_add(reloc.addend);
1035                let abs_rel = rel.unsigned_abs() as u32;
1036                let (op, imm8, rot) = if rel >= 0 {
1037                    // ADD Rd, PC, #imm
1038                    let (i, r) = encode_arm_imm_for_linker(abs_rel).ok_or_else(|| {
1039                        AsmError::BranchOutOfRange {
1040                            label: reloc.label.to_string(),
1041                            disp: rel,
1042                            max: 255, // max unrotated; actual range depends on pattern
1043                            span,
1044                        }
1045                    })?;
1046                    (0x4u32, i, r)
1047                } else {
1048                    // SUB Rd, PC, #imm
1049                    let (i, r) = encode_arm_imm_for_linker(abs_rel).ok_or_else(|| {
1050                        AsmError::BranchOutOfRange {
1051                            label: reloc.label.to_string(),
1052                            disp: rel,
1053                            max: 255,
1054                            span,
1055                        }
1056                    })?;
1057                    (0x2u32, i, r)
1058                };
1059                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1060                // Clear opcode (bits 24:21) and immediate field (bits 11:0)
1061                word = (word & 0xF1F0_F000) | (op << 21) | ((rot as u32) << 8) | (imm8 as u32);
1062                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1063            }
1064            #[cfg(feature = "arm")]
1065            RelocKind::ThumbBranch8 => {
1066                // Thumb conditional branch (16-bit): PC = instr + 4
1067                let pc = reloc_abs + 4;
1068                let rel = (target_addr as i64)
1069                    .wrapping_sub(pc as i64)
1070                    .wrapping_add(reloc.addend);
1071                let offset = scaled_displacement(rel, 2, &reloc.label, span)?;
1072                if !(-(1i64 << 7)..(1i64 << 7)).contains(&offset) {
1073                    return Err(AsmError::BranchOutOfRange {
1074                        label: reloc.label.to_string(),
1075                        disp: rel,
1076                        max: 254,
1077                        span,
1078                    });
1079                }
1080                let imm8 = (offset as u8) as u16;
1081                let mut hw = read_le16(bytes, reloc.offset, &reloc.label, span)?;
1082                hw = (hw & 0xFF00) | (imm8 & 0xFF);
1083                bytes[reloc.offset..reloc.offset + 2].copy_from_slice(&hw.to_le_bytes());
1084            }
1085            #[cfg(feature = "arm")]
1086            RelocKind::ThumbBranch11 => {
1087                // Thumb unconditional branch (16-bit): PC = instr + 4
1088                let pc = reloc_abs + 4;
1089                let rel = (target_addr as i64)
1090                    .wrapping_sub(pc as i64)
1091                    .wrapping_add(reloc.addend);
1092                let offset = scaled_displacement(rel, 2, &reloc.label, span)?;
1093                if !(-(1i64 << 10)..(1i64 << 10)).contains(&offset) {
1094                    return Err(AsmError::BranchOutOfRange {
1095                        label: reloc.label.to_string(),
1096                        disp: rel,
1097                        max: 2046,
1098                        span,
1099                    });
1100                }
1101                let imm11 = (offset as u16) & 0x7FF;
1102                let mut hw = read_le16(bytes, reloc.offset, &reloc.label, span)?;
1103                hw = (hw & 0xF800) | imm11;
1104                bytes[reloc.offset..reloc.offset + 2].copy_from_slice(&hw.to_le_bytes());
1105            }
1106            #[cfg(feature = "arm")]
1107            RelocKind::ThumbBl => {
1108                // Thumb-2 BL (32-bit): PC = instr + 4
1109                let pc = reloc_abs + 4;
1110                let rel = (target_addr as i64)
1111                    .wrapping_sub(pc as i64)
1112                    .wrapping_add(reloc.addend);
1113                let offset = scaled_displacement(rel, 2, &reloc.label, span)?;
1114                if !(-(1i64 << 23)..(1i64 << 23)).contains(&offset) {
1115                    return Err(AsmError::BranchOutOfRange {
1116                        label: reloc.label.to_string(),
1117                        disp: rel,
1118                        max: (1 << 24) - 2,
1119                        span,
1120                    });
1121                }
1122                let (s, j1, j2, imm10, imm11) = thumb_t1_t4_fields(offset);
1123                let hw1 = 0xF000 | (s << 10) | imm10;
1124                let hw2 = 0xD000 | (j1 << 13) | (j2 << 11) | imm11;
1125                bytes[reloc.offset..reloc.offset + 2].copy_from_slice(&hw1.to_le_bytes());
1126                bytes[reloc.offset + 2..reloc.offset + 4].copy_from_slice(&hw2.to_le_bytes());
1127            }
1128            #[cfg(feature = "arm")]
1129            RelocKind::ThumbBranchW => {
1130                // Thumb-2 B.W (32-bit wide unconditional): PC = instr + 4
1131                let pc = reloc_abs + 4;
1132                let rel = (target_addr as i64)
1133                    .wrapping_sub(pc as i64)
1134                    .wrapping_add(reloc.addend);
1135                let offset = scaled_displacement(rel, 2, &reloc.label, span)?;
1136                if !(-(1i64 << 23)..(1i64 << 23)).contains(&offset) {
1137                    return Err(AsmError::BranchOutOfRange {
1138                        label: reloc.label.to_string(),
1139                        disp: rel,
1140                        max: (1 << 24) - 2,
1141                        span,
1142                    });
1143                }
1144                let (s, j1, j2, imm10, imm11) = thumb_t1_t4_fields(offset);
1145                let hw1 = 0xF000 | (s << 10) | imm10;
1146                let hw2 = 0x9000 | (j1 << 13) | (j2 << 11) | imm11;
1147                bytes[reloc.offset..reloc.offset + 2].copy_from_slice(&hw1.to_le_bytes());
1148                bytes[reloc.offset + 2..reloc.offset + 4].copy_from_slice(&hw2.to_le_bytes());
1149            }
1150            #[cfg(feature = "arm")]
1151            RelocKind::ThumbCondBranchW => {
1152                // Thumb-2 B<cond>.W (32-bit wide conditional): PC = instr + 4
1153                let pc = reloc_abs + 4;
1154                let rel = (target_addr as i64)
1155                    .wrapping_sub(pc as i64)
1156                    .wrapping_add(reloc.addend);
1157                let offset = scaled_displacement(rel, 2, &reloc.label, span)?;
1158                if !(-(1i64 << 19)..(1i64 << 19)).contains(&offset) {
1159                    return Err(AsmError::BranchOutOfRange {
1160                        label: reloc.label.to_string(),
1161                        disp: rel,
1162                        max: (1 << 20) - 2,
1163                        span,
1164                    });
1165                }
1166                let s = if offset < 0 { 1_u16 } else { 0 };
1167                let imm = offset as u32;
1168                let imm6 = ((imm >> 11) & 0x3F) as u16;
1169                let imm11 = (imm & 0x7FF) as u16;
1170                let j1 = ((imm >> 17) & 1) as u16;
1171                let j2 = ((imm >> 18) & 1) as u16;
1172                // Read existing hw1 to preserve condition code bits
1173                let existing_hw1 = read_le16(bytes, reloc.offset, &reloc.label, span)?;
1174                let cond = (existing_hw1 >> 6) & 0xF;
1175                let hw1 = 0xF000 | (s << 10) | (cond << 6) | imm6;
1176                let hw2 = 0x8000 | (j1 << 13) | (j2 << 11) | imm11;
1177                bytes[reloc.offset..reloc.offset + 2].copy_from_slice(&hw1.to_le_bytes());
1178                bytes[reloc.offset + 2..reloc.offset + 4].copy_from_slice(&hw2.to_le_bytes());
1179            }
1180            #[cfg(feature = "arm")]
1181            RelocKind::ThumbLdrLit8 => {
1182                // Thumb LDR Rt, [PC, #imm8×4]: PC = (instr_addr + 4) & ~3
1183                // Forward only, word-aligned, 0..1020 byte range
1184                let pc = (reloc_abs + 4) & !3;
1185                let rel = (target_addr as i64)
1186                    .wrapping_sub(pc as i64)
1187                    .wrapping_add(reloc.addend);
1188                if !(0..=1020).contains(&rel) || (rel & 3) != 0 {
1189                    return Err(AsmError::BranchOutOfRange {
1190                        label: reloc.label.to_string(),
1191                        disp: rel,
1192                        max: 1020,
1193                        span,
1194                    });
1195                }
1196                let imm8 = (rel >> 2) as u16;
1197                let existing = read_le16(bytes, reloc.offset, &reloc.label, span)?;
1198                let hw = (existing & 0xFF00) | imm8;
1199                bytes[reloc.offset..reloc.offset + 2].copy_from_slice(&hw.to_le_bytes());
1200            }
1201            #[cfg(feature = "aarch64")]
1202            RelocKind::Aarch64Jump26 => {
1203                // AArch64 B/BL: PC-relative offset >> 2 in bits 25:0
1204                let rel = (target_addr as i64)
1205                    .wrapping_sub(reloc_abs as i64)
1206                    .wrapping_add(reloc.addend);
1207                let offset = scaled_displacement(rel, 4, &reloc.label, span)?;
1208                if !(-(1 << 25)..(1 << 25)).contains(&offset) {
1209                    return Err(AsmError::BranchOutOfRange {
1210                        label: reloc.label.to_string(),
1211                        disp: rel,
1212                        max: (1 << 27) - 4,
1213                        span,
1214                    });
1215                }
1216                let imm26 = (offset as u32) & 0x03FF_FFFF;
1217                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1218                word = (word & 0xFC00_0000) | imm26;
1219                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1220            }
1221            #[cfg(feature = "aarch64")]
1222            RelocKind::Aarch64Branch19 => {
1223                // AArch64 B.cond / CBZ / CBNZ: PC-relative offset >> 2 in bits 23:5
1224                let rel = (target_addr as i64)
1225                    .wrapping_sub(reloc_abs as i64)
1226                    .wrapping_add(reloc.addend);
1227                let offset = scaled_displacement(rel, 4, &reloc.label, span)?;
1228                if !(-(1 << 18)..(1 << 18)).contains(&offset) {
1229                    return Err(AsmError::BranchOutOfRange {
1230                        label: reloc.label.to_string(),
1231                        disp: rel,
1232                        max: (1 << 20) - 4,
1233                        span,
1234                    });
1235                }
1236                let imm19 = (offset as u32) & 0x7FFFF;
1237                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1238                word = (word & 0xFF00_001F) | (imm19 << 5);
1239                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1240            }
1241            #[cfg(feature = "aarch64")]
1242            RelocKind::Aarch64Branch14 => {
1243                // AArch64 TBZ / TBNZ: PC-relative offset >> 2 in bits 18:5
1244                let rel = (target_addr as i64)
1245                    .wrapping_sub(reloc_abs as i64)
1246                    .wrapping_add(reloc.addend);
1247                let offset = scaled_displacement(rel, 4, &reloc.label, span)?;
1248                if !(-(1 << 13)..(1 << 13)).contains(&offset) {
1249                    return Err(AsmError::BranchOutOfRange {
1250                        label: reloc.label.to_string(),
1251                        disp: rel,
1252                        max: (1 << 15) - 4,
1253                        span,
1254                    });
1255                }
1256                let imm14 = (offset as u32) & 0x3FFF;
1257                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1258                word = (word & 0xFFF8_001F) | (imm14 << 5);
1259                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1260            }
1261            #[cfg(feature = "aarch64")]
1262            RelocKind::Aarch64LdrLit19 => {
1263                // AArch64 LDR (literal): PC-relative offset >> 2 in bits 23:5
1264                let rel = (target_addr as i64)
1265                    .wrapping_sub(reloc_abs as i64)
1266                    .wrapping_add(reloc.addend);
1267                let offset = scaled_displacement(rel, 4, &reloc.label, span)?;
1268                if !(-(1 << 18)..(1 << 18)).contains(&offset) {
1269                    return Err(AsmError::BranchOutOfRange {
1270                        label: reloc.label.to_string(),
1271                        disp: rel,
1272                        max: (1 << 20) - 4,
1273                        span,
1274                    });
1275                }
1276                let imm19 = (offset as u32) & 0x7FFFF;
1277                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1278                word = (word & 0xFF00_001F) | (imm19 << 5);
1279                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1280            }
1281            #[cfg(feature = "aarch64")]
1282            RelocKind::Aarch64Adr21 => {
1283                // AArch64 ADR: PC-relative, immhi (bits 23:5), immlo (bits 30:29)
1284                let rel = (target_addr as i64)
1285                    .wrapping_sub(reloc_abs as i64)
1286                    .wrapping_add(reloc.addend);
1287                if !(-(1 << 20)..(1 << 20)).contains(&rel) {
1288                    return Err(AsmError::BranchOutOfRange {
1289                        label: reloc.label.to_string(),
1290                        disp: rel,
1291                        max: (1 << 20) - 1,
1292                        span,
1293                    });
1294                }
1295                let immhi = ((rel >> 2) as u32) & 0x7FFFF;
1296                let immlo = (rel as u32) & 0x3;
1297                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1298                word = (word & 0x9F00_001F) | (immlo << 29) | (immhi << 5);
1299                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1300            }
1301            #[cfg(feature = "aarch64")]
1302            RelocKind::Aarch64Adrp => {
1303                // AArch64 ADRP: page-relative, page = addr & ~0xFFF
1304                let pc_page = reloc_abs & !0xFFF;
1305                let target_page = target_addr.wrapping_add(reloc.addend as u64) & !0xFFF;
1306                let rel = (target_page as i64).wrapping_sub(pc_page as i64);
1307                let page_off = rel >> 12;
1308                if !(-(1 << 20)..(1 << 20)).contains(&page_off) {
1309                    return Err(AsmError::BranchOutOfRange {
1310                        label: reloc.label.to_string(),
1311                        disp: rel,
1312                        max: (1i64 << 32) - 1,
1313                        span,
1314                    });
1315                }
1316                let immhi = ((page_off >> 2) as u32) & 0x7FFFF;
1317                let immlo = (page_off as u32) & 0x3;
1318                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1319                word = (word & 0x9F00_001F) | (immlo << 29) | (immhi << 5);
1320                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1321            }
1322            #[cfg(feature = "aarch64")]
1323            RelocKind::Aarch64AdrpAddPair => {
1324                // AArch64 ADRP+ADD pair: first word is ADRP, second is ADD.
1325                // ADRP: page-relative offset in immhi/immlo
1326                let pc_page = reloc_abs & !0xFFF;
1327                let target_with_addend = target_addr.wrapping_add(reloc.addend as u64);
1328                let target_page = target_with_addend & !0xFFF;
1329                let rel = (target_page as i64).wrapping_sub(pc_page as i64);
1330                let page_off = rel >> 12;
1331                if !(-(1 << 20)..(1 << 20)).contains(&page_off) {
1332                    return Err(AsmError::BranchOutOfRange {
1333                        label: reloc.label.to_string(),
1334                        disp: rel,
1335                        max: (1i64 << 32) - 1,
1336                        span,
1337                    });
1338                }
1339                let immhi_p = ((page_off >> 2) as u32) & 0x7FFFF;
1340                let immlo_p = (page_off as u32) & 0x3;
1341                let mut adrp_word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1342                adrp_word = (adrp_word & 0x9F00_001F) | (immlo_p << 29) | (immhi_p << 5);
1343                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&adrp_word.to_le_bytes());
1344
1345                // ADD: lo12 bits of target address in imm12 (bits 21:10)
1346                let lo12 = (target_with_addend & 0xFFF) as u32;
1347                let add_offset = reloc.offset + 4;
1348                let mut add_word = read_le32(bytes, add_offset, &reloc.label, span)?;
1349                add_word = (add_word & 0xFFC003FF) | (lo12 << 10);
1350                bytes[add_offset..add_offset + 4].copy_from_slice(&add_word.to_le_bytes());
1351            }
1352            #[cfg(feature = "riscv")]
1353            RelocKind::RvJal20 => {
1354                // RISC-V JAL: 21-bit signed PC-relative offset (bit 0 always 0)
1355                // J-type immediate: imm[20|10:1|11|19:12] packed into bits 31:12
1356                let rel = (target_addr as i64)
1357                    .wrapping_sub(reloc_abs as i64)
1358                    .wrapping_add(reloc.addend);
1359                if !(-(1i64 << 20)..(1i64 << 20)).contains(&rel) {
1360                    return Err(AsmError::BranchOutOfRange {
1361                        label: reloc.label.to_string(),
1362                        disp: rel,
1363                        max: (1 << 20) - 2,
1364                        span,
1365                    });
1366                }
1367                // Bit 0 of the displacement is implicit in the encoding.
1368                scaled_displacement(rel, 2, &reloc.label, span)?;
1369                let imm = rel as u32;
1370                let packed = ((imm & 0x0010_0000) << 11)  // imm[20]   → bit 31
1371                    | ((imm & 0x7FE) << 20)               // imm[10:1] → bits 30:21
1372                    | ((imm & 0x800) << 9)                // imm[11]   → bit 20
1373                    | (imm & 0x000F_F000); // imm[19:12]→ bits 19:12
1374                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1375                word = (word & 0xFFF) | packed;
1376                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1377            }
1378            #[cfg(feature = "riscv")]
1379            RelocKind::RvBranch12 => {
1380                // RISC-V B-type: 13-bit signed PC-relative offset (bit 0 always 0)
1381                // B-type immediate: imm[12|10:5] in bits 31:25, imm[4:1|11] in bits 11:7
1382                let rel = (target_addr as i64)
1383                    .wrapping_sub(reloc_abs as i64)
1384                    .wrapping_add(reloc.addend);
1385                if !(-(1i64 << 12)..(1i64 << 12)).contains(&rel) {
1386                    return Err(AsmError::BranchOutOfRange {
1387                        label: reloc.label.to_string(),
1388                        disp: rel,
1389                        max: (1 << 12) - 2,
1390                        span,
1391                    });
1392                }
1393                scaled_displacement(rel, 2, &reloc.label, span)?;
1394                let imm = rel as u32;
1395                let packed_hi = ((imm & 0x1000) << 19)    // imm[12]   → bit 31
1396                    | ((imm & 0x7E0) << 20); // imm[10:5] → bits 30:25
1397                let packed_lo = ((imm & 0x1E) << 7)       // imm[4:1]  → bits 11:8
1398                    | ((imm & 0x800) >> 4); // imm[11]   → bit 7
1399                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1400                word = (word & 0x01FF_F07F) | packed_hi | packed_lo;
1401                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1402            }
1403            #[cfg(feature = "riscv")]
1404            RelocKind::RvAuipc20 => {
1405                // RISC-V AUIPC+JALR pair: patches both instructions.
1406                // AUIPC at reloc.offset, JALR at reloc.offset+4.
1407                // hi20 = (offset + 0x800) >> 12  (rounds for sign-extension of lo12)
1408                // lo12 = offset - (hi20 << 12)
1409                let rel = (target_addr as i64)
1410                    .wrapping_sub(reloc_abs as i64)
1411                    .wrapping_add(reloc.addend);
1412                // AUIPC contributes a sign-extended 32-bit value, and the JALR
1413                // immediate is sign-extended from 12 bits, so the reachable
1414                // window is [-2^31 - 2^11, 2^31 - 2^11).  Without this check a
1415                // far target silently truncates to a wrong (and jumpable)
1416                // address instead of producing a diagnostic.
1417                if !(-(1i64 << 31) - 0x800..(1i64 << 31) - 0x800).contains(&rel) {
1418                    return Err(AsmError::BranchOutOfRange {
1419                        label: reloc.label.to_string(),
1420                        disp: rel,
1421                        max: (1i64 << 31) - 0x800 - 1,
1422                        span,
1423                    });
1424                }
1425                let hi20 = ((rel + 0x800) >> 12) as u32;
1426                let lo12 = (rel as u32).wrapping_sub(hi20 << 12);
1427                // Patch AUIPC: upper 20 bits in bits 31:12
1428                let mut word = read_le32(bytes, reloc.offset, &reloc.label, span)?;
1429                word = (word & 0xFFF) | (hi20 << 12);
1430                bytes[reloc.offset..reloc.offset + 4].copy_from_slice(&word.to_le_bytes());
1431                // Patch JALR: lower 12 bits in bits 31:20 (I-type immediate)
1432                let jalr_off = reloc.offset + 4;
1433                let mut jalr = read_le32(bytes, jalr_off, &reloc.label, span)?;
1434                jalr = (jalr & 0x000F_FFFF) | ((lo12 & 0xFFF) << 20);
1435                bytes[jalr_off..jalr_off + 4].copy_from_slice(&jalr.to_le_bytes());
1436            }
1437            #[cfg(feature = "riscv")]
1438            RelocKind::RvCBranch8 => {
1439                // RISC-V C-extension CB-type branch: 9-bit signed PC-relative offset
1440                // CB-type immediate: imm[8|4:3] in bits 12:10, imm[7:6|2:1|5] in bits 6:2
1441                let rel = (target_addr as i64)
1442                    .wrapping_sub(reloc_abs as i64)
1443                    .wrapping_add(reloc.addend);
1444                if !(-(1i64 << 8)..(1i64 << 8)).contains(&rel) {
1445                    return Err(AsmError::BranchOutOfRange {
1446                        label: reloc.label.to_string(),
1447                        disp: rel,
1448                        max: (1 << 8) - 2,
1449                        span,
1450                    });
1451                }
1452                scaled_displacement(rel, 2, &reloc.label, span)?;
1453                let imm = rel as u16;
1454                // Reconstruct the CB-type halfword with the new offset
1455                let mut hw = read_le16(bytes, reloc.offset, &reloc.label, span)?;
1456                // Clear the immediate fields: bits 12:10 and 6:2
1457                hw &= 0xE383; // keep funct3(15:13), rs1'(9:7), op(1:0)
1458                              // Pack: bit8→12, bit4:3→11:10, bit7:6→6:5, bit2:1→4:3, bit5→2
1459                hw |= ((imm >> 8) & 1) << 12;
1460                hw |= ((imm >> 3) & 3) << 10;
1461                hw |= ((imm >> 6) & 3) << 5;
1462                hw |= ((imm >> 1) & 3) << 3;
1463                hw |= ((imm >> 5) & 1) << 2;
1464                bytes[reloc.offset..reloc.offset + 2].copy_from_slice(&hw.to_le_bytes());
1465            }
1466            #[cfg(feature = "riscv")]
1467            RelocKind::RvCJump11 => {
1468                // RISC-V C-extension CJ-type jump: 12-bit signed PC-relative offset
1469                // CJ-type immediate: imm[11|4|9:8|10|6|7|3:1|5] in bits 12:2
1470                let rel = (target_addr as i64)
1471                    .wrapping_sub(reloc_abs as i64)
1472                    .wrapping_add(reloc.addend);
1473                if !(-(1i64 << 11)..(1i64 << 11)).contains(&rel) {
1474                    return Err(AsmError::BranchOutOfRange {
1475                        label: reloc.label.to_string(),
1476                        disp: rel,
1477                        max: (1 << 11) - 2,
1478                        span,
1479                    });
1480                }
1481                scaled_displacement(rel, 2, &reloc.label, span)?;
1482                let imm = rel as u16;
1483                let mut hw = read_le16(bytes, reloc.offset, &reloc.label, span)?;
1484                // Clear immediate fields: bits 12:2
1485                hw &= 0xE003; // keep funct3(15:13) and op(1:0)
1486                              // Pack: bit11→12, bit4→11, bit9:8→10:9, bit10→8, bit6→7, bit7→6, bit3:1→5:3, bit5→2
1487                hw |= ((imm >> 11) & 1) << 12;
1488                hw |= ((imm >> 4) & 1) << 11;
1489                hw |= ((imm >> 8) & 3) << 9;
1490                hw |= ((imm >> 10) & 1) << 8;
1491                hw |= ((imm >> 6) & 1) << 7;
1492                hw |= ((imm >> 7) & 1) << 6;
1493                hw |= ((imm >> 1) & 7) << 3;
1494                hw |= ((imm >> 5) & 1) << 2;
1495                bytes[reloc.offset..reloc.offset + 2].copy_from_slice(&hw.to_le_bytes());
1496            }
1497        }
1498        Ok(())
1499    }
1500
1501    // ── label resolution ───────────────────────────────────
1502
1503    fn resolve_label_with_offsets(
1504        &self,
1505        name: &str,
1506        from_fragment: usize,
1507        offsets: &[u64],
1508    ) -> Result<u64, AsmError> {
1509        // Constants
1510        if let Some(&value) = self.constants.get(name) {
1511            // Treat constant as a signed value that fits the target address space.
1512            // For negative constants, we rely on wrapping semantics (e.g., -1 → 0xFFFF_FFFF_FFFF_FFFF).
1513            return Ok(value as i64 as u64);
1514        }
1515        // Externals
1516        if let Some(&addr) = self.externals.get(name) {
1517            return Ok(addr);
1518        }
1519        // Numeric labels (e.g. "1f", "1b")
1520        if name.len() >= 2 {
1521            let last = name.as_bytes()[name.len() - 1];
1522            let num_part = &name[..name.len() - 1];
1523            if last == b'f' || last == b'b' {
1524                if let Ok(n) = num_part.parse::<u32>() {
1525                    return self.resolve_numeric_with_offsets(
1526                        n,
1527                        from_fragment,
1528                        last == b'f',
1529                        offsets,
1530                    );
1531                }
1532            }
1533        }
1534        // Named labels
1535        if let Some(def) = self.labels.get(name) {
1536            return Ok(offsets[def.fragment_index]);
1537        }
1538
1539        Err(AsmError::UndefinedLabel {
1540            label: String::from(name),
1541            span: Span::new(0, 0, 0, 0),
1542        })
1543    }
1544
1545    fn resolve_numeric_with_offsets(
1546        &self,
1547        num: u32,
1548        from_fragment: usize,
1549        forward: bool,
1550        offsets: &[u64],
1551    ) -> Result<u64, AsmError> {
1552        if let Some(defs) = self.numeric.defs.get(&num) {
1553            if forward {
1554                for &def_idx in defs {
1555                    if def_idx > from_fragment {
1556                        return Ok(offsets[def_idx]);
1557                    }
1558                }
1559            } else {
1560                for &def_idx in defs.iter().rev() {
1561                    if def_idx <= from_fragment {
1562                        return Ok(offsets[def_idx]);
1563                    }
1564                }
1565            }
1566        }
1567        Err(AsmError::UndefinedLabel {
1568            label: alloc::format!("{}{}", num, if forward { 'f' } else { 'b' }),
1569            span: Span::new(0, 0, 0, 0),
1570        })
1571    }
1572}
1573
1574// ─── Multi-byte NOP padding (x86/x86-64) ──────────────────
1575
1576/// Intel-recommended multi-byte NOP instruction sequences.
1577///
1578/// These are architecturally guaranteed to behave as NOPs on all modern
1579/// x86/x86-64 processors and execute in a single cycle on most
1580/// microarchitectures.
1581const NOP_SEQUENCES: [&[u8]; 10] = [
1582    &[],                                                     // 0 bytes (unused)
1583    &[0x90],                                                 // 1 byte : NOP
1584    &[0x66, 0x90],                                           // 2 bytes: 66 NOP
1585    &[0x0F, 0x1F, 0x00],                                     // 3 bytes: NOP DWORD ptr [EAX]
1586    &[0x0F, 0x1F, 0x40, 0x00],                               // 4 bytes: NOP DWORD ptr [EAX + 00H]
1587    &[0x0F, 0x1F, 0x44, 0x00, 0x00], // 5 bytes: NOP DWORD ptr [EAX + EAX*1 + 00H]
1588    &[0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00], // 6 bytes: 66 NOP DWORD ptr [EAX + EAX*1 + 00H]
1589    &[0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00], // 7 bytes: NOP DWORD ptr [EAX + 00000000H]
1590    &[0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00], // 8 bytes: NOP DWORD ptr [EAX + EAX*1 + 00000000H]
1591    &[0x66, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00], // 9 bytes: 66 NOP DWORD ptr [EAX + EAX*1 + 00000000H]
1592];
1593
1594/// Emit optimal multi-byte NOP padding of exactly `n` bytes.
1595///
1596/// Uses the largest available NOP sequences first, then fills the
1597/// remainder with smaller ones.
1598fn emit_nop_padding(output: &mut Vec<u8>, mut n: usize) {
1599    while n > 0 {
1600        let chunk = core::cmp::min(n, 9);
1601        output.extend_from_slice(NOP_SEQUENCES[chunk]);
1602        n -= chunk;
1603    }
1604}
1605
1606// ─── Tests ─────────────────────────────────────────────────
1607
1608#[cfg(test)]
1609mod tests {
1610    use super::*;
1611
1612    fn span() -> Span {
1613        Span::new(1, 1, 0, 0)
1614    }
1615
1616    fn fixed(bytes: Vec<u8>, reloc: Option<Relocation>) -> Fragment {
1617        Fragment::Fixed {
1618            bytes: FragmentBytes::Heap(bytes),
1619            relocation: reloc,
1620            span: span(),
1621        }
1622    }
1623
1624    fn nop() -> Fragment {
1625        fixed(vec![0x90], None)
1626    }
1627
1628    fn relaxable_jmp(label: &str) -> Fragment {
1629        Fragment::Relaxable {
1630            short_bytes: InstrBytes::from_slice(&[0xEB, 0x00]),
1631            short_reloc_offset: 1,
1632            short_relocation: None,
1633            long_bytes: InstrBytes::from_slice(&[0xE9, 0, 0, 0, 0]),
1634            long_relocation: Relocation {
1635                offset: 1,
1636                size: 4,
1637                label: alloc::rc::Rc::from(label),
1638                kind: RelocKind::X86Relative,
1639                addend: 0,
1640                trailing_bytes: 0,
1641            },
1642            is_long: false,
1643            span: span(),
1644        }
1645    }
1646
1647    fn relaxable_jcc(cc: u8, label: &str) -> Fragment {
1648        Fragment::Relaxable {
1649            short_bytes: InstrBytes::from_slice(&[0x70 + cc, 0x00]),
1650            short_reloc_offset: 1,
1651            short_relocation: None,
1652            long_bytes: InstrBytes::from_slice(&[0x0F, 0x80 + cc, 0, 0, 0, 0]),
1653            long_relocation: Relocation {
1654                offset: 2,
1655                size: 4,
1656                label: alloc::rc::Rc::from(label),
1657                kind: RelocKind::X86Relative,
1658                addend: 0,
1659                trailing_bytes: 0,
1660            },
1661            is_long: false,
1662            span: span(),
1663        }
1664    }
1665
1666    // ── Basic label resolution (fixed fragments) ────────
1667
1668    #[test]
1669    fn resolve_forward_label() {
1670        let mut linker = Linker::new();
1671        linker.add_fragment(fixed(
1672            vec![0xE9, 0, 0, 0, 0],
1673            Some(Relocation {
1674                offset: 1,
1675                size: 4,
1676                label: alloc::rc::Rc::from("target"),
1677                kind: RelocKind::X86Relative,
1678                addend: 0,
1679                trailing_bytes: 0,
1680            }),
1681        ));
1682        linker.add_label("target", span()).unwrap();
1683        linker.add_fragment(nop());
1684
1685        let (output, _, _, _) = linker.resolve().unwrap();
1686        assert_eq!(output, vec![0xE9, 0x00, 0x00, 0x00, 0x00, 0x90]);
1687    }
1688
1689    #[test]
1690    fn resolve_backward_label() {
1691        let mut linker = Linker::new();
1692        linker.add_label("top", span()).unwrap();
1693        linker.add_fragment(nop());
1694        linker.add_fragment(fixed(
1695            vec![0xE9, 0, 0, 0, 0],
1696            Some(Relocation {
1697                offset: 1,
1698                size: 4,
1699                label: alloc::rc::Rc::from("top"),
1700                kind: RelocKind::X86Relative,
1701                addend: 0,
1702                trailing_bytes: 0,
1703            }),
1704        ));
1705
1706        let (output, _, _, _) = linker.resolve().unwrap();
1707        let rel = i32::from_le_bytes([output[2], output[3], output[4], output[5]]);
1708        assert_eq!(rel, -6);
1709    }
1710
1711    #[test]
1712    fn resolve_with_base_address() {
1713        let mut linker = Linker::new();
1714        linker.set_base_address(0x1000);
1715        linker.add_fragment(fixed(
1716            vec![0xE9, 0, 0, 0, 0],
1717            Some(Relocation {
1718                offset: 1,
1719                size: 4,
1720                label: alloc::rc::Rc::from("target"),
1721                kind: RelocKind::X86Relative,
1722                addend: 0,
1723                trailing_bytes: 0,
1724            }),
1725        ));
1726        linker.add_label("target", span()).unwrap();
1727        linker.add_fragment(nop());
1728
1729        let (output, _, _, _) = linker.resolve().unwrap();
1730        assert_eq!(output, vec![0xE9, 0x00, 0x00, 0x00, 0x00, 0x90]);
1731    }
1732
1733    #[test]
1734    fn resolve_external_label() {
1735        let mut linker = Linker::new();
1736        linker.define_external("printf", 0xDEAD_BEEF);
1737        linker.add_fragment(fixed(
1738            vec![0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0],
1739            Some(Relocation {
1740                offset: 2,
1741                size: 8,
1742                label: alloc::rc::Rc::from("printf"),
1743                kind: RelocKind::Absolute,
1744                addend: 0,
1745                trailing_bytes: 0,
1746            }),
1747        ));
1748
1749        let (output, _, _, _) = linker.resolve().unwrap();
1750        assert_eq!(output[2..10], 0xDEAD_BEEFu64.to_le_bytes());
1751    }
1752
1753    #[test]
1754    fn resolve_constant() {
1755        let mut linker = Linker::new();
1756        linker.define_constant("SYS_WRITE", 1);
1757        linker.add_fragment(fixed(
1758            vec![0xB8, 0, 0, 0, 0],
1759            Some(Relocation {
1760                offset: 1,
1761                size: 4,
1762                label: alloc::rc::Rc::from("SYS_WRITE"),
1763                kind: RelocKind::Absolute,
1764                addend: 0,
1765                trailing_bytes: 0,
1766            }),
1767        ));
1768
1769        let (output, _, _, _) = linker.resolve().unwrap();
1770        assert_eq!(output, vec![0xB8, 0x01, 0x00, 0x00, 0x00]);
1771    }
1772
1773    #[test]
1774    fn duplicate_label_error() {
1775        let mut linker = Linker::new();
1776        linker.add_label("foo", span()).unwrap();
1777        linker.add_fragment(nop());
1778        let err = linker.add_label("foo", span()).unwrap_err();
1779        assert!(matches!(err, AsmError::DuplicateLabel { .. }));
1780    }
1781
1782    #[test]
1783    fn undefined_label_error() {
1784        let mut linker = Linker::new();
1785        linker.add_fragment(fixed(
1786            vec![0xE9, 0, 0, 0, 0],
1787            Some(Relocation {
1788                offset: 1,
1789                size: 4,
1790                label: alloc::rc::Rc::from("nowhere"),
1791                kind: RelocKind::X86Relative,
1792                addend: 0,
1793                trailing_bytes: 0,
1794            }),
1795        ));
1796        let err = linker.resolve().unwrap_err();
1797        assert!(matches!(err, AsmError::UndefinedLabel { .. }));
1798    }
1799
1800    // ── Numeric labels ────────
1801
1802    #[test]
1803    fn numeric_label_forward() {
1804        let mut linker = Linker::new();
1805        linker.add_fragment(fixed(
1806            vec![0xE9, 0, 0, 0, 0],
1807            Some(Relocation {
1808                offset: 1,
1809                size: 4,
1810                label: alloc::rc::Rc::from("1f"),
1811                kind: RelocKind::X86Relative,
1812                addend: 0,
1813                trailing_bytes: 0,
1814            }),
1815        ));
1816        linker.add_label("1", span()).unwrap();
1817        linker.add_fragment(nop());
1818
1819        let (output, _, _, _) = linker.resolve().unwrap();
1820        assert_eq!(&output[1..5], &[0, 0, 0, 0]);
1821    }
1822
1823    #[test]
1824    fn numeric_label_backward() {
1825        let mut linker = Linker::new();
1826        linker.add_label("1", span()).unwrap();
1827        linker.add_fragment(nop());
1828        linker.add_fragment(fixed(
1829            vec![0xE9, 0, 0, 0, 0],
1830            Some(Relocation {
1831                offset: 1,
1832                size: 4,
1833                label: alloc::rc::Rc::from("1b"),
1834                kind: RelocKind::X86Relative,
1835                addend: 0,
1836                trailing_bytes: 0,
1837            }),
1838        ));
1839
1840        let (output, _, _, _) = linker.resolve().unwrap();
1841        let rel = i32::from_le_bytes([output[2], output[3], output[4], output[5]]);
1842        assert_eq!(rel, -6);
1843    }
1844
1845    // ── Branch relaxation ────────
1846
1847    #[test]
1848    fn relaxation_short_jmp_forward() {
1849        let mut linker = Linker::new();
1850        linker.add_fragment(relaxable_jmp("target"));
1851        linker.add_label("target", span()).unwrap();
1852        linker.add_fragment(nop());
1853
1854        let (output, _, _, _) = linker.resolve().unwrap();
1855        // Short form: EB 00 90
1856        assert_eq!(output, vec![0xEB, 0x00, 0x90]);
1857    }
1858
1859    #[test]
1860    fn relaxation_short_jmp_backward() {
1861        let mut linker = Linker::new();
1862        linker.add_label("top", span()).unwrap();
1863        linker.add_fragment(nop());
1864        linker.add_fragment(relaxable_jmp("top"));
1865
1866        let (output, _, _, _) = linker.resolve().unwrap();
1867        // top=0, nop@0 (1B), jmp_short@1 (2B), frag_end=3, disp=0-3=-3
1868        assert_eq!(output, vec![0x90, 0xEB, 0xFD]);
1869    }
1870
1871    #[test]
1872    fn relaxation_promotes_jmp_to_long() {
1873        let mut linker = Linker::new();
1874        linker.add_fragment(relaxable_jmp("target"));
1875        linker.add_fragment(fixed(vec![0x90; 200], None));
1876        linker.add_label("target", span()).unwrap();
1877        linker.add_fragment(nop());
1878
1879        let (output, _, _, _) = linker.resolve().unwrap();
1880        assert_eq!(output[0], 0xE9); // long form
1881        assert_eq!(output.len(), 5 + 200 + 1);
1882        let rel = i32::from_le_bytes([output[1], output[2], output[3], output[4]]);
1883        assert_eq!(rel, 200);
1884    }
1885
1886    #[test]
1887    fn relaxation_short_jcc() {
1888        let mut linker = Linker::new();
1889        linker.add_fragment(relaxable_jcc(0x4, "done")); // je
1890        linker.add_label("done", span()).unwrap();
1891        linker.add_fragment(nop());
1892
1893        let (output, _, _, _) = linker.resolve().unwrap();
1894        assert_eq!(output, vec![0x74, 0x00, 0x90]);
1895    }
1896
1897    #[test]
1898    fn relaxation_promotes_jcc_to_long() {
1899        let mut linker = Linker::new();
1900        linker.add_fragment(relaxable_jcc(0x4, "done"));
1901        linker.add_fragment(fixed(vec![0x90; 200], None));
1902        linker.add_label("done", span()).unwrap();
1903        linker.add_fragment(nop());
1904
1905        let (output, _, _, _) = linker.resolve().unwrap();
1906        assert_eq!(output[0], 0x0F);
1907        assert_eq!(output[1], 0x84);
1908        let rel = i32::from_le_bytes([output[2], output[3], output[4], output[5]]);
1909        assert_eq!(rel, 200);
1910    }
1911
1912    #[test]
1913    fn relaxation_boundary_127() {
1914        // Exactly 127 bytes displacement: should stay short
1915        let mut linker = Linker::new();
1916        linker.add_fragment(relaxable_jmp("target"));
1917        linker.add_fragment(fixed(vec![0x90; 125], None)); // 2 + 125 = 127
1918        linker.add_label("target", span()).unwrap();
1919        linker.add_fragment(nop());
1920
1921        let (output, _, _, _) = linker.resolve().unwrap();
1922        assert_eq!(output[0], 0xEB); // still short
1923        assert_eq!(output[1], 125u8); // disp = 127 - 2 = 125
1924    }
1925
1926    #[test]
1927    fn relaxation_boundary_128() {
1928        // 128 bytes displacement: must go long
1929        // short jmp = 2B, 128 NOPs: target at 130, frag_end at 2, disp = 128 > 127 → promote
1930        let mut linker = Linker::new();
1931        linker.add_fragment(relaxable_jmp("target"));
1932        linker.add_fragment(fixed(vec![0x90; 128], None));
1933        linker.add_label("target", span()).unwrap();
1934        linker.add_fragment(nop());
1935
1936        let (output, _, _, _) = linker.resolve().unwrap();
1937        assert_eq!(output[0], 0xE9); // promoted to long
1938    }
1939
1940    #[test]
1941    fn cascading_relaxation() {
1942        // Two branches where expanding the second forces the first to expand too.
1943        let mut linker = Linker::new();
1944
1945        // jmp L1 (2 or 5 bytes)
1946        linker.add_fragment(relaxable_jmp("L1"));
1947        // 125 NOPs
1948        linker.add_fragment(fixed(vec![0x90; 125], None));
1949        // jne L2 (2 or 6 bytes)
1950        linker.add_fragment(relaxable_jcc(0x5, "L2"));
1951
1952        linker.add_label("L1", span()).unwrap();
1953        linker.add_fragment(fixed(vec![0x90; 130], None));
1954        linker.add_label("L2", span()).unwrap();
1955        linker.add_fragment(nop());
1956
1957        let (output, _, _, _) = linker.resolve().unwrap();
1958        // Both should be long form due to cascading
1959        assert_eq!(output[0], 0xE9); // jmp rel32
1960        assert_eq!(output[5 + 125], 0x0F); // jne rel32
1961        assert_eq!(output[5 + 125 + 1], 0x85);
1962    }
1963
1964    // ── Alignment ────────
1965
1966    #[test]
1967    fn alignment_fragment() {
1968        let mut linker = Linker::new();
1969        linker.add_fragment(nop()); // 1 byte
1970        linker.add_alignment(4, 0x00, None, false, span());
1971        linker.add_fragment(nop());
1972
1973        let (output, _, _, _) = linker.resolve().unwrap();
1974        assert_eq!(output, vec![0x90, 0x00, 0x00, 0x00, 0x90]);
1975    }
1976
1977    #[test]
1978    fn alignment_already_aligned() {
1979        let mut linker = Linker::new();
1980        linker.add_fragment(fixed(vec![0x90; 4], None));
1981        linker.add_alignment(4, 0xCC, None, false, span());
1982        linker.add_fragment(nop());
1983
1984        let (output, _, _, _) = linker.resolve().unwrap();
1985        assert_eq!(output, vec![0x90, 0x90, 0x90, 0x90, 0x90]);
1986    }
1987
1988    #[test]
1989    fn alignment_with_base_address() {
1990        let mut linker = Linker::new();
1991        linker.set_base_address(0x1001); // base is 1 past alignment
1992        linker.add_alignment(4, 0xCC, None, false, span());
1993        linker.add_fragment(nop());
1994
1995        let (output, _, _, _) = linker.resolve().unwrap();
1996        // Needs 3 bytes padding to reach 0x1004
1997        assert_eq!(output, vec![0xCC, 0xCC, 0xCC, 0x90]);
1998    }
1999
2000    // ── Label table ────────
2001
2002    #[test]
2003    fn label_table_exported() {
2004        let mut linker = Linker::new();
2005        linker.add_label("start", span()).unwrap();
2006        linker.add_fragment(nop());
2007        linker.add_fragment(nop());
2008        linker.add_label("end", span()).unwrap();
2009        linker.add_fragment(nop());
2010
2011        let (_, labels, _, _) = linker.resolve().unwrap();
2012        let m: BTreeMap<String, u64> = labels.into_iter().collect();
2013        assert_eq!(m["start"], 0);
2014        assert_eq!(m["end"], 2);
2015    }
2016
2017    #[test]
2018    fn label_table_with_base_address() {
2019        let mut linker = Linker::new();
2020        linker.set_base_address(0x1000);
2021        linker.add_label("func", span()).unwrap();
2022        linker.add_fragment(fixed(vec![0x90; 10], None));
2023
2024        let (_, labels, _, _) = linker.resolve().unwrap();
2025        assert_eq!(labels[0].1, 0x1000);
2026    }
2027
2028    // ── Misc ────────
2029
2030    #[test]
2031    fn multiple_fragments_no_reloc() {
2032        let mut linker = Linker::new();
2033        linker.add_fragment(nop());
2034        linker.add_fragment(fixed(vec![0xCC], None));
2035        linker.add_fragment(fixed(vec![0xC3], None));
2036        let (output, _, _, _) = linker.resolve().unwrap();
2037        assert_eq!(output, vec![0x90, 0xCC, 0xC3]);
2038    }
2039
2040    #[test]
2041    fn empty_linker() {
2042        let mut linker = Linker::new();
2043        let (output, labels, _, _) = linker.resolve().unwrap();
2044        assert!(output.is_empty());
2045        assert!(labels.is_empty());
2046    }
2047
2048    #[test]
2049    fn relocation_with_addend() {
2050        let mut linker = Linker::new();
2051        linker.add_label("data", span()).unwrap();
2052        linker.add_fragment(fixed(vec![0; 16], None));
2053        linker.add_fragment(fixed(
2054            vec![0x48, 0x8D, 0x05, 0, 0, 0, 0],
2055            Some(Relocation {
2056                offset: 3,
2057                size: 4,
2058                label: alloc::rc::Rc::from("data"),
2059                kind: RelocKind::X86Relative,
2060                addend: 4,
2061                trailing_bytes: 0,
2062            }),
2063        ));
2064
2065        let (output, _, _, _) = linker.resolve().unwrap();
2066        let rel = i32::from_le_bytes([output[19], output[20], output[21], output[22]]);
2067        assert_eq!(rel, -19);
2068    }
2069
2070    // ── Relaxation + alignment interaction ────────
2071
2072    #[test]
2073    fn relaxation_with_alignment() {
2074        let mut linker = Linker::new();
2075        linker.add_label("top", span()).unwrap();
2076        linker.add_fragment(nop()); // 1 byte
2077        linker.add_alignment(16, 0xCC, None, false, span()); // pad to 16
2078                                                             // jne top — after alignment, offset = 16, so disp = 0-18 = -18 for short (or -22 for long)
2079        linker.add_fragment(relaxable_jcc(0x5, "top"));
2080
2081        let (output, _, _, _) = linker.resolve().unwrap();
2082        // 1 NOP + 15 padding = 16 bytes. With short jne (2B): disp = 0 - 18 = -18, fits in rel8.
2083        assert_eq!(output[0], 0x90);
2084        assert_eq!(output[16], 0x75); // short jne
2085        let disp = output[17] as i8;
2086        assert_eq!(disp, -18);
2087    }
2088
2089    // ── add_encoded helper ────────
2090
2091    #[test]
2092    fn add_encoded_creates_relaxable() {
2093        let mut linker = Linker::new();
2094        linker
2095            .add_encoded(
2096                InstrBytes::from_slice(&[0xE9, 0, 0, 0, 0]),
2097                Some(Relocation {
2098                    offset: 1,
2099                    size: 4,
2100                    label: alloc::rc::Rc::from("target"),
2101                    kind: RelocKind::X86Relative,
2102                    addend: 0,
2103                    trailing_bytes: 0,
2104                }),
2105                Some(RelaxInfo {
2106                    short_bytes: InstrBytes::from_slice(&[0xEB, 0x00]),
2107                    short_reloc_offset: 1,
2108                    short_relocation: None,
2109                }),
2110                span(),
2111            )
2112            .unwrap();
2113        linker.add_label("target", span()).unwrap();
2114        linker.add_fragment(nop());
2115
2116        let (output, _, _, _) = linker.resolve().unwrap();
2117        assert_eq!(output, vec![0xEB, 0x00, 0x90]);
2118    }
2119
2120    #[test]
2121    fn add_encoded_creates_fixed() {
2122        let mut linker = Linker::new();
2123        linker
2124            .add_encoded(InstrBytes::from_slice(&[0x90]), None, None, span())
2125            .unwrap();
2126
2127        let (output, _, _, _) = linker.resolve().unwrap();
2128        assert_eq!(output, vec![0x90]);
2129    }
2130
2131    // ── Multi-byte NOP alignment ────────
2132
2133    #[test]
2134    fn alignment_with_nop_padding() {
2135        let mut linker = Linker::new();
2136        linker.add_fragment(nop()); // 1 byte at offset 0
2137                                    // Align to 4 bytes with NOP padding (use_nop=true)
2138        linker.add_alignment(4, 0x00, None, true, span());
2139        linker.add_fragment(nop());
2140
2141        let (output, _, _, _) = linker.resolve().unwrap();
2142        // 1 NOP + 3-byte NOP + 1 NOP = 5 bytes
2143        assert_eq!(output.len(), 5);
2144        assert_eq!(output[0], 0x90); // 1-byte NOP
2145                                     // Next 3 bytes should be the Intel 3-byte NOP: 0F 1F 00
2146        assert_eq!(&output[1..4], &[0x0F, 0x1F, 0x00]);
2147        assert_eq!(output[4], 0x90); // final NOP
2148    }
2149
2150    #[test]
2151    fn alignment_nop_padding_large() {
2152        let mut linker = Linker::new();
2153        linker.add_fragment(nop()); // 1 byte at offset 0
2154                                    // Align to 16 bytes with NOP padding
2155        linker.add_alignment(16, 0x00, None, true, span());
2156        linker.add_fragment(nop());
2157
2158        let (output, _, _, _) = linker.resolve().unwrap();
2159        // 1 + 15 padding + 1 = 17 bytes
2160        assert_eq!(output.len(), 17);
2161        assert_eq!(output[0], 0x90);
2162        // 15 bytes of NOP padding: 9-byte NOP + 6-byte NOP
2163        assert_eq!(
2164            &output[1..10],
2165            &[0x66, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00]
2166        );
2167        assert_eq!(&output[10..16], &[0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00]);
2168        assert_eq!(output[16], 0x90);
2169    }
2170
2171    #[test]
2172    fn alignment_max_skip_respected() {
2173        let mut linker = Linker::new();
2174        linker.add_fragment(nop()); // 1 byte at offset 0
2175                                    // Align to 16, but max_skip = 2 — padding needed is 15, which exceeds 2
2176        linker.add_alignment(16, 0x00, Some(2), false, span());
2177        linker.add_fragment(nop());
2178
2179        let (output, _, _, _) = linker.resolve().unwrap();
2180        // Alignment should be skipped — just 2 NOPs
2181        assert_eq!(output, vec![0x90, 0x90]);
2182    }
2183
2184    #[test]
2185    fn alignment_max_skip_allows_small_padding() {
2186        let mut linker = Linker::new();
2187        linker.add_fragment(fixed(vec![0x90; 3], None)); // 3 bytes
2188                                                         // Align to 4 with max_skip = 2 — padding needed is 1, which is ≤ 2
2189        linker.add_alignment(4, 0xCC, Some(2), false, span());
2190        linker.add_fragment(nop());
2191
2192        let (output, _, _, _) = linker.resolve().unwrap();
2193        assert_eq!(output, vec![0x90, 0x90, 0x90, 0xCC, 0x90]);
2194    }
2195
2196    // ── .org directive ────────
2197
2198    #[test]
2199    fn org_forward_padding() {
2200        let mut linker = Linker::new();
2201        linker.set_base_address(0x100);
2202        linker.add_fragment(nop()); // 1 byte at 0x100
2203        linker.add_org(0x110, 0x00, span()); // advance to 0x110
2204        linker.add_fragment(nop());
2205
2206        let (output, _, _, _) = linker.resolve().unwrap();
2207        // 1 NOP + 15 zero-fill + 1 NOP = 17 bytes
2208        assert_eq!(output.len(), 17);
2209        assert_eq!(output[0], 0x90);
2210        // 15 zero bytes
2211        assert!(output[1..16].iter().all(|&b| b == 0x00));
2212        assert_eq!(output[16], 0x90);
2213    }
2214
2215    #[test]
2216    fn org_already_at_target() {
2217        let mut linker = Linker::new();
2218        linker.set_base_address(0x100);
2219        linker.add_fragment(fixed(vec![0x90; 16], None)); // exactly at 0x110
2220        linker.add_org(0x110, 0x00, span()); // already there
2221        linker.add_fragment(nop());
2222
2223        let (output, _, _, _) = linker.resolve().unwrap();
2224        assert_eq!(output.len(), 17); // 16 + 0 padding + 1
2225    }
2226
2227    #[test]
2228    fn org_backward_error() {
2229        let mut linker = Linker::new();
2230        linker.set_base_address(0x200);
2231        linker.add_fragment(fixed(vec![0x90; 16], None)); // at 0x210
2232        linker.add_org(0x100, 0x00, span()); // behind!
2233
2234        let err = linker.resolve().unwrap_err();
2235        assert!(matches!(err, AsmError::Syntax { .. }));
2236    }
2237
2238    #[test]
2239    fn org_with_labels() {
2240        let mut linker = Linker::new();
2241        linker.set_base_address(0x1000);
2242        linker.add_fragment(nop());
2243        linker.add_org(0x1010, 0x00, span());
2244        linker.add_label("after_org", span()).unwrap();
2245        linker.add_fragment(nop());
2246
2247        let (_, labels, _, _) = linker.resolve().unwrap();
2248        let m: BTreeMap<String, u64> = labels.into_iter().collect();
2249        assert_eq!(m["after_org"], 0x1010);
2250    }
2251
2252    // === 8th Audit: Branch relaxation with addend ===
2253
2254    #[test]
2255    fn relaxable_jmp_with_positive_addend() {
2256        // target: (offset 0)
2257        //   nop   (1 byte)
2258        //   jmp target+1  (short: EB xx, 2 bytes)
2259        // frag_end = 1 + 2 = 3.  target=0.  addend=1.
2260        // disp = 0 + 1 - 3 = -2  → short fits, should encode [EB FE]
2261        let mut linker = Linker::new();
2262        linker.add_label("target", span()).unwrap();
2263        linker.add_fragment(nop());
2264        linker.add_fragment(Fragment::Relaxable {
2265            short_bytes: InstrBytes::from_slice(&[0xEB, 0x00]),
2266            short_reloc_offset: 1,
2267            short_relocation: None,
2268            long_bytes: InstrBytes::from_slice(&[0xE9, 0, 0, 0, 0]),
2269            long_relocation: Relocation {
2270                offset: 1,
2271                size: 4,
2272                label: alloc::rc::Rc::from("target"),
2273                kind: RelocKind::X86Relative,
2274                addend: 1,
2275                trailing_bytes: 0,
2276            },
2277            is_long: false,
2278            span: span(),
2279        });
2280
2281        let (output, _, _, _) = linker.resolve().unwrap();
2282        // nop + jmp target+1 → [0x90, 0xEB, 0xFE]
2283        assert_eq!(output, vec![0x90, 0xEB, 0xFE_u8]); // -2 as i8 = 0xFE
2284    }
2285
2286    #[test]
2287    fn relaxable_jmp_addend_forces_long_form() {
2288        // target: (offset 0)
2289        //   .space 126  (126 bytes of NOP)
2290        //   jmp target - 200  (addend = -200)
2291        // frag_end = 126 + 2 = 128.  target=0.  addend=-200.
2292        // disp = 0 + (-200) - 128 = -328  → doesn't fit rel8, must use long form
2293        let mut linker = Linker::new();
2294        linker.add_label("target", span()).unwrap();
2295        // Add 126 bytes of padding
2296        linker.add_fragment(fixed(vec![0x90; 126], None));
2297        linker.add_fragment(Fragment::Relaxable {
2298            short_bytes: InstrBytes::from_slice(&[0xEB, 0x00]),
2299            short_reloc_offset: 1,
2300            short_relocation: None,
2301            long_bytes: InstrBytes::from_slice(&[0xE9, 0, 0, 0, 0]),
2302            long_relocation: Relocation {
2303                offset: 1,
2304                size: 4,
2305                label: alloc::rc::Rc::from("target"),
2306                kind: RelocKind::X86Relative,
2307                addend: -200,
2308                trailing_bytes: 0,
2309            },
2310            is_long: false,
2311            span: span(),
2312        });
2313
2314        let (output, _, _, _) = linker.resolve().unwrap();
2315        // Should have been promoted to long form: 0xE9 + 4 bytes
2316        assert_eq!(output.len(), 126 + 5); // 126 nops + 5-byte jmp
2317                                           // Long form: disp32 = target + addend - frag_end = 0 + (-200) - (126+5) = -331
2318        let disp = i32::from_le_bytes([output[127], output[128], output[129], output[130]]);
2319        assert_eq!(disp, -331);
2320    }
2321}