Skip to content

[Backend][Diagnostics] Preserve generated assembly line provenance in internal assembler errors #21

Description

@a19q3

Summary

The internal assembler now correctly performs B-type branch relaxation, but most parser, layout, encoding, and machine-CFG failures still construct CompileError with Span::default().

As a result, a malformed or unsupported generated assembly operation can still surface as line 0, even when the assembler knows the input line, operation index, section, label, and byte offset. This was a secondary finding in #1 and remains unresolved after the primary branch-range bug was fixed.

Current evidence

  • The assembler parses source lines but stores plain AsmOp values without their originating line number:
    fn from_lines_relaxed(lines: &[String], layout: &SectionLayout) -> Result<Self> {
    let conservative = Self::from_lines_with_branch_mode(lines, BranchSizeMode::Conservative)?;
    let relaxed_text_branches = conservative.relaxed_branch_indices(layout)?;
    Self::from_lines_with_branch_mode(lines, BranchSizeMode::Exact(&relaxed_text_branches))
    }
    fn from_lines_with_branch_mode(lines: &[String], branch_size_mode: BranchSizeMode<'_>) -> Result<Self> {
    let mut current_section = SectionKind::Text;
    let mut text_size = 0usize;
    let mut rodata_size = 0usize;
    let mut text_ops = Vec::new();
    let mut rodata_ops = Vec::new();
    let mut symbols = HashMap::new();
    let mut globals = BTreeSet::new();
    let mut entry_label = None;
    let mut fallback_entry = None;
    for line in lines {
    let Some(clean) = strip_comment(line) else {
    continue;
    };
    if clean.is_empty() {
    continue;
    }
    if let Some(section) = parse_section_directive(clean)? {
    current_section = section;
    continue;
    }
    if clean.starts_with(".option ") || clean.starts_with(".type ") {
    continue;
    }
    if let Some(symbol) = clean.strip_prefix(".global ") {
    globals.insert(symbol.trim().to_string());
    continue;
    }
    let (ops, offset) = match current_section {
    SectionKind::Text => (&mut text_ops, &mut text_size),
    SectionKind::Rodata => (&mut rodata_ops, &mut rodata_size),
    };
    let op_index = ops.len();
    if let Some(label) = clean.strip_suffix(':') {
    let label = label.trim().to_string();
    let symbol = SymbolDef { section: current_section, offset: *offset };
    if symbols.insert(label.clone(), symbol).is_some() {
    return Err(CompileError::new(format!("duplicate assembly label '{}'", label), crate::error::Span::default()));
    }
    if current_section == SectionKind::Text && globals.contains(&label) {
    if fallback_entry.is_none() {
    fallback_entry = Some(label.clone());
    }
    if !label.starts_with("__") && entry_label.is_none() {
    entry_label = Some(label.clone());
    }
    }
    ops.push(AsmOp::Label(label));
    continue;
    }
    let op = parse_asm_op(clean)?;
    *offset += op_size(&op, *offset, current_section, op_index, branch_size_mode);
    ops.push(op);
    }
  • Unknown labels, unsupported instructions, invalid immediates, encoding failures, and CFG/layout errors still use Span::default(): https://github.com/CellScript-Labs/CellScript/blob/8ae6dc4a2749fa107ff42c166772600648c19bb7/src/codegen/assembler.rs
  • Branch relaxation itself is covered and passing:
    fn internal_assembler_relaxes_out_of_range_conditional_branch() {
    let mut lines = vec![
    ".section .text".to_string(),
    ".global entry".to_string(),
    "entry:".to_string(),
    "li a0, 0".to_string(),
    "beqz a0, far_target".to_string(),
    ];
    for _ in 0..1500 {
    lines.push("addi t0, t0, 0".to_string());
    }
    lines.push("far_target:".to_string());
    lines.push("ret".to_string());
    let elf = assemble_elf_internal(&lines).expect("internal assembler should relax long conditional branches");
    assert!(elf.starts_with(b"\x7fELF"));
    }
    #[test]
    fn internal_assembler_encodes_register_conditional_branches() {
  • The complete CellScript test suite is included by the CI gate at
    require_cmd npm
    require_node_22
    printf '{"status":"not-generated","reason":"test suite did not reach backend shape report generation"}\n' >"$CELLSCRIPT_BACKEND_SHAPE_REPORT"
    cargo_fmt_workspace --check
    check_canonical_cellscript_format
    check_example_u64_boundaries
    run cargo test --locked -p cellscript -- --test-threads=1
    run cargo test --locked -p cellscript-artifact-checker -- --test-threads=1
    check_artifact_checker_dependency_boundary
    run cargo test --locked -p cellscript-fiber-adapter -- --test-threads=1
    run cargo test --locked -p cellscript-ckb-adapter -- --test-threads=1
    run cargo test --locked -p cellscript-wasm --features wasm -- --test-threads=1
    run cargo test --locked -p cellscript-ckb-sdk-builder-example -- --test-threads=1
    run cargo test --locked -p cellscript-tools -- --test-threads=1
    run_executable_package_scenarios all

Required diagnostic contract

Preserve at least this provenance for each parsed operation:

  • generated assembly line number;
  • text/rodata section;
  • operation index and byte offset after layout;
  • instruction/directive text or a bounded normalized representation;
  • target label and resolved displacement for branch/jump failures.

Generated-assembly provenance must not be mislabeled as a CellScript source span. Where codegen has a real source/lowering-map origin, diagnostics may additionally attach it through an explicitly separate field.

All failures caused by user/compiler-produced assembly must remain ordinary stable diagnostics. No parser, layout, or encoding path should panic.

Acceptance criteria

  • Parsed assembly operations retain their original generated line number.
  • Parse, symbol, layout, branch/jump, immediate, register, directive, and encoding errors report the relevant assembly line.
  • Machine-CFG errors identify the involved label/block/operation without pretending to have a CellScript source span.
  • Human diagnostics no longer print an unexplained line 0 for an operation with known provenance.
  • Machine JSON distinguishes source provenance from generated-assembly provenance.
  • Tests cover duplicate/unknown labels, invalid registers/immediates/directives, out-of-range jumps, malformed memory operands, and CFG target errors.
  • Existing branch-relaxation and ELF identity tests remain unchanged or are intentionally rebaselined with evidence.
  • dev, ci, and backend gates pass.

Non-goals

  • reconstructing a CellScript source span when lowering records do not provide one;
  • exposing unbounded assembly text in diagnostics;
  • changing branch-relaxation semantics;
  • replacing the standalone artifact checker's independent diagnostics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions