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}