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:
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"; }