kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 72389f31ee6a23b5fd957e5614ee77e17313efb9
parent 6aa07f2e4635fb0b1af0c0afc2ab2360dbda43c5
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 30 May 2026 07:05:18 -0700

asm: close 9/10 toy round-trip skips (&&label, .comm, macho addend)

The Toy round-trip lane's 10 skips were real cc -S / assembler gaps the hand
corpus never reached. Fix three, closing nine cases (310/0/1, was 292/0/10):

- &&label / computed goto: codegen resolves an `adr` to a local code label in
  place (no reloc), and the disassembly rendered it numerically (`adr x,0x1c`),
  which `as` rejected ("symbol required"). The branch-label synthesizer now
  also labels `adr` (and tbz/tbnz) targets, and P2 relaxes ADR_PREL_LO21.

- tentative/common globals: cc -S now emits .comm/.lcomm for SK_COMMON symbols.
  They live in no output section (the linker allocates .bss), so the section
  walk never saw them — a reference re-assembled to an undefined symbol.

- Mach-O implicit addend: the assembler's `.quad sym+N` wrote a zero data field
  and relied on the explicit reloc addend, which Mach-O (REL form — addend is
  implicit in the relocated field) drops. So every switch jump-table entry
  resolved to sym+0 and `br` dispatched into hyperspace (the 6 macho jump-table
  crashes). Write the addend into the data like codegen; harmless on ELF, where
  RELA overwrites the field with S+A (jumptable.c L1/L2 still green).

Remaining skip: 118_decl_extra_attrs — a global with an explicit section() +
merge attrs lands in SEC_OTHER, which cc -S doesn't emit (section-attribute
round-tripping, out of scope). Regressions green: aa64 roundtrip 858/0,
asm/link/elf/macho, smoke x64/rv64, isa, symmetry (baseline unchanged),
diff-llvm.

Diffstat:
Mdoc/ASM_ROUNDTRIP_TESTING.md | 36++++++++++++++++++++++--------------
Msrc/api/asm_emit.c | 50++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/asm/asm.c | 10++++++++--
Mtest/asm/roundtrip_toy.sh | 18++++++------------
4 files changed, 84 insertions(+), 30 deletions(-)

