Spec M17 — machine interface split and the x86-64 machine (Linux x64)
Goal: a second instruction set without touching stage0. Two steps, each gated by make check.
Step A — split gen_lower into a target-independent walker and a machine interface #
Today src/gen_arm64.mc mixes the AST walk (frames, depth stack, labels, calls, name resolution)
with AArch64 instruction selection (I_* opcodes, x9..x15 depth registers, x16/x17 scratch).
After the split:
src/gen_walk.mc: the walker. It knows nothing about registers. It drives a machine table (uptr mtab[], one&fnper operation, registered withmachine(name, mtab)inhooks.mc) throughcallp. Operations (each takes depth indices, not registers):m_prologue(frame, nparams),m_epilogue(),m_const(d, imm),m_bin(op, d, d2)(add sub mul sdiv udiv smod umod and or xor shl shr sar),m_un(op, d)(not neg),m_cmp(cond, d, d2),m_cast(width, d),m_load(width, d, dbase),m_store(width, dval, dbase),m_local_addr(d, off),m_local_load/store(width, d, off),m_global_addr(d, sym),m_str_addr(d, sym),m_call(sym, nargs, ...)(args are depthsdbase..dbase+nargs-1),m_callp(dptr, nargs),m_ret(d),m_label(l),m_jump(l),m_jz(d, l),m_jnz(d, l),m_word(imm)(+ pending reloc),m_func_begin(sym),m_func_end(),m_arg_move(d, i)where needed. The walker owns the depth stack, the label counter, the frame layout in bytes (slots of 8) and spill decisions are the machine's (a machine may keep few depths in registers and spill the rest).src/machine_arm64.mc: today's selection + encoders moved behind the table. Acceptance of step A:scripts/check-obj.sh build/mc0 build/mc1stays 32/32 andcheck-asm61/61 — the C stage0 (frozen, monolithic) is the oracle: the refactored.mcmust emit byte-identical objects.gen_encode_alland the format writers (macho,macho-exe,elf-obj) are unchanged; each machine suppliescputype/e_machineand its relocation kinds through the sameRelocrecords.
Step B — the x86-64 machine (src/machine_x86_64.mc) for Linux (SysV ABI) #
- Depth registers:
r8..r11for depths 0..3; deeper depths live in the frame (spill). Scratch:rax,rcx,rdx(never hold live values across operations). Locals at[rbp - off]; frame =push rbp; mov rbp, rsp; sub rsp, N(N 16-aligned, no 4095 limit -> keep the language limit anyway for parity of diagnostics). - Arguments:
rdi rsi rdx rcx r8 r9; params 7 and 8 on the stack ([rbp+16],[rbp+24]) — the walker asksm_callwith all depths; the machine pushes the extra ones. Return inrax. Beforecall: save live depth registers (r8..r11are caller-saved) to frame slots, like arm64. - Encoders (~30):
REX.Wforms ofmov r,imm64,mov r,r,add/sub/and/or/xor r,r,imul r,r,cqo + idiv r/xor edx,edx + div r,shl/shr/sar r,cl,not/neg r,cmp r,r+setcc r8 + movzx,test r,r,jmp/jcc rel32(fixups),call rel32(R_X86_64_PLT32, addend -4),call r(callp),lea r,[rip+disp32](R_X86_64_PC32, addend -4) for globals/strings,mov [rbp+disp], r/mov r,[rbp+disp](disp8/disp32), loadsmov/movzx byte/word/dword, stores of 1/2/4/8 (mov [r], r8/r16/r32/r64with0x66prefix for 16-bit),ret, rawm_word(4 bytes little-endian;#opcoderemains arch-specific by nature — tests using it are skipped on x86-64 via a// skip-x86_64header). - ELF x86-64:
EM_X86_64(62), relocationsR_X86_64_64(1),PC32(2),PLT32(4). [target] arch = "x86_64"/--target=x86_64-linuxselect machine + format.
Verification #
- Step A: byte-identical arm64 objects (above);
make checkgreen; golden rewritten once. - Step B:
scripts/test-linux.shgains--arch x86_64: sysroot from Alpinelinux/amd64(musl-dev), link withld.lld, run in Dockerlinux/amd64(emulated on Apple Silicon) —N/N tests passed on linux/x86_64with the skipped list printed. Cross-check the encoder againstclang --target=x86_64-linux-musl -coutput of equivalent C withllvm-objdump -d. - Windows later (M20) is the same machine with the Win64 ABI variant (args
rcx rdx r8 r9, 32-byte shadow space, callee-savedrsi rdi).