code wiki / (root) / nx_aarch64.nx

nx_aarch64.nx source

↩ module page · 398 lines · 13039 B

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