commit 528839e8ae1a94431e83938c207a187752152095
parent feaa52041dcd6ed0dfcb6bf1853df0da25d76d23
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 30 May 2026 17:37:01 -0700
x64: cc -S symbolizer + disasm/as fidelity fixes (cross-assemble green)
Bring x86-64 `cc -S` output to where it re-assembles through both cfree-as and
clang for the whole toy corpus (312/312 assemble+link, up from ~0).
Generalize the cc -S symbolizer (src/api/asm_emit.c) so the arch supplies the
operand syntax, then add the x64 implementation:
- ArchAsmOps gains is_local_branch (arch-specific intra-section branch mnemonics)
and ArchRelocOperand gains addend_bias (undo an instruction-encoding addend
bias) + ARCH_RELOC_SURG_RIP. The symbolizer now picks the surgery site from
the operand text — an `(%rip)` operand always takes RIP surgery — so one reloc
kind (x64 R_PC32) serves both a branch target and a RIP-relative lea/mov.
- aa64: is_local_branch_mnem moves to aa64_is_local_branch (behavior unchanged);
addend_bias 0. x64: x64_reloc_operand maps PC32/PLT32 (bare sym) and
GOTPCREL{,X}/REX_GOTPCRELX (sym@GOTPCREL) with addend_bias +4 (rel32 stores
addend-4); x64_is_local_branch covers jmp + the Jcc family.
Two x64 disasm/as fidelity bugs surfaced by executing the result:
- MOVZX/MOVSX printed an `l` (32-bit) mnemonic with a 64-bit destination under
REX.W (clang rejects `movsbl ..., %r10`). Split the rows by REX.W so the
disassembler emits the `q` form (movsbq/movswq/movzbq/movzwq) with a 64-bit
dest; teach the assembler's parse_mnemonic the q-forms.
- The assembler couldn't parse `movq %rax, %xmm6` (GPR<->XMM): add the
66 [REX.W] 0F 6E/7E form to parse_mov via emit_sse_rr_w.
Also fix a latent UB in the shared asm expression parser: unary minus negated
INT64_MIN (`$-9223372036854775808`) as i64 — now negate as unsigned.
aarch64 unaffected (hostas-toy 312/0, roundtrip 572/0, roundtrip-toy 624/0,
diff-llvm, test-asm/-x64, test-isa, test-toy 1338/0 all green). x64 cross-EXEC
is 272/312; the remaining 23 cases are a separate, broader x64 cc -S data
round-trip backlog (jump tables, global/fp/array data, varargs) — confirmed
cc -S infidelity (direct `cc -c` executes correctly), tracked next.
Diffstat:
8 files changed, 190 insertions(+), 34 deletions(-)
diff --git a/src/api/asm_emit.c b/src/api/asm_emit.c
@@ -563,6 +563,10 @@ static int build_symref(char* buf, u32 cap, Compiler* c,
if (!name) return -1;
s = pool_slice(c->global, name);
if (!sym_is_assemblable(s)) return -1;
+ /* Undo any instruction-encoding addend bias so the printed offset is the
+ * symbol offset (x86-64 rel32 relocs store addend-4; the assembler re-applies
+ * the -4, so emit `sym` for a stored -4). */
+ addend += ro->addend_bias;
for (i = 0; ro->prefix[i] && p + 1 < cap; ++i) buf[p++] = ro->prefix[i];
for (i = 0; i < s.len && p + 1 < cap; ++i) buf[p++] = s.s[i];
if (addend != 0) {
@@ -581,11 +585,44 @@ static int build_symref(char* buf, u32 cap, Compiler* c,
return (int)p;
}
-/* Write `ops` with the relocated operand rewritten to `symref`. `surg`
- * selects the shape: TAIL replaces the last comma-separated component (or the
- * whole operand if there is no comma); MEM rewrites the offset inside [...]. */
+/* Position of the "(%rip)" substring in [ops, ops+olen), or -1. */
+static i32 find_rip(const char* ops, u32 olen) {
+ u32 i;
+ if (olen < 6) return -1;
+ for (i = 0; i + 6 <= olen; ++i)
+ if (memcmp(ops + i, "(%rip)", 6) == 0) return (i32)i;
+ return -1;
+}
+
+/* Write `ops` with the relocated operand rewritten to `symref`. The surgery
+ * site is chosen from the operand text first: an x86-64 `disp(%rip)` operand
+ * always takes RIP surgery (insert sym before the displacement), regardless of
+ * `surg`. Otherwise `surg` selects: TAIL replaces the last comma-separated
+ * component (or the whole operand if there is no comma — branch targets); MEM
+ * rewrites the offset inside [...] (aarch64 ldst). */
static CfreeStatus w_symbolized(Writer* w, const char* ops, u32 olen,
const char* symref, ArchRelocSurg surg) {
+ i32 rip = find_rip(ops, olen);
+ if (rip >= 0) surg = ARCH_RELOC_SURG_RIP;
+ if (surg == ARCH_RELOC_SURG_RIP) {
+ /* `[disp](%rip)[, ...]` -> `symref(%rip)[, ...]`: replace any numeric
+ * displacement immediately before `(%rip)` with symref. */
+ i32 ds = rip; /* start of the displacement run before the '(' */
+ CfreeStatus st;
+ while (ds > 0) {
+ char ch = ops[ds - 1];
+ if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') ||
+ (ch >= 'A' && ch <= 'F') || ch == 'x' || ch == '-' || ch == '+')
+ --ds;
+ else
+ break;
+ }
+ st = cfree_writer_write(w, ops, (u32)ds); /* text before the displacement */
+ if (st != CFREE_OK) return st;
+ st = w_str(w, symref);
+ if (st != CFREE_OK) return st;
+ return cfree_writer_write(w, ops + rip, olen - (u32)rip); /* "(%rip)..." */
+ }
if (surg == ARCH_RELOC_SURG_TAIL) {
i32 last_comma = -1;
u32 i;
@@ -647,23 +684,10 @@ static int cmp_u32(const void* va, const void* vb) {
return 0;
}
-/* 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;
-}
+/* Which mnemonics are intra-section local branches (target codegen resolved in
+ * place, no relocation) is arch-specific: routed through arch_is_local_branch
+ * (ArchAsmOps.is_local_branch). The disassembler renders such a target
+ * numerically; we synthesize a label at it so the operand re-assembles. */
/* Parse the trailing `0x<hex>` branch-target operand (the last comma-separated
* component). Returns 1 and the value on success. */
@@ -781,7 +805,7 @@ static u32* collect_branch_targets(Compiler* c, ArchDisasm* dasm,
continue;
}
if (!reloc_in_range(relocs, nrelocs, off, nb) &&
- is_local_branch_mnem(insn.mnemonic) &&
+ arch_is_local_branch(c, insn.mnemonic) &&
parse_hex_tail(insn.operands, &tgt) && tgt < total) {
u32 j;
int found = 0;
@@ -824,7 +848,7 @@ static CfreeStatus emit_operands(Writer* w, const EmitCtx* x,
return w_symbolized(w, insn->operands.s, insn->operands.len, symref,
ro.surg);
}
- } else if (is_local_branch_mnem(insn->mnemonic)) {
+ } else if (arch_is_local_branch(x->c, insn->mnemonic)) {
u64 tgt;
if (parse_hex_tail(insn->operands, &tgt) && is_btarget(x, (u32)tgt)) {
char name[256];
@@ -866,7 +890,7 @@ static CfreeStatus emit_data_range(Writer* w, Compiler* c, const u8* data,
char symref[256];
/* Data relocations spell the bare symbol (`.quad sym+addend`): no
* page/lo12-style operand modifier on either format. */
- ArchRelocOperand bare = {ARCH_RELOC_SURG_NONE, "", ""};
+ ArchRelocOperand bare = {ARCH_RELOC_SURG_NONE, "", "", 0};
if (data_reloc_directive(r->kind, &dir, &width) && off + width <= end &&
build_symref(symref, sizeof symref, c, &bare, r->sym, r->addend) >=
0) {
diff --git a/src/arch/aa64/asm.c b/src/arch/aa64/asm.c
@@ -469,6 +469,7 @@ static int aa64_reloc_operand(u16 kind, CfreeObjFmt fmt, ArchRelocOperand* out)
return 0; /* TLV and anything else: keep the numeric operand */
}
out->surg = surg;
+ out->addend_bias = 0; /* aarch64 relocs store the symbol offset directly */
if (fmt == CFREE_OBJ_MACHO) {
out->prefix = "";
out->suffix = macho;
@@ -479,7 +480,26 @@ static int aa64_reloc_operand(u16 kind, CfreeObjFmt fmt, ArchRelocOperand* out)
return 1;
}
-const ArchAsmOps aa64_asm_ops = {.reloc_operand = aa64_reloc_operand};
+/* Intra-section local branches whose target codegen resolved in place (no
+ * relocation): b, b.<cc>, cbz/cbnz, tbz/tbnz, and adr (address-of-label, e.g.
+ * `&&label`). Excludes bl (a call — always relocated), adrp (page-relative; its
+ * lo12 partner carries the reloc), and register-form branches. Moved here from
+ * the printer so branch-mnemonic knowledge is arch-local. */
+static int aa64_is_local_branch(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;
+}
+
+const ArchAsmOps aa64_asm_ops = {
+ .reloc_operand = aa64_reloc_operand,
+ .is_local_branch = aa64_is_local_branch,
+};
static void emit32(AsmDriver* d, u32 word) {
MCEmitter* mc = asm_driver_mc(d);
diff --git a/src/arch/arch.h b/src/arch/arch.h
@@ -186,21 +186,34 @@ typedef struct ArchDbgOps {
typedef enum ArchRelocSurg {
ARCH_RELOC_SURG_NONE = 0, /* not symbolizable here; keep numeric operand */
ARCH_RELOC_SURG_TAIL, /* replace last comma component (or whole operand) */
- ARCH_RELOC_SURG_MEM, /* rewrite the offset inside [...] */
+ ARCH_RELOC_SURG_MEM, /* rewrite the offset inside [...] (aarch64 ldst) */
+ ARCH_RELOC_SURG_RIP, /* insert sym before disp(%rip) (x86-64 RIP-rel) */
} ArchRelocSurg;
typedef struct ArchRelocOperand {
ArchRelocSurg surg;
const char* prefix; /* e.g. ":lo12:" (ELF); "" if none */
- const char* suffix; /* e.g. "@PAGEOFF" (Mach-O); "" if none */
+ const char* suffix; /* e.g. "@PAGEOFF" / "@GOTPCREL"; "" if none */
+ /* Added to the relocation's stored addend before spelling `sym[+/-N]`. Undoes
+ * an instruction-encoding bias so the printed offset is the *symbol* offset:
+ * 0 for aarch64; +4 for x86-64 rel32 (PC32/PLT32/GOTPCREL store addend-4). */
+ int addend_bias;
} ArchRelocOperand;
typedef struct ArchAsmOps {
/* Map (reloc kind, target object format) to the operand syntax the cc -S
* symbolizer must emit (and that this arch's .s parser accepts back).
* Returns 1 and fills *out when the kind is symbolizable for fmt; 0
- * otherwise (printer keeps the numeric operand). */
+ * otherwise (printer keeps the numeric operand). The symbolizer picks the
+ * surgery site from the operand text (an `(%rip)` operand always uses RIP
+ * surgery regardless of `out->surg`), so a single reloc kind can serve both
+ * a branch target and a RIP-relative memory operand (x86-64 R_PC32). */
int (*reloc_operand)(u16 reloc_kind, CfreeObjFmt fmt, ArchRelocOperand* out);
+ /* 1 if `mnemonic` is an intra-section local branch whose un-relocated
+ * numeric target the symbolizer should replace with a synthesized label
+ * (aarch64 b/b.cc/cbz/...; x86-64 jmp/jcc). Calls are excluded — they carry
+ * relocations. NULL hook = no local-branch symbolization for the arch. */
+ int (*is_local_branch)(CfreeSlice mnemonic);
} ArchAsmOps;
typedef struct ArchImpl {
@@ -255,6 +268,11 @@ const ArchImpl* arch_for_compiler(const Compiler*);
int arch_reloc_operand(const Compiler* c, u16 reloc_kind,
ArchRelocOperand* out);
+/* 1 if `mnemonic` is an intra-section local branch for the compiler's target
+ * arch (so cc -S synthesizes a label at its un-relocated target). 0 when the
+ * arch has no asm_ops/is_local_branch hook. */
+int arch_is_local_branch(const Compiler* c, CfreeSlice mnemonic);
+
ArchDisasm* arch_disasm_new(Compiler*);
u32 arch_disasm_decode(ArchDisasm*, const u8* bytes, size_t len, u64 vaddr,
CfreeInsn* out);
diff --git a/src/arch/registry.c b/src/arch/registry.c
@@ -95,6 +95,12 @@ int arch_reloc_operand(const Compiler* c, u16 reloc_kind,
return a->asm_ops->reloc_operand(reloc_kind, c->target.obj, out);
}
+int arch_is_local_branch(const Compiler* c, CfreeSlice mnemonic) {
+ const ArchImpl* a = arch_for_compiler(c);
+ if (!a || !a->asm_ops || !a->asm_ops->is_local_branch) return 0;
+ return a->asm_ops->is_local_branch(mnemonic);
+}
+
const CGBackend* cg_backend_for_session(const Compiler* c,
const CfreeCodeOptions* opts) {
if (opts && opts->check_only) {
diff --git a/src/arch/x64/arch.c b/src/arch/x64/arch.c
@@ -14,6 +14,7 @@
extern const LinkArchDesc link_arch_x64;
extern const ArchDbgOps x64_dbg_ops;
extern const ArchDwarfOps x64_dwarf_ops;
+extern const ArchAsmOps x64_asm_ops;
static int x64_apply_label_fixup(Compiler* c, const ArchLabelFixup* fx) {
(void)c;
@@ -90,6 +91,7 @@ const ArchImpl arch_impl_x64 = {
.link = &link_arch_x64,
.dwarf = &x64_dwarf_ops,
.dbg = &x64_dbg_ops,
+ .asm_ops = &x64_asm_ops,
.predefined_macros = x64_predefined_macros,
.npredefined_macros =
(u32)(sizeof x64_predefined_macros / sizeof x64_predefined_macros[0]),
diff --git a/src/arch/x64/asm.c b/src/arch/x64/asm.c
@@ -641,7 +641,9 @@ static int parse_mnemonic(const char* s, size_t n, X64MnInfo* out) {
return 1;
}
if (n >= 6 && (memcmp(s, "movzbl", 6) == 0 || memcmp(s, "movzwl", 6) == 0 ||
- memcmp(s, "movsbl", 6) == 0 || memcmp(s, "movswl", 6) == 0)) {
+ memcmp(s, "movsbl", 6) == 0 || memcmp(s, "movswl", 6) == 0 ||
+ memcmp(s, "movzbq", 6) == 0 || memcmp(s, "movzwq", 6) == 0 ||
+ memcmp(s, "movsbq", 6) == 0 || memcmp(s, "movswq", 6) == 0)) {
memcpy(out->base, s, 6);
out->base_len = 6;
return 1;
@@ -1503,6 +1505,18 @@ static void x64_arch_asm_insn(ArchAsm* base, AsmDriver* d, Sym mnemonic) {
emit_mov_load_operand(d, mc, w, dst.reg, src);
return;
}
+ /* movd/movq between a GPR and an XMM register: 66 [REX.W] 0F 6E (to
+ * xmm) / 7E (to gpr). The xmm is always the ModRM.reg field, the gpr
+ * the r/m; movq sets REX.W (w==8), movd does not (w==4). */
+ if ((src.kind == X64_ASM_OP_REG && dst.kind == X64_ASM_OP_XMM) ||
+ (src.kind == X64_ASM_OP_XMM && dst.kind == X64_ASM_OP_REG)) {
+ int to_xmm = (dst.kind == X64_ASM_OP_XMM);
+ u32 xmm = to_xmm ? dst.reg : src.reg;
+ u32 gpr = to_xmm ? src.reg : dst.reg;
+ emit_sse_rr_w(mc, X64_OPSIZE_PFX, to_xmm ? 0x6Eu : 0x7Eu,
+ width_to_w(w), xmm, gpr);
+ return;
+ }
asm_driver_panic(d, "x64 asm: mov form");
}
}
@@ -1550,6 +1564,63 @@ X64Asm* x64_asm_open(Compiler* c) {
void x64_asm_close(X64Asm* a) { (void)a; }
+/* ---- cc -S symbolization hooks (printer <-> this parser) ------------------
+ *
+ * Inverse of the operand-syntax this parser accepts (parse_rel32_branch,
+ * x64_parse_reloc_suffix): how the printer spells a relocated x64 operand so it
+ * re-assembles. x64 relocs store addend-4 (rel32 bias), so addend_bias=4 makes
+ * the printed offset the symbol offset. R_PC32 covers BOTH a branch target and
+ * a RIP-relative lea/mov, so surgery is chosen from the operand text by the
+ * printer (an `(%rip)` operand uses RIP surgery); we just supply the modifier.
+ * Calls (R_X64_PLT32) print as a bare symbol — both cfree-as (call default) and
+ * clang resolve a same-TU callee, so execution matches regardless of the exact
+ * reloc kind each assembler picks. */
+static int x64_reloc_operand(u16 kind, CfreeObjFmt fmt, ArchRelocOperand* out) {
+ const char* suffix;
+ (void)fmt; /* x64 cc -S cross-targets ELF; one spelling */
+ switch (kind) {
+ case R_PC32: /* jmp/jcc target, or RIP-relative lea/mov */
+ case R_X64_PLT32: /* call target -> bare symbol */
+ suffix = "";
+ break;
+ case R_X64_GOTPCREL:
+ case R_X64_GOTPCRELX:
+ case R_X64_REX_GOTPCRELX:
+ suffix = "@GOTPCREL"; /* RIP-relative GOT load */
+ break;
+ default:
+ return 0; /* data (R_ABS*) via emit_data_range; TLS/etc. unsymbolized */
+ }
+ out->surg = ARCH_RELOC_SURG_TAIL; /* promoted to RIP by the printer if (%rip) */
+ out->prefix = "";
+ out->suffix = suffix;
+ out->addend_bias = 4;
+ return 1;
+}
+
+/* Intra-section local branches whose target codegen resolved in place (no
+ * relocation): jmp and the Jcc family. Excludes call (always relocated) and
+ * indirect/register-form jumps (no numeric target to relabel). */
+static int x64_is_local_branch(CfreeSlice m) {
+ static const char* const br[] = {
+ "jmp", "jo", "jno", "jb", "jae", "je", "jne", "jbe", "ja", "js", "jns",
+ "jp", "jnp", "jl", "jge", "jle", "jg",
+ /* aliases the disassembler may not emit but harmless to accept */
+ "jz", "jnz", "jc", "jnc",
+ };
+ u32 i;
+ for (i = 0; i < sizeof br / sizeof br[0]; ++i) {
+ size_t n = strlen(br[i]);
+ if (m.len == (u32)n && memcmp(m.s, br[i], n) == 0) return 1;
+ }
+ return 0;
+}
+
+const ArchAsmOps x64_asm_ops = {
+ .reloc_operand = x64_reloc_operand,
+ .is_local_branch = x64_is_local_branch,
+};
+
ArchAsm* x64_arch_asm_new(Compiler* c) { return &x64_asm_open(c)->base; }
void x64_inline_bind(X64Asm* a, const AsmConstraint* outs, u32 nout,
diff --git a/src/arch/x64/isa.c b/src/arch/x64/isa.c
@@ -110,14 +110,27 @@ const X64InsnDesc x64_insn_table[] = {
ROW("lea", X64_PFX_NONE, 1, 0x8D, 0, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
X64_FMT_MOV_RM_LOAD, X64_ASMFL_W_FROM_REX),
- /* ---- MOVZX / MOVSX r32, r/m{8,16} ---- */
- ROW("movzbl", X64_PFX_NONE, 2, 0x0F, 0xB6, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
+ /* ---- MOVZX / MOVSX r{32,64}, r/m{8,16} ----
+ * The destination width is the *l* (32-bit) form without REX.W and the *q*
+ * (64-bit) form with it; split by W so the disassembler emits a mnemonic
+ * whose size letter matches the printed register width (clang rejects a
+ * `movsbl` with a 64-bit destination). Same opcodes; W disambiguates,
+ * exactly like cltd/cqto (0x99). */
+ ROW("movzbl", X64_PFX_NONE, 2, 0x0F, 0xB6, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
X64_FMT_MOVZX_MOVSX, 0),
- ROW("movzwl", X64_PFX_NONE, 2, 0x0F, 0xB7, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
+ ROW("movzbq", X64_PFX_NONE, 2, 0x0F, 0xB6, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
X64_FMT_MOVZX_MOVSX, 0),
- ROW("movsbl", X64_PFX_NONE, 2, 0x0F, 0xBE, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
+ ROW("movzwl", X64_PFX_NONE, 2, 0x0F, 0xB7, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
X64_FMT_MOVZX_MOVSX, 0),
- ROW("movswl", X64_PFX_NONE, 2, 0x0F, 0xBF, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY,
+ ROW("movzwq", X64_PFX_NONE, 2, 0x0F, 0xB7, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
+ X64_FMT_MOVZX_MOVSX, 0),
+ ROW("movsbl", X64_PFX_NONE, 2, 0x0F, 0xBE, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
+ X64_FMT_MOVZX_MOVSX, 0),
+ ROW("movsbq", X64_PFX_NONE, 2, 0x0F, 0xBE, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
+ X64_FMT_MOVZX_MOVSX, 0),
+ ROW("movswl", X64_PFX_NONE, 2, 0x0F, 0xBF, 0, 0xFF, NO_MODRM, X64_W_REQ_0,
+ X64_FMT_MOVZX_MOVSX, 0),
+ ROW("movswq", X64_PFX_NONE, 2, 0x0F, 0xBF, 0, 0xFF, NO_MODRM, X64_W_REQ_1,
X64_FMT_MOVZX_MOVSX, 0),
/* ---- MOVSXD r64, r/m32 ---- */
diff --git a/src/asm/asm.c b/src/asm/asm.c
@@ -299,7 +299,9 @@ static AsmExpr parse_unary(AsmDriver* d) {
(void)d_next(d);
AsmExpr e = parse_unary(d);
if (e.sym) d_panicf(d, "asm: unary '-' on symbol");
- return expr_c(-e.value);
+ /* Unsigned negate so `$-9223372036854775808` (negating INT64_MIN) is
+ * well-defined 2's-complement, not signed-overflow UB. */
+ return expr_c((i64)(0u - (u64)e.value));
}
if (tok_is_punct(t, '+')) {
(void)d_next(d);