kit

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

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:
Msrc/api/asm_emit.c | 70+++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/arch/aa64/asm.c | 22+++++++++++++++++++++-
Msrc/arch/arch.h | 24+++++++++++++++++++++---
Msrc/arch/registry.c | 6++++++
Msrc/arch/x64/arch.c | 2++
Msrc/arch/x64/asm.c | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/arch/x64/isa.c | 23++++++++++++++++++-----
Msrc/asm/asm.c | 4+++-
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);