diff --git a/doc/ASM_ROUNDTRIP_TESTING.md b/doc/ASM_ROUNDTRIP_TESTING.md @@ -135,20 +135,28 @@ native: `cfree run case.toy` (direct) vs `cfree cc -S | cfree as | cfree run` (round-trip); the exit codes must match. `cfree run` propagates `main()`'s return on the native arch (aarch64 macOS), so the oracle is the exit code. -292 pass / 0 fail / 10 skip. It immediately found a real miscompile — see the -`.inst` fix above. The 10 skips are two backlog groups: -- **`cc -S` symbolizer gaps** — `as` hits an undefined reference / "symbol - required" because the disassembly omits a symbol kind: tentative/common - (`.comm`) definitions, merged-string symbols, and computed-`goto` - label-address tables (`62_decl_data_attrs`, `118_decl_extra_attrs`, - `51_labeladdr_goto`, `123_spec_demo`). -- **Mach-O object-file jump table** — an `as`-produced macho `.o` with a switch - jump table crashes when run, while codegen's macho `.o` and the assemble- - inline path (`cfree run x.s`) both work — a macho obj write/read axis, not a - `-S`|`as` round-trip issue (the ELF round-trip of jump tables is green). -- Also tracked: `smulh`/`umulh` (and `smaddl`/`umaddl`/`smsubl`/`umsubl`) DP3 - long/high-multiply *decode* (the `.inst` the lane found; correctness is - already restored by the `.inst` fix, but `-S` still shows `.inst` for them). +310 pass / 0 fail / 1 skip. It found — and drove fixes for — a real miscompile +and four classes of gap the hand corpus never reached: +- the dropped-`.inst` miscompile (see above); +- **computed-`goto` `&&label` materialization** — codegen resolves an `adr` to a + local code label in place (no reloc); the disassembly rendered it numerically + (`adr x, 0x1c`), which `as` rejected. Now the branch-label synthesizer also + labels `adr` (and `tbz`/`tbnz`) targets, and P2 relaxes `ADR_PREL_LO21`; +- **tentative/common globals** — `cc -S` now emits `.comm`/`.lcomm` for + `SK_COMMON` symbols (they live in no section, so the section walk missed them); +- **Mach-O implicit addend** — the assembler's `.quad sym+N` wrote a zero data + field and relied on the explicit reloc addend, which Mach-O (REL form, addend + implicit in the data) drops — so every switch jump-table entry resolved to + `sym+0` and `br` dispatched into hyperspace. It now writes the addend into the + data like codegen (harmless on ELF, where RELA overwrites it with S+A). + +The lone remaining skip (`118_decl_extra_attrs`): a global with an explicit +`__attribute__((section(...)))` + merge attributes lands in a `SEC_OTHER` +section that `cc -S` doesn't emit. Round-tripping arbitrary named sections with +their flags (without also emitting `.eh_frame`/debug) is a section-attribute +feature beyond this lane's scope. Also still open: `smulh`/`umulh` (and the +`*L` long multiplies) DP3 *decode* — correctness is restored by the `.inst` +fix, but `-S` still shows `.inst` for them. ### llvm differential (`test-diff-llvm`) diff --git a/src/api/asm_emit.c b/src/api/asm_emit.c @@ -129,6 +129,42 @@ static CfreeStatus emit_label(Writer* w, Compiler* c, const SymLabel* lbl) { return w_newline(w); } +/* Emit `.comm`/`.lcomm` for tentative (common) symbols. These live in no output + * section — the linker allocates .bss space at link time — so the section walk + * never sees them; without this a `cc -S` that references a tentative global + * (`int x;` at file scope) re-assembles to an undefined reference. Global + * commons use `.comm name, size, align`; local ones `.lcomm`. */ +static CfreeStatus emit_common_symbols(Writer* w, Compiler* c, ObjBuilder* ob) { + ObjSymIter* it = obj_symiter_new(ob); + CfreeStatus st = CFREE_OK; + if (!it) return CFREE_NOMEM; + for (;;) { + ObjSymEntry e; + const ObjSym* sym; + if (!obj_symiter_next(it, &e)) break; + sym = e.sym; + if (!sym || sym->removed || sym->kind != SK_COMMON || !sym->name) continue; + st = w_str(w, sym->bind == SB_LOCAL ? " .lcomm " : " .comm "); + if (st != CFREE_OK) break; + st = w_sym(w, c, sym->name); + if (st != CFREE_OK) break; + st = w_str(w, ", "); + if (st != CFREE_OK) break; + st = w_dec(w, sym->size); + if (st != CFREE_OK) break; + if (sym->common_align > 1) { + st = w_str(w, ", "); + if (st != CFREE_OK) break; + st = w_dec(w, sym->common_align); + if (st != CFREE_OK) break; + } + st = w_newline(w); + if (st != CFREE_OK) break; + } + obj_symiter_free(it); + return st; +} + static CfreeStatus emit_size_directives(Writer* w, Compiler* c, ObjBuilder* ob, ObjSecId sec_id) { ObjSymIter* it = obj_symiter_new(ob); @@ -442,13 +478,21 @@ static int cmp_u32(const void* va, const void* vb) { return 0; } -/* PC-relative branch with an immediate label target: b, b.<cc>, cbz, cbnz. - * Excludes bl (a call — always relocated) and register-form branches. */ +/* PC-relative instruction whose immediate operand is a local code offset that + * codegen resolved in place (no relocation): b, b.<cc>, cbz, cbnz, tbz, tbnz + * (branches) and adr (address-of-label materialization, e.g. `&&label` for + * computed goto). The disassembler renders the target numerically; we + * synthesize a label at it so the operand re-assembles. Excludes bl (a call, + * always relocated), adrp (page-relative — its lo12 partner carries the reloc), + * and register-form branches. */ static int is_local_branch_mnem(CfreeSlice m) { if (m.len == 1 && m.s[0] == 'b') return 1; if (m.len >= 2 && m.s[0] == 'b' && m.s[1] == '.') return 1; if (m.len == 3 && memcmp(m.s, "cbz", 3) == 0) return 1; if (m.len == 4 && memcmp(m.s, "cbnz", 4) == 0) return 1; + if (m.len == 3 && memcmp(m.s, "tbz", 3) == 0) return 1; + if (m.len == 4 && memcmp(m.s, "tbnz", 4) == 0) return 1; + if (m.len == 3 && memcmp(m.s, "adr", 3) == 0) return 1; return 0; } @@ -852,5 +896,7 @@ CfreeStatus cfree_obj_builder_emit_asm(CfreeObjBuilder* builder, w_newline(w); } + emit_common_symbols(w, c, ob); + return cfree_writer_status(out_w); } diff --git a/src/asm/asm.c b/src/asm/asm.c @@ -624,8 +624,13 @@ static void emit_int_directive(AsmDriver* d, u32 width) { d_panicf(d, "asm: symbolic .byte/.hword not supported"); (void)asm_driver_cur_section(d); u32 ofs = d->mc->pos(d->mc); - u8 zero[8] = {0}; - d->mc->emit_bytes(d->mc, zero, width); + /* Write the addend into the data field, not zero. Mach-O relocations + * carry the addend implicitly in the relocated field (REL form); writing + * zero loses it (every `.quad sym+N` would resolve to sym+0 — a switch + * jump table dispatching into hyperspace). codegen pre-writes the addend + * the same way. On ELF (RELA) the linker overwrites the field with S+A, + * so the pre-written value is harmless there. */ + emit_le(d, (u64)e.value, width); d->mc->emit_reloc_at(d->mc, d->cur_sec, ofs, k, e.sym, e.value, 1, 0); } else { emit_le(d, (u64)e.value, width); @@ -1026,6 +1031,7 @@ static int is_relaxable_branch_kind(u16 kind) { case R_AARCH64_JUMP26: case R_AARCH64_CONDBR19: case R_AARCH64_TSTBR14: + case R_AARCH64_ADR_PREL_LO21: /* adr to a local code label (&&label) */ return 1; default: return 0; diff --git a/test/asm/roundtrip_toy.sh b/test/asm/roundtrip_toy.sh @@ -28,18 +28,12 @@ WORK="$ROOT/build/test/asm/roundtrip_toy" OPTS="${CFREE_TEST_OPTS:-O0 O1}" FILTER="${1:-}" -# Known gaps the toy corpus surfaced (see doc/ASM_ROUNDTRIP_TESTING.md): -# (a) cc -S symbolizer gaps — `as` fails with an undefined reference / -# "symbol required" because the disassembly omits a symbol kind it can't -# resolve: tentative/common (.comm) defs, merged strings, and computed-goto -# label-address tables. -# (b) Mach-O object-file jump-table relocation — an `as`-produced .o with a -# switch jump table crashes when run, while codegen's macho .o and the -# assemble-inline path (`cfree run x.s`) both work. A macho obj write/read -# axis, not a -S|as round-trip issue. -SKIP="62_decl_data_attrs 118_decl_extra_attrs 51_labeladdr_goto 123_spec_demo \ - 118_many_enum_switch_values 119_static_labeladdr_data 119_switch_strategy_hints \ - 125_switch_dense_boundaries 127_switch_forced_jump_table 47_target_arch_switch" +# Remaining known gap: a global with an explicit __attribute__((section(...))) +# plus merge/strings attributes becomes a SEC_OTHER section, which cc -S does +# not emit (so `as` sees an undefined reference). Round-tripping arbitrary named +# sections with their flags is a section-attribute feature beyond this lane's +# scope. See doc/ASM_ROUNDTRIP_TESTING.md. +SKIP="118_decl_extra_attrs" color_red() { printf '\033[31m%s\033[0m' "$1"; } color_grn() { printf '\033[32m%s\033[0m' "$1"; }