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}