code wiki / (root) / nx_aarch64.nx

nx_aarch64.nx source

↩ module page · 401 lines · 13316 B

1// DEFINITION-COMPLETE: an instruction/ABI emitter exposes one function per emitted form. Emitters 2// with no caller yet are the backend still being built, NOT dead code. Declared so the reachability 3// census buckets them instead of ratcheting a correct steady state as debt. 4// nx_aarch64.nx -- AArch64 Linux AAPCS asm emitter (foundation; session 1). 5// 6// Pure NishiLang port of nxc2/aarch64.c. Lives outside the C side per 7// cardinal feedback-no-nxc2-c-extension-only-nishilang-forward. This 8// file is the FOUNDATION layer; sessions 2-N fill in the per-opcode 9// IR-to-asm dispatch and wire into nxc.nx as --target aarch64. 10// 11// Session 1 scope (this file): 12// * AArch64 register name table (AAPCS) 13// * function prologue / epilogue (stp x29,x30 / add x29,sp / sub sp) 14// * .text / .rodata section directives 15// * label emission 16// * literal constant load (movz + movk chain for full 64-bit imm) 17// * register-to-register move (mov xD, xS) 18// * PIC-aware address load (adrp + add for label) 19// * add / sub register-to-register (add xD, xS, xN / sub) 20// * syscall (svc #0 -- the ARM64 supervisor call) 21// * .asciz string literal emission with byte-escape 22// 23// Sessions 2-N (deferred): 24// * IR-instruction dispatch (binop / cmp / branch / call / load / 25// store / GEP / phi / return / tail_call / inline_asm) 26// * stack-machine slot layout per nxc2/aarch64.c 27// * regalloc integration (linear-scan with AAPCS-preserved regs) 28// * NEON SIMD (mov vD.16b, vS.16b / add vD.4s, vS.4s, vN.4s) 29// * full nxc.nx wiring as --target aarch64 dispatch 30// 31// Output format: GNU AS unified syntax (no AT&T % prefix), Linux 32// AAPCS ABI, position-independent via adrp/add for label refs. 33// Byte-compatible with `aarch64-linux-gnu-as`. 34// 35// genealogy_id: nxc2_aarch64_c_2026 + aapcs64_v1 36// lineage_id: nx_aarch64_foundation_v1 37// 38// nx_safety_envelope: 39// intended_use: "Foundation emit library for AArch64 Linux 40// AAPCS assembly text. Per-primitive functions 41// writing a shared OutBuf. Session 1 of N for 42// the full IR-driven backend. Required for the 43// hardware-agnostic self-host (M0d / M2 kernel 44// ARM64 port)." 45// sil_target: SIL3 (codegen correctness; silent 46// miscompilation worse than crash) 47// asil_target: QM 48// dal_target: DAL B 49// iec_62304_class: NONE 50// evidence: [no_floating_point_in_logic, 51// unified_syntax_only_no_legacy, 52// aapcs64_documented_per_function, 53// adrp_pic_aware, 54// bounded_loops_in_string_emit] 55// hazard_register: [bug-tape-syscall-num-not-set-x8, 56// bug-tape-stp-misaligned-sp, 57// bug-tape-ret-without-restore-fp-lr, 58// bug-tape-svc-imm-not-zero] 59// residual_risk: "Skeleton subset (foundation); does NOT yet 60// cover every IR opcode -- caller must hand- 61// sequence the primitives. Full coverage is 62// the remaining sessions 2-N work." 63// verdict: NOT_YET_EVALUATED 64 65import "nx_syscalls.nx" 66import "nx_outbuf.nx" 67 68// ===== AAPCS register names ======================================= 69// 70// First 8 integer arguments: x0..x7. Return in x0 (and x1 for 71// composite return). Frame pointer: x29. Link register: x30 (lr). 72// Stack pointer: sp. Callee-saved (must preserve): x19..x28. 73// Caller-saved (may clobber): x0..x18, x30. 74 75const NX_A64_N_ARG_REGS: i64 = 8 76 77func a64_arg_reg_name(idx: i64) -> *u8 { 78 if idx == 0 { return "x0" as *u8 } 79 if idx == 1 { return "x1" as *u8 } 80 if idx == 2 { return "x2" as *u8 } 81 if idx == 3 { return "x3" as *u8 } 82 if idx == 4 { return "x4" as *u8 } 83 if idx == 5 { return "x5" as *u8 } 84 if idx == 6 { return "x6" as *u8 } 85 if idx == 7 { return "x7" as *u8 } 86 return 0 as *u8 87} 88 89// Linux AArch64 syscall convention reuses arg regs. Syscall number 90// goes in x8 (not x0 like other ABIs). Args 0..5 in x0..x5. No 91// special r10 swap like x86_64. 92 93func a64_syscall_arg_reg_name(idx: i64) -> *u8 { 94 if idx == 0 { return "x0" as *u8 } 95 if idx == 1 { return "x1" as *u8 } 96 if idx == 2 { return "x2" as *u8 } 97 if idx == 3 { return "x3" as *u8 } 98 if idx == 4 { return "x4" as *u8 } 99 if idx == 5 { return "x5" as *u8 } 100 return 0 as *u8 101} 102 103// ===== indent + small helpers ===================================== 104 105func a64_indent(o: *OutBuf) -> i64 { 106 out_char(o, 0x20) 107 out_char(o, 0x20) 108 out_char(o, 0x20) 109 out_char(o, 0x20) 110 return 0 111} 112 113// ===== section directives ========================================= 114 115func a64_emit_section_text(o: *OutBuf) -> i64 { 116 out_str(o, " .text\n") 117 return 0 118} 119 120func a64_emit_section_rodata(o: *OutBuf) -> i64 { 121 out_str(o, " .section .rodata\n") 122 return 0 123} 124 125// ===== labels + function symbols ================================== 126 127func a64_emit_label(o: *OutBuf, name: *u8) -> i64 { 128 out_str(o, name) 129 out_char(o, 0x3A) 130 out_char(o, 0x0A) 131 return 0 132} 133 134func a64_emit_function_start(o: *OutBuf, name: *u8) -> i64 { 135 a64_emit_section_text(o) 136 out_str(o, " .globl ") 137 out_str(o, name) 138 out_char(o, 0x0A) 139 out_str(o, " .type ") 140 out_str(o, name) 141 out_str(o, ", @function\n") 142 a64_emit_label(o, name) 143 return 0 144} 145 146func a64_emit_function_end(o: *OutBuf, name: *u8) -> i64 { 147 out_str(o, " .size ") 148 out_str(o, name) 149 out_str(o, ", .-") 150 out_str(o, name) 151 out_char(o, 0x0A) 152 return 0 153} 154 155// ===== string literal emission ==================================== 156// 157// Identical shape to x86_64's emit_asciz -- byte-escape for quotes, 158// backslashes, newlines, tabs, and non-printable. GNU AS accepts 159// the same .asciz syntax on both x86_64 and aarch64. 160 161func a64_emit_asciz(o: *OutBuf, s: *u8, n: i64) -> i64 { 162 out_str(o, " .asciz \"") 163 var i: i64 = 0 164 let BUDGET: i64 = n + 2 165 var iter: i64 = 0 166 while i < n { 167 if iter >= BUDGET { i = n } 168 if i < n { 169 let c: i64 = s[i] 170 if c == 0x22 { 171 out_char(o, 0x5C); out_char(o, 0x22) 172 } 173 if c == 0x5C { 174 out_char(o, 0x5C); out_char(o, 0x5C) 175 } 176 if c == 0x0A { 177 out_char(o, 0x5C); out_char(o, 0x6E) 178 } 179 if c == 0x09 { 180 out_char(o, 0x5C); out_char(o, 0x74) 181 } 182 if c == 0x0D { 183 out_char(o, 0x5C); out_char(o, 0x72) 184 } 185 if c != 0x22 { 186 if c != 0x5C { 187 if c != 0x0A { 188 if c != 0x09 { 189 if c != 0x0D { 190 if c >= 0x20 { 191 if c < 0x7F { 192 out_char(o, c) 193 } 194 } 195 if c < 0x20 { 196 out_char(o, 0x5C) 197 out_char(o, 0x30 + ((c >> 6) & 7)) 198 out_char(o, 0x30 + ((c >> 3) & 7)) 199 out_char(o, 0x30 + (c & 7)) 200 } 201 if c >= 0x7F { 202 out_char(o, 0x5C) 203 out_char(o, 0x30 + ((c >> 6) & 7)) 204 out_char(o, 0x30 + ((c >> 3) & 7)) 205 out_char(o, 0x30 + (c & 7)) 206 } 207 } 208 } 209 } 210 } 211 } 212 i = i + 1 213 } 214 iter = iter + 1 215 } 216 out_str(o, "\"\n") 217 return 0 218} 219 220// ===== function prologue / epilogue =============================== 221// 222// AAPCS requires 16-byte aligned sp at function entry. The 223// canonical prologue: 224// stp x29, x30, [sp, -frame]! ; pre-decrement sp, save fp+lr 225// mov x29, sp ; new frame pointer 226// And the matching epilogue: 227// ldp x29, x30, [sp], frame ; post-increment sp, restore fp+lr 228// ret ; ret = br x30 229// 230// Frame size must be a multiple of 16 to keep sp aligned for nested 231// calls. 232 233func a64_round_up_16(n: i64) -> i64 { 234 return (n + 15) & (0 - 16) 235} 236 237func a64_emit_prologue(o: *OutBuf, frame_size: i64) -> i64 { 238 var f: i64 = a64_round_up_16(frame_size) 239 // Need at least 16 bytes for the fp+lr pair. 240 if f < 16 { f = 16 } 241 out_str(o, " stp x29, x30, [sp, -") 242 out_i64(o, f) 243 out_str(o, "]!\n") 244 out_str(o, " mov x29, sp\n") 245 return 0 246} 247 248func a64_emit_epilogue(o: *OutBuf, frame_size: i64) -> i64 { 249 var f: i64 = a64_round_up_16(frame_size) 250 if f < 16 { f = 16 } 251 out_str(o, " ldp x29, x30, [sp], ") 252 out_i64(o, f) 253 out_char(o, 0x0A) 254 out_str(o, " ret\n") 255 return 0 256} 257 258// ===== constant load ============================================== 259// 260// ARM64 has no 64-bit immediate; build via movz + up to 3 movks. 261// movz xD, #imm, lsl #shift sets the 16-bit chunk and zeros others. 262// movk xD, #imm, lsl #shift sets the 16-bit chunk, preserving others. 263// 264// For brevity, we always emit the full 4-chunk sequence -- the 265// assembler may optimize to fewer when imm fits in 16/32/48 bits, 266// but the emitter is correct on every input. 267 268func a64_emit_mov_imm64(o: *OutBuf, reg: *u8, imm: i64) -> i64 { 269 let c0: i64 = imm & 0xFFFF 270 let c1: i64 = (imm >> 16) & 0xFFFF 271 let c2: i64 = (imm >> 32) & 0xFFFF 272 let c3: i64 = (imm >> 48) & 0xFFFF 273 // movz reg, #c0 274 out_str(o, " movz ") 275 out_str(o, reg) 276 out_str(o, ", #") 277 out_i64(o, c0) 278 out_char(o, 0x0A) 279 if c1 != 0 { 280 out_str(o, " movk ") 281 out_str(o, reg) 282 out_str(o, ", #") 283 out_i64(o, c1) 284 out_str(o, ", lsl #16\n") 285 } 286 if c2 != 0 { 287 out_str(o, " movk ") 288 out_str(o, reg) 289 out_str(o, ", #") 290 out_i64(o, c2) 291 out_str(o, ", lsl #32\n") 292 } 293 if c3 != 0 { 294 out_str(o, " movk ") 295 out_str(o, reg) 296 out_str(o, ", #") 297 out_i64(o, c3) 298 out_str(o, ", lsl #48\n") 299 } 300 return 0 301} 302 303// ===== register-to-register move ================================== 304 305func a64_emit_mov_reg_reg(o: *OutBuf, dst: *u8, src: *u8) -> i64 { 306 out_str(o, " mov ") 307 out_str(o, dst) 308 out_str(o, ", ") 309 out_str(o, src) 310 out_char(o, 0x0A) 311 return 0 312} 313 314// ===== PIC-aware address load (adrp + add) ======================== 315// 316// adrp xD, label = put page (4KB) base of label's address in xD. 317// add xD, xD, :lo12:label = add the low 12 bits. 318// Net effect: xD holds the runtime address of `label`. 319// This is the standard ARM64 PIC idiom; works for .text + .rodata 320// references and is position-independent. 321 322func a64_emit_adrp(o: *OutBuf, reg: *u8, label: *u8) -> i64 { 323 out_str(o, " adrp ") 324 out_str(o, reg) 325 out_str(o, ", ") 326 out_str(o, label) 327 out_char(o, 0x0A) 328 out_str(o, " add ") 329 out_str(o, reg) 330 out_str(o, ", ") 331 out_str(o, reg) 332 out_str(o, ", :lo12:") 333 out_str(o, label) 334 out_char(o, 0x0A) 335 return 0 336} 337 338// ===== add / sub register-to-register ============================= 339// 340// AArch64 unified syntax: dst comes FIRST (unlike AT&T x86). 341// add dst, src1, src2 == dst = src1 + src2. 342// sub dst, src1, src2 == dst = src1 - src2. 343 344func a64_emit_add_rrr(o: *OutBuf, dst: *u8, src1: *u8, src2: *u8) -> i64 { 345 out_str(o, " add ") 346 out_str(o, dst) 347 out_str(o, ", ") 348 out_str(o, src1) 349 out_str(o, ", ") 350 out_str(o, src2) 351 out_char(o, 0x0A) 352 return 0 353} 354 355func a64_emit_sub_rrr(o: *OutBuf, dst: *u8, src1: *u8, src2: *u8) -> i64 { 356 out_str(o, " sub ") 357 out_str(o, dst) 358 out_str(o, ", ") 359 out_str(o, src1) 360 out_str(o, ", ") 361 out_str(o, src2) 362 out_char(o, 0x0A) 363 return 0 364} 365 366// ===== syscall ==================================================== 367// 368// On AArch64 Linux the syscall instruction is `svc #0`. ABI: 369// x8 = syscall number 370// x0..x5 = args 0..5 371// x0 = return value 372// clobbers: x0 (return). x8 is not callee-saved either. 373// 374// Caller must load x8 (syscall number) and x0..x5 (args) before 375// calling this helper -- it just emits the bare `svc #0`. 376 377func a64_emit_svc(o: *OutBuf) -> i64 { 378 out_str(o, " svc #0\n") 379 return 0 380} 381 382// Convenience: load `num` into x8, then svc. Caller still wires the 383// arg registers separately (a64_arg_reg_name idx for n_args params). 384// Mirrors x86_emit_syscall_imm's shape. 385 386func a64_emit_svc_imm(o: *OutBuf, num: i64) -> i64 { 387 a64_emit_mov_imm64(o, "x8" as *u8, num) 388 a64_emit_svc(o) 389 return 0 390} 391 392// ===== misc: GNU stack note + safety ============================== 393// 394// ARM64 GNU AS emits an unwind-info section by default; we mirror 395// the x86_64 backend's note for non-executable stack which is 396// hygiene on Linux. 397 398func a64_emit_gnu_stack_note(o: *OutBuf) -> i64 { 399 out_str(o, " .section .note.GNU-stack,\"\",@progbits\n") 400 return 0 401}