nxasm_v2.nx source
↩ module page · 1356 lines · 53128 B
1// nxasm_v2.nx -- the Nishi RV64 assembler, with labels + directives.
2//
3// Complete enough to assemble the .s files nxc2 emits for small
4// programs. Two-pass:
5// Pass 1: walk tokens, record labels and their addresses.
6// Pass 2: walk tokens, emit bytes to output buffer.
7//
8// Supports:
9// * Mnemonics: li, mv, ret, ecall, ld, sd, add, sub, addi, jalr
10// plus branches / jumps with label operands: j, jal, call, tail,
11// bnez, beqz.
12// * Directives: .text, .globl, .type, .size, .attribute, .option,
13// .section, .asciz, .byte. First four are ignored (metadata).
14// * Labels: `name:` at line start; referenced by jumps.
15//
16// Output is a raw byte buffer of code + rodata. An ELF wrapper
17// layer (see elf_writer.nx) adds headers to make it runnable.
18
19// Stderr logging routes through sys_write -- pull in the RV64 syscall
20// vocabulary that the assembler will run under (it produces RV64 ELFs
21// and is itself expected to execute on RV64 targets).
22import "../runtime/nx_syscalls.nx"
23
24// ===== encoder =====================================================
25
26func enc_r(opcode: i64, funct3: i64, funct7: i64,
27 rd: i64, rs1: i64, rs2: i64) -> i64 {
28 return (funct7 << 25) | (rs2 << 20) | (rs1 << 15)
29 | (funct3 << 12) | (rd << 7) | opcode
30}
31
32func enc_i(opcode: i64, funct3: i64,
33 rd: i64, rs1: i64, imm: i64) -> i64 {
34 let imm12: i64 = imm & 0xFFF
35 return (imm12 << 20) | (rs1 << 15) | (funct3 << 12)
36 | (rd << 7) | opcode
37}
38
39func enc_s(opcode: i64, funct3: i64,
40 rs1: i64, rs2: i64, imm: i64) -> i64 {
41 let hi: i64 = (imm >> 5) & 0x7F
42 let lo: i64 = imm & 0x1F
43 return (hi << 25) | (rs2 << 20) | (rs1 << 15)
44 | (funct3 << 12) | (lo << 7) | opcode
45}
46
47func enc_u(opcode: i64, rd: i64, imm20: i64) -> i64 {
48 return ((imm20 & 0xFFFFF) << 12) | (rd << 7) | opcode
49}
50
51// J-type with bit-scrambled 21-bit signed offset.
52func enc_j(opcode: i64, rd: i64, imm: i64) -> i64 {
53 let b20: i64 = (imm >> 20) & 1
54 let b10_1: i64 = (imm >> 1) & 0x3FF
55 let b11: i64 = (imm >> 11) & 1
56 let b19_12: i64 = (imm >> 12) & 0xFF
57 return (b20 << 31) | (b10_1 << 21) | (b11 << 20)
58 | (b19_12 << 12) | (rd << 7) | opcode
59}
60
61// B-type 13-bit signed offset.
62func enc_b(opcode: i64, funct3: i64,
63 rs1: i64, rs2: i64, imm: i64) -> i64 {
64 let b12: i64 = (imm >> 12) & 1
65 let b10_5: i64 = (imm >> 5) & 0x3F
66 let b4_1: i64 = (imm >> 1) & 0xF
67 let b11: i64 = (imm >> 11) & 1
68 return (b12 << 31) | (b10_5 << 25) | (rs2 << 20) | (rs1 << 15)
69 | (funct3 << 12) | (b4_1 << 8) | (b11 << 7) | opcode
70}
71
72// ===== tokens =======================================================
73
74struct AsmTok {
75 kind: i64,
76 val: i64,
77 start: i64,
78 len: i64,
79}
80
81func asm_is_alpha(c: i64) -> i64 {
82 if c >= 0x41 { if c <= 0x5A { return 1 } }
83 if c >= 0x61 { if c <= 0x7A { return 1 } }
84 if c == 0x5F { return 1 }
85 if c == 0x2E { return 1 }
86 return 0
87}
88func asm_is_digit(c: i64) -> i64 {
89 if c >= 0x30 { if c <= 0x39 { return 1 } }
90 return 0
91}
92func is_hex(c: i64) -> i64 {
93 if asm_is_digit(c) { return 1 }
94 if c >= 0x41 { if c <= 0x46 { return 1 } }
95 if c >= 0x61 { if c <= 0x66 { return 1 } }
96 return 0
97}
98func hexv(c: i64) -> i64 {
99 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } }
100 if c >= 0x41 { if c <= 0x46 { return c - 0x37 } }
101 if c >= 0x61 { if c <= 0x66 { return c - 0x57 } }
102 return 0
103}
104func is_space(c: i64) -> i64 {
105 if c == 0x20 { return 1 }
106 if c == 0x09 { return 1 }
107 if c == 0x0A { return 1 }
108 if c == 0x0D { return 1 }
109 return 0
110}
111
112func lex_one(src: *u8, pos: i64, len: i64, out: *AsmTok) -> i64 {
113 var done_ws: i64 = 0
114 while done_ws == 0 {
115 if pos >= len { done_ws = 1 }
116 if done_ws == 0 {
117 let c: i64 = src[pos]
118 if is_space(c) { pos = pos + 1 }
119 if is_space(c) == 0 {
120 if c == 0x23 {
121 var nl: i64 = 0
122 while nl == 0 {
123 if pos >= len { nl = 1 }
124 if nl == 0 {
125 if src[pos] == 0x0A { pos = pos + 1; nl = 1 }
126 if nl == 0 { pos = pos + 1 }
127 }
128 }
129 }
130 if c != 0x23 { done_ws = 1 }
131 }
132 }
133 }
134 if pos >= len {
135 out.kind = 0; out.start = pos; out.len = 0; out.val = 0
136 return pos
137 }
138 let c0: i64 = src[pos]
139 var sign: i64 = 1
140 if c0 == 0x2D {
141 if pos + 1 < len {
142 if asm_is_digit(src[pos + 1]) { sign = -1; pos = pos + 1 }
143 }
144 }
145 let c1: i64 = src[pos]
146 if asm_is_digit(c1) {
147 var v: i64 = 0
148 let s: i64 = pos
149 if c1 == 0x30 {
150 if pos + 1 < len {
151 if src[pos + 1] == 0x78 {
152 pos = pos + 2
153 while pos < len {
154 let d: i64 = src[pos]
155 if is_hex(d) == 0 { break }
156 v = (v << 4) | hexv(d)
157 pos = pos + 1
158 }
159 out.kind = 2; out.val = v * sign; out.start = s; out.len = pos - s
160 return pos
161 }
162 }
163 }
164 while pos < len {
165 let d: i64 = src[pos]
166 if asm_is_digit(d) == 0 { break }
167 v = v * 10 + (d - 0x30)
168 pos = pos + 1
169 }
170 out.kind = 2; out.val = v * sign; out.start = s; out.len = pos - s
171 return pos
172 }
173 if asm_is_alpha(c1) {
174 let s: i64 = pos
175 pos = pos + 1
176 while pos < len {
177 let d: i64 = src[pos]
178 if asm_is_alpha(d) == 0 {
179 if asm_is_digit(d) == 0 { break }
180 }
181 pos = pos + 1
182 }
183 out.kind = 1; out.val = pos - s; out.start = s; out.len = pos - s
184 return pos
185 }
186 if c1 == 0x22 {
187 let s: i64 = pos
188 pos = pos + 1
189 while pos < len {
190 if src[pos] == 0x22 { break }
191 pos = pos + 1
192 }
193 if pos < len { pos = pos + 1 }
194 out.kind = 4; out.val = pos - s; out.start = s; out.len = pos - s
195 return pos
196 }
197 out.kind = 3; out.val = c1; out.start = pos; out.len = 1
198 return pos + 1
199}
200
201// ===== register-name lookup ========================================
202
203func s_eq2(src: *u8, off: i64, len: i64, a: i64, b: i64) -> i64 {
204 if len != 2 { return 0 }
205 if src[off] != a { return 0 }
206 if src[off + 1] != b { return 0 }
207 return 1
208}
209func s_eq3(src: *u8, off: i64, len: i64, a: i64, b: i64, c: i64) -> i64 {
210 if len != 3 { return 0 }
211 if src[off] != a { return 0 }
212 if src[off + 1] != b { return 0 }
213 if src[off + 2] != c { return 0 }
214 return 1
215}
216func s_eq4(src: *u8, off: i64, len: i64, a: i64, b: i64, c: i64, d: i64) -> i64 {
217 if len != 4 { return 0 }
218 if src[off] != a { return 0 }
219 if src[off + 1] != b { return 0 }
220 if src[off + 2] != c { return 0 }
221 if src[off + 3] != d { return 0 }
222 return 1
223}
224func s_eq5(src: *u8, off: i64, len: i64, a: i64, b: i64, c: i64, d: i64, e: i64) -> i64 {
225 if len != 5 { return 0 }
226 if src[off] != a { return 0 }
227 if src[off + 1] != b { return 0 }
228 if src[off + 2] != c { return 0 }
229 if src[off + 3] != d { return 0 }
230 if src[off + 4] != e { return 0 }
231 return 1
232}
233// Was: s_eq6(src, off, len, a, b, c, d, e, f) -- 9 args. The
234// self-host compiler's Instr struct only has 8 inline operand slots
235// (op0..op7); a 9-arg call falls through to bogus `mv a8, ...`
236// emission at codegen time (a8 isn't a real RV64 register). Blocked
237// Wheeler DDC self-self compile until refactored. 2026-05-16 fix:
238// take the 6 bytes as a string pointer + length so the call is 5
239// args, well under the ABI limit. Caller passes a string literal
240// like ".asciz" instead of 6 char codes.
241func s_eq6(src: *u8, off: i64, len: i64, pat: *u8, plen: i64) -> i64 {
242 if len != plen { return 0 }
243 var i: i64 = 0
244 while i < plen {
245 if src[off + i] != pat[i] { return 0 }
246 i = i + 1
247 }
248 return 1
249}
250
251func reg_lookup(src: *u8, off: i64, len: i64) -> i64 {
252 if len == 2 {
253 let a: i64 = src[off]
254 let b: i64 = src[off + 1]
255 if a == 0x61 { if b >= 0x30 { if b <= 0x37 { return 10 + (b - 0x30) } } }
256 if a == 0x74 {
257 if b >= 0x30 { if b <= 0x32 { return 5 + (b - 0x30) } }
258 if b >= 0x33 { if b <= 0x36 { return 28 + (b - 0x33) } }
259 }
260 if a == 0x73 {
261 if b == 0x30 { return 8 }
262 if b == 0x31 { return 9 }
263 if b >= 0x32 { if b <= 0x39 { return 18 + (b - 0x32) } }
264 if b == 0x70 { return 2 }
265 }
266 // Raw x-form: x0..x9 (Task #37). Kernel trap.S uses these
267 // directly instead of ABI names. Without this branch nxasm
268 // bails on `sd x1, 0(sp)` with misleading "bad mnemonic 'sd'".
269 if a == 0x78 { if b >= 0x30 { if b <= 0x39 { return b - 0x30 } } }
270 if s_eq2(src, off, len, 0x72, 0x61) { return 1 }
271 if s_eq2(src, off, len, 0x67, 0x70) { return 3 }
272 if s_eq2(src, off, len, 0x74, 0x70) { return 4 }
273 if s_eq2(src, off, len, 0x66, 0x70) { return 8 }
274 }
275 if len == 3 {
276 if s_eq3(src, off, len, 0x73, 0x31, 0x30) { return 26 }
277 if s_eq3(src, off, len, 0x73, 0x31, 0x31) { return 27 }
278 // Raw x-form: x10..x31 (Task #37).
279 let a3: i64 = src[off]
280 let b3: i64 = src[off + 1]
281 let c3: i64 = src[off + 2]
282 if a3 == 0x78 {
283 if b3 == 0x31 { // x10..x19
284 if c3 >= 0x30 { if c3 <= 0x39 { return 10 + (c3 - 0x30) } }
285 }
286 if b3 == 0x32 { // x20..x29
287 if c3 >= 0x30 { if c3 <= 0x39 { return 20 + (c3 - 0x30) } }
288 }
289 if b3 == 0x33 { // x30, x31
290 if c3 == 0x30 { return 30 }
291 if c3 == 0x31 { return 31 }
292 }
293 }
294 }
295 if len == 4 {
296 if s_eq4(src, off, len, 0x7A, 0x65, 0x72, 0x6F) { return 0 }
297 }
298 return -1
299}
300
301// ===== label table =================================================
302//
303// Label array. The assembler walks tokens twice; pass 1 fills the
304// table with (name, addr), pass 2 consults it for offset resolution.
305
306struct Label {
307 name_start: i64,
308 name_len: i64,
309 addr: i64,
310 defined: i64,
311}
312
313struct Asm {
314 src: *u8,
315 src_len: i64,
316 pos: i64, // token cursor
317 cur_addr: i64, // address in the text section as we emit
318 pass: i64, // 1 or 2
319 out: *u8, // output byte buffer (pass 2)
320 out_pos: i64, // next byte to write
321 labels: *Label,
322 n_labels: i64,
323}
324
325func label_eq(src: *u8, a_off: i64, a_len: i64,
326 b_off: i64, b_len: i64) -> i64 {
327 if a_len != b_len { return 0 }
328 var i: i64 = 0
329 while i < a_len {
330 if src[a_off + i] != src[b_off + i] { return 0 }
331 i = i + 1
332 }
333 return 1
334}
335
336// Find a label by name; returns index or -1.
337func label_find(A: *Asm, name_off: i64, name_len: i64) -> i64 {
338 var i: i64 = 0
339 let base: i64 = A.labels as i64
340 while i < A.n_labels {
341 let lab: *Label = (base + i * 32) as *Label
342 if label_eq(A.src, lab.name_start, lab.name_len, name_off, name_len) {
343 return i
344 }
345 i = i + 1
346 }
347 return -1
348}
349
350// Define a label at the current address.
351// Bounds-checked against the pool size set in `assemble`. If we
352// overflow, print a loud message + return -1 so the caller can decide
353// (today: assemble_pass treats <0 as fatal).
354const LABELS_CAP: i64 = 8192
355
356func label_define(A: *Asm, name_off: i64, name_len: i64) -> i64 {
357 let idx: i64 = A.n_labels
358 if idx >= LABELS_CAP {
359 sys_write(2, "nxasm: label pool overflow (cap=" as *u8, 32)
360 sys_write(2, "8192)\n" as *u8, 6)
361 return -1
362 }
363 let base: i64 = A.labels as i64
364 let lab: *Label = (base + idx * 32) as *Label
365 lab.name_start = name_off
366 lab.name_len = name_len
367 lab.addr = A.cur_addr
368 lab.defined = 1
369 A.n_labels = idx + 1
370 return idx
371}
372
373// ===== byte emission helpers (pass 2 only) ==========================
374
375func emit_u32(A: *Asm, v: i64) -> i64 {
376 if A.pass == 2 {
377 let out: *u8 = A.out
378 let p: i64 = A.out_pos
379 out[p] = v & 0xFF
380 out[p + 1] = (v >> 8) & 0xFF
381 out[p + 2] = (v >> 16) & 0xFF
382 out[p + 3] = (v >> 24) & 0xFF
383 A.out_pos = p + 4
384 }
385 A.cur_addr = A.cur_addr + 4
386 return 0
387}
388
389func emit_byte(A: *Asm, v: i64) -> i64 {
390 if A.pass == 2 {
391 let out: *u8 = A.out
392 let p: i64 = A.out_pos
393 out[p] = v & 0xFF
394 A.out_pos = p + 1
395 }
396 A.cur_addr = A.cur_addr + 1
397 return 0
398}
399
400// ===== parser helpers =============================================
401
402func next_tok(A: *Asm, t: *AsmTok) -> i64 {
403 A.pos = lex_one(A.src, A.pos, A.src_len, t)
404 return A.pos
405}
406func expect_punct(A: *Asm, t: *AsmTok, c: i64) -> i64 {
407 next_tok(A, t)
408 if t.kind != 3 { return 1 }
409 if t.val != c { return 2 }
410 return 0
411}
412func expect_reg(A: *Asm, t: *AsmTok) -> i64 {
413 next_tok(A, t)
414 if t.kind != 1 { return -1 }
415 return reg_lookup(A.src, t.start, t.len)
416}
417func expect_int(A: *Asm, t: *AsmTok, err: *i64) -> i64 {
418 next_tok(A, t)
419 if t.kind != 2 {
420 *err = 1
421 return 0
422 }
423 *err = 0
424 return t.val
425}
426
427// ===== instruction parsers (tokens after mnemonic) =================
428
429func parse_rrr(A: *Asm, t: *AsmTok,
430 opcode: i64, funct3: i64, funct7: i64) -> i64 {
431 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
432 if expect_punct(A, t, 0x2C) != 0 { return -1 }
433 let rs1: i64 = expect_reg(A, t); if rs1 < 0 { return -1 }
434 if expect_punct(A, t, 0x2C) != 0 { return -1 }
435 let rs2: i64 = expect_reg(A, t); if rs2 < 0 { return -1 }
436 return enc_r(opcode, funct3, funct7, rd, rs1, rs2)
437}
438func parse_rri(A: *Asm, t: *AsmTok, opcode: i64, funct3: i64) -> i64 {
439 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
440 if expect_punct(A, t, 0x2C) != 0 { return -1 }
441 let rs1: i64 = expect_reg(A, t); if rs1 < 0 { return -1 }
442 if expect_punct(A, t, 0x2C) != 0 { return -1 }
443 var err: i64 = 0
444 let imm: i64 = expect_int(A, t, &err)
445 if err != 0 { return -1 }
446 return enc_i(opcode, funct3, rd, rs1, imm)
447}
448func parse_mem(A: *Asm, t: *AsmTok, is_store: i64, funct3: i64) -> i64 {
449 let first: i64 = expect_reg(A, t); if first < 0 { return -1 }
450 if expect_punct(A, t, 0x2C) != 0 { return -1 }
451 var err: i64 = 0
452 let imm: i64 = expect_int(A, t, &err); if err != 0 { return -1 }
453 if expect_punct(A, t, 0x28) != 0 { return -1 }
454 let second: i64 = expect_reg(A, t); if second < 0 { return -1 }
455 if expect_punct(A, t, 0x29) != 0 { return -1 }
456 if is_store { return enc_s(0x23, funct3, second, first, imm) }
457 return enc_i(0x03, funct3, first, second, imm)
458}
459// `li rd, imm` -- macro that expands to 1, 2, or 6 instructions
460// depending on the immediate width. The old form silently truncated
461// to 12 bits via enc_i's masking, mangling 64-bit literals like
462// 0x0001000100010001 (the SIMD smoke's packed-i16 pattern) into
463// a stored value of 1. Fixed 2026-05-16 after the silent-truncate
464// bug took 2 hours to bisect through the self-host SIMD pipeline.
465//
466// Expansion forms:
467// 12-bit signed : addi rd, zero, imm
468// 32-bit signed : lui rd, hi20 ; addi rd, rd, lo12 (sign-adjusted)
469// 64-bit any : lui rd, h32_hi20 ; addi rd, rd, h32_lo12
470// slli rd, rd, 32
471// lui t6, l32_hi20 ; addi t6, t6, l32_lo12
472// add rd, rd, t6
473//
474// The 64-bit form uses t6 (= x31) as scratch -- matches the
475// codegen-side scratch reservation used by nx_riscv.nx's
476// emit_sp_* helpers.
477func parse_li(A: *Asm, t: *AsmTok) -> i64 {
478 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
479 if expect_punct(A, t, 0x2C) != 0 { return -1 }
480 var err: i64 = 0
481 let imm: i64 = expect_int(A, t, &err); if err != 0 { return -1 }
482 // 12-bit signed fits in one addi.
483 if imm >= -2048 {
484 if imm <= 2047 {
485 return enc_i(0x13, 0, rd, 0, imm)
486 }
487 }
488 // Does the immediate fit in signed 32-bit? If so, use the
489 // 2-instruction lui+addi form. Otherwise fall through to the
490 // 8-instruction 64-bit construction.
491 var fits: i64 = 0
492 if imm >= (0 - 2147483648) {
493 if imm <= 2147483647 {
494 fits = 1
495 }
496 }
497 if fits == 1 {
498 let hi20: i64 = (imm + 0x800) >> 12
499 let lo12: i64 = imm - (hi20 << 12)
500 let lui_word: i64 = enc_u(0x37, rd, hi20)
501 emit_u32(A, lui_word)
502 if lo12 == 0 {
503 // Returning nop would still emit a u32; we want NO further
504 // instruction. Hack: return enc that the dispatcher emits,
505 // then deduct... Simpler: emit one safe addi rd, rd, 0
506 // which is `mv rd, rd` (no-op). Cheaper alternative:
507 // return the second emit ourselves and let dispatch emit
508 // a nop. Use the dispatcher's slot for the addi.
509 return enc_i(0x13, 0, rd, rd, 0)
510 }
511 return enc_i(0x13, 0, rd, rd, lo12)
512 }
513 // 64-bit case. Build via high32 << 32 | low32.
514 let hi32: i64 = (imm >> 32) & 0xFFFFFFFF
515 let lo32: i64 = imm & 0xFFFFFFFF
516 // Hi32 -- split into hi20 + lo12. Treat as signed 32-bit when
517 // computing the addi adjustment.
518 var hi32_s: i64 = hi32
519 if hi32_s >= 0x80000000 { hi32_s = hi32_s - 0x100000000 }
520 let hh20: i64 = (hi32_s + 0x800) >> 12
521 let hl12: i64 = hi32_s - (hh20 << 12)
522 // Lo32 -- same split. We need t6 to hold a value WHOSE low 32
523 // bits = lo32 and whose upper bits ZERO -- otherwise the `add`
524 // at the end would over-extend into the upper half. The
525 // canonical trick: shift up then down to zero-extend. Easier:
526 // use `srli` to clear upper bits before add.
527 var lo32_s: i64 = lo32
528 if lo32_s >= 0x80000000 { lo32_s = lo32_s - 0x100000000 }
529 let lh20: i64 = (lo32_s + 0x800) >> 12
530 let ll12: i64 = lo32_s - (lh20 << 12)
531 // Sequence:
532 // lui rd, hh20
533 // addi rd, rd, hl12
534 // slli rd, rd, 32
535 // lui t6, lh20
536 // addi t6, t6, ll12
537 // slli t6, t6, 32 ; srli t6, t6, 32 (zero-extend)
538 // add rd, rd, t6
539 emit_u32(A, enc_u(0x37, rd, hh20))
540 emit_u32(A, enc_i(0x13, 0, rd, rd, hl12))
541 emit_u32(A, enc_i(0x13, 1, rd, rd, 32)) // slli rd, rd, 32
542 emit_u32(A, enc_u(0x37, 31, lh20)) // lui t6, lh20
543 emit_u32(A, enc_i(0x13, 0, 31, 31, ll12)) // addi t6, t6, ll12
544 emit_u32(A, enc_i(0x13, 1, 31, 31, 32)) // slli t6, t6, 32
545 emit_u32(A, enc_i(0x13, 5, 31, 31, 32)) // srli t6, t6, 32 (zero-ext low32)
546 return enc_r(0x33, 0, 0, rd, rd, 31) // add rd, rd, t6
547}
548func parse_mv(A: *Asm, t: *AsmTok) -> i64 {
549 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
550 if expect_punct(A, t, 0x2C) != 0 { return -1 }
551 let rs: i64 = expect_reg(A, t); if rs < 0 { return -1 }
552 return enc_i(0x13, 0, rd, rs, 0)
553}
554// `srai rd, rs1, shamt` -- arithmetic right shift (sign-extending).
555// Distinct from srli (logical right shift, zero-fill) by funct7
556// bits 31:26 = 010000 (= 0x10 in the high 6 bits of the immediate
557// field, which becomes shamt's neighbor in the I-type encoding).
558// nxasm previously routed srai through the same parse_rri as srli,
559// silently turning `srai t6, t6, 48` into a logical shift -- which
560// is what made vreduce_min/max return UNSIGNED i16 values instead
561// of sign-extended i64 (2026-05-16, bisected from
562// _self_host_pack4_neg_test.nx returning 65537 instead of 1).
563func parse_srai(A: *Asm, t: *AsmTok) -> i64 {
564 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
565 if expect_punct(A, t, 0x2C) != 0 { return -1 }
566 let rs1: i64 = expect_reg(A, t); if rs1 < 0 { return -1 }
567 if expect_punct(A, t, 0x2C) != 0 { return -1 }
568 var err: i64 = 0
569 let shamt: i64 = expect_int(A, t, &err)
570 if err != 0 { return -1 }
571 // Encoding: bits 31:26 = 010000 (funct6 for srai in RV64), bits
572 // 25:20 = shamt (6 bits since XLEN=64). Combined imm field
573 // (bits 31:20) = 0x400 | (shamt & 0x3F).
574 let imm: i64 = 0x400 | (shamt & 0x3F)
575 return enc_i(0x13, 5, rd, rs1, imm)
576}
577
578// `j label` and `jal label` -- we parse just the label identifier,
579// look up its address, compute offset from cur_addr (pass 2 only),
580// and encode a JAL instruction. rd=0 for `j`, rd=1 for `jal` unless
581// a rd is given explicitly.
582func parse_j_label(A: *Asm, t: *AsmTok, rd: i64) -> i64 {
583 next_tok(A, t)
584 if t.kind != 1 {
585 sys_write(2, "nxasm:j: kind != 1\n" as *u8, 19)
586 return -1
587 }
588 if A.pass == 1 {
589 return enc_j(0x6F, rd, 0) // pass 1: offset unknown, any value
590 }
591 let idx: i64 = label_find(A, t.start, t.len)
592 if idx < 0 {
593 sys_write(2, "nxasm:j: label not found '" as *u8, 26)
594 var di: i64 = 0
595 while di < t.len {
596 sys_write(2, (((A.src as i64) + t.start + di) as *u8), 1)
597 di = di + 1
598 }
599 sys_write(2, "' n_labels=" as *u8, 11)
600 let dnbuf: *u8 = sys_mmap(16)
601 var dn: i64 = A.n_labels
602 var dk: i64 = 0
603 if dn == 0 { dnbuf[dk] = 0x30; dk = 1 }
604 while dn > 0 {
605 dnbuf[dk] = 0x30 + (dn - (dn / 10) * 10)
606 dn = dn / 10
607 dk = dk + 1
608 }
609 var dj: i64 = dk - 1
610 while dj >= 0 {
611 sys_write(2, (((dnbuf as i64) + dj) as *u8), 1)
612 dj = dj - 1
613 }
614 sys_write(2, "\n" as *u8, 1)
615 return -1
616 }
617 let base: i64 = A.labels as i64
618 let lab: *Label = (base + idx * 32) as *Label
619 let off: i64 = lab.addr - A.cur_addr
620 return enc_j(0x6F, rd, off)
621}
622
623// `bnez rs1, label` -> beq rs1, zero, label with funct3=1 (bne).
624// `beqz rs1, label` -> beq rs1, zero, label with funct3=0 (beq).
625func parse_branch_zero(A: *Asm, t: *AsmTok, funct3: i64) -> i64 {
626 let rs1: i64 = expect_reg(A, t); if rs1 < 0 { return -1 }
627 if expect_punct(A, t, 0x2C) != 0 { return -1 }
628 next_tok(A, t)
629 if t.kind != 1 { return -1 }
630 if A.pass == 1 {
631 return enc_b(0x63, funct3, rs1, 0, 0)
632 }
633 let idx: i64 = label_find(A, t.start, t.len)
634 if idx < 0 { return -1 }
635 let base: i64 = A.labels as i64
636 let lab: *Label = (base + idx * 32) as *Label
637 let off: i64 = lab.addr - A.cur_addr
638 return enc_b(0x63, funct3, rs1, 0, off)
639}
640
641func parse_ret(A: *Asm, t: *AsmTok) -> i64 { return enc_i(0x67, 0, 0, 1, 0) }
642func parse_ecall(A: *Asm, t: *AsmTok) -> i64 { return enc_i(0x73, 0, 0, 0, 0) }
643
644// B-type branch family (Task #36). Full 3-operand form:
645// beq/bne/blt/bge/bltu/bgeu rs1, rs2, label
646// funct3 selects op: 0=BEQ, 1=BNE, 4=BLT, 5=BGE, 6=BLTU, 7=BGEU.
647// Pass 1: emit a placeholder (offset=0) so label_define sees the size.
648// Pass 2: resolve label address and emit the real offset.
649func parse_branch(A: *Asm, t: *AsmTok, funct3: i64) -> i64 {
650 let rs1: i64 = expect_reg(A, t); if rs1 < 0 { return -1 }
651 if expect_punct(A, t, 0x2C) != 0 { return -1 }
652 let rs2: i64 = expect_reg(A, t); if rs2 < 0 { return -1 }
653 if expect_punct(A, t, 0x2C) != 0 { return -1 }
654 next_tok(A, t)
655 if t.kind != 1 { return -1 }
656 if A.pass == 1 {
657 return enc_b(0x63, funct3, rs1, rs2, 0)
658 }
659 let idx: i64 = label_find(A, t.start, t.len)
660 if idx < 0 { return -1 }
661 let base: i64 = A.labels as i64
662 let lab: *Label = (base + idx * 32) as *Label
663 let off: i64 = lab.addr - A.cur_addr
664 return enc_b(0x63, funct3, rs1, rs2, off)
665}
666
667// ===== CSR instructions (Task #35) ================================
668//
669// CSR ops are I-type with opcode=0x73, funct3 selecting the op:
670// 001 CSRRW 010 CSRRS 011 CSRRC
671// 101 CSRRWI 110 CSRRSI 111 CSRRCI
672// Immediate field carries the 12-bit CSR address.
673//
674// Full form: `csrrw rd, csr, rs` -- 3 operands.
675// Pseudo: `csrr rd, csr` = csrrs rd, csr, x0 (read)
676// `csrw csr, rs` = csrrw x0, csr, rs (write)
677// `csrs csr, rs` = csrrs x0, csr, rs (set bits)
678// `csrc csr, rs` = csrrc x0, csr, rs (clear bits)
679//
680// CSR address may be a numeric literal (nxc2 emits 2816, 832 etc.) OR
681// a symbolic name (kernel .S files use mstatus, mhartid, etc.).
682
683// Read CSR address from next token: accepts either integer literal
684// or known symbolic name (mstatus / mhartid / mscratch / mepc /
685// mtvec / mcause / mtval / mie / mip / mcycle). Returns the 12-bit
686// CSR address; sets *err on unknown name or non-int/non-ident token.
687func expect_csr_or_int(A: *Asm, t: *AsmTok, err: *i64) -> i64 {
688 next_tok(A, t)
689 *err = 0
690 if t.kind == 2 { return t.val }
691 if t.kind == 1 {
692 let off: i64 = t.start
693 let n: i64 = t.len
694 if n == 3 {
695 if s_eq3(A.src, off, n, 0x6D, 0x69, 0x65) { return 0x304 } // mie
696 if s_eq3(A.src, off, n, 0x6D, 0x69, 0x70) { return 0x344 } // mip
697 }
698 if n == 4 {
699 if s_eq4(A.src, off, n, 0x6D, 0x65, 0x70, 0x63) { return 0x341 } // mepc
700 }
701 if n == 5 {
702 if s_eq5(A.src, off, n, 0x6D, 0x74, 0x76, 0x65, 0x63) { return 0x305 } // mtvec
703 if s_eq5(A.src, off, n, 0x6D, 0x74, 0x76, 0x61, 0x6C) { return 0x343 } // mtval
704 }
705 if n == 6 {
706 // mcause / mcycle (both length-6, common prefix "mc")
707 let mcause_pat: *u8 = "mcause" as *u8
708 if s_eq6(A.src, off, n, mcause_pat, 6) { return 0x342 }
709 let mcycle_pat: *u8 = "mcycle" as *u8
710 if s_eq6(A.src, off, n, mcycle_pat, 6) { return 0xB00 }
711 }
712 if n == 7 {
713 let mstatus_pat: *u8 = "mstatus" as *u8
714 if s_eq6(A.src, off, n, mstatus_pat, 7) { return 0x300 }
715 let mhartid_pat: *u8 = "mhartid" as *u8
716 if s_eq6(A.src, off, n, mhartid_pat, 7) { return 0xF14 }
717 }
718 if n == 8 {
719 let mscratch_pat: *u8 = "mscratch" as *u8
720 if s_eq6(A.src, off, n, mscratch_pat, 8) { return 0x340 }
721 }
722 }
723 *err = 1
724 return 0
725}
726
727// `csrrw rd, csr, rs` (3-operand form, used by csrrw/csrrs/csrrc).
728func parse_csr_full(A: *Asm, t: *AsmTok, funct3: i64) -> i64 {
729 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
730 if expect_punct(A, t, 0x2C) != 0 { return -1 }
731 var err: i64 = 0
732 let csr: i64 = expect_csr_or_int(A, t, &err); if err != 0 { return -1 }
733 if expect_punct(A, t, 0x2C) != 0 { return -1 }
734 let rs: i64 = expect_reg(A, t); if rs < 0 { return -1 }
735 return enc_i(0x73, funct3, rd, rs, csr & 0xFFF)
736}
737
738// `csrr rd, csr` = csrrs rd, csr, x0.
739func parse_csrr(A: *Asm, t: *AsmTok) -> i64 {
740 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
741 if expect_punct(A, t, 0x2C) != 0 { return -1 }
742 var err: i64 = 0
743 let csr: i64 = expect_csr_or_int(A, t, &err); if err != 0 { return -1 }
744 return enc_i(0x73, 2, rd, 0, csr & 0xFFF)
745}
746
747// `csrw csr, rs` = csrrw x0, csr, rs. Also covers csrs / csrc with
748// different funct3.
749func parse_csr_write(A: *Asm, t: *AsmTok, funct3: i64) -> i64 {
750 var err: i64 = 0
751 let csr: i64 = expect_csr_or_int(A, t, &err); if err != 0 { return -1 }
752 if expect_punct(A, t, 0x2C) != 0 { return -1 }
753 let rs: i64 = expect_reg(A, t); if rs < 0 { return -1 }
754 return enc_i(0x73, funct3, 0, rs, csr & 0xFFF)
755}
756
757// seqz rd, rs -- rd = (rs == 0)? Pseudo: sltiu rd, rs, 1.
758func parse_seqz(A: *Asm, t: *AsmTok) -> i64 {
759 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
760 if expect_punct(A, t, 0x2C) != 0 { return -1 }
761 let rs: i64 = expect_reg(A, t); if rs < 0 { return -1 }
762 return enc_i(0x13, 3, rd, rs, 1) // sltiu rd, rs, 1
763}
764
765// snez rd, rs -- rd = (rs != 0)? Pseudo: sltu rd, x0, rs.
766func parse_snez(A: *Asm, t: *AsmTok) -> i64 {
767 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
768 if expect_punct(A, t, 0x2C) != 0 { return -1 }
769 let rs: i64 = expect_reg(A, t); if rs < 0 { return -1 }
770 return enc_r(0x33, 3, 0, rd, 0, rs) // sltu rd, x0, rs
771}
772
773// `la rd, sym` -- two-instruction expansion:
774// auipc rd, (sym_hi20)
775// addi rd, rd, (sym_lo12)
776// emit_u32 the first word inside, return the second so the caller
777// emits it normally. pass-1: advance cur_addr by 8.
778func parse_la(A: *Asm, t: *AsmTok) -> i64 {
779 let rd: i64 = expect_reg(A, t); if rd < 0 { return -1 }
780 if expect_punct(A, t, 0x2C) != 0 { return -1 }
781 next_tok(A, t)
782 if t.kind != 1 { return -1 }
783 var tgt: i64 = 0
784 if A.pass == 2 {
785 let idx: i64 = label_find(A, t.start, t.len)
786 if idx < 0 {
787 // Task #39 diagnostic: name the missing label so the
788 // dispatcher's "bad mnemonic" message points the user at
789 // the actual problem (label resolution, not parsing).
790 sys_write(2, "nxasm:la label-not-found '" as *u8, 26)
791 var di: i64 = 0
792 while di < t.len {
793 sys_write(2, (((A.src as i64) + t.start + di) as *u8), 1)
794 di = di + 1
795 }
796 sys_write(2, "'\n" as *u8, 2)
797 return -1
798 }
799 let base: i64 = A.labels as i64
800 let lab: *Label = (base + idx * 32) as *Label
801 tgt = lab.addr - A.cur_addr
802 }
803 // Split into hi20 + lo12 with proper sign handling: lo12 is
804 // sign-extended by the addi, so pre-add 0x800 to tgt before
805 // taking the hi20 if the low 12 bits are negative-signed.
806 var hi: i64 = (tgt + 0x800) >> 12
807 let lo: i64 = tgt - (hi << 12)
808 let auipc_word: i64 = enc_u(0x17, rd, hi)
809 emit_u32(A, auipc_word)
810 // Return the addi; caller emits it.
811 return enc_i(0x13, 0, rd, rd, lo)
812}
813
814// `tail sym` -- tail call: auipc x6, hi; jalr x0, lo(x6).
815// We follow RISC-V ABI: t1 (x6) is the staging register.
816func parse_tail(A: *Asm, t: *AsmTok) -> i64 {
817 next_tok(A, t)
818 if t.kind != 1 { return -1 }
819 var tgt: i64 = 0
820 if A.pass == 2 {
821 let idx: i64 = label_find(A, t.start, t.len)
822 if idx < 0 { return -1 }
823 let base: i64 = A.labels as i64
824 let lab: *Label = (base + idx * 32) as *Label
825 tgt = lab.addr - A.cur_addr
826 }
827 var hi: i64 = (tgt + 0x800) >> 12
828 let lo: i64 = tgt - (hi << 12)
829 let auipc_word: i64 = enc_u(0x17, 6, hi) // auipc t1, hi
830 emit_u32(A, auipc_word)
831 return enc_i(0x67, 0, 0, 6, lo) // jalr x0, t1, lo
832}
833
834// ===== directive handlers ==========================================
835//
836// Only those nxc2 emits; others fall through to a no-op dispatcher.
837
838// `.byte N [, N ...]`
839func handle_byte(A: *Asm, t: *AsmTok) -> i64 {
840 var more: i64 = 1
841 while more {
842 var err: i64 = 0
843 let v: i64 = expect_int(A, t, &err)
844 if err != 0 { return -1 }
845 emit_byte(A, v)
846 // Peek next token; if it's a comma, consume and continue.
847 let save: i64 = A.pos
848 next_tok(A, t)
849 if t.kind == 3 {
850 if t.val == 0x2C {
851 // comma -- keep going
852 more = 1
853 }
854 if t.val != 0x2C {
855 A.pos = save
856 more = 0
857 }
858 }
859 if t.kind != 3 {
860 A.pos = save
861 more = 0
862 }
863 }
864 return 0
865}
866
867// `.asciz "..."` -- emit the string bytes + a trailing NUL, with
868// gas-style escape processing: \n -> 0x0A, \t -> 0x09, \r -> 0x0D,
869// \\ -> 0x5C, \" -> 0x22, \0 -> 0x00, \ooo -> octal byte. Without
870// this, a `.asciz "hi\n"` emitted by nxc.nx's globals dump would
871// land as 4 literal bytes h, i, \, n -- and sys_write(1, p, 3) would
872// print "hi\\". Closes nxasm side of T#selfhost-003.
873// `.skip N` / `.zero N` -- emit N zero bytes. Used for BSS-style
874// reservations (trap.S allocates 8 KiB trap stack; nxc2-emitted .s
875// uses .zero N for static array reservations). Without this nxasm
876// silently skipped these directives and labels following them landed
877// at the WRONG cur_addr. Closes Phase F1 structural blocker class
878// for single-segment kernel builds.
879func handle_skip(A: *Asm, t: *AsmTok) -> i64 {
880 var err: i64 = 0
881 let n: i64 = expect_int(A, t, &err)
882 if err != 0 { return -1 }
883 if n <= 0 { return 0 }
884 var i: i64 = 0
885 while i < n {
886 emit_byte(A, 0)
887 i = i + 1
888 }
889 return 0
890}
891
892func handle_asciz(A: *Asm, t: *AsmTok) -> i64 {
893 next_tok(A, t)
894 if t.kind != 4 { return -1 }
895 // Walk t.start+1 .. t.start+t.len-1 (skip surrounding quotes).
896 var i: i64 = 1
897 let end: i64 = t.len - 1
898 while i < end {
899 let c: i64 = A.src[t.start + i]
900 if c == 0x5C {
901 if i + 1 < end {
902 let nx: i64 = A.src[t.start + i + 1]
903 if nx == 0x6E { emit_byte(A, 0x0A); i = i + 2; continue } // \n
904 if nx == 0x74 { emit_byte(A, 0x09); i = i + 2; continue } // \t
905 if nx == 0x72 { emit_byte(A, 0x0D); i = i + 2; continue } // \r
906 if nx == 0x5C { emit_byte(A, 0x5C); i = i + 2; continue } // \\
907 if nx == 0x22 { emit_byte(A, 0x22); i = i + 2; continue } // \"
908 if nx == 0x30 {
909 // \0 (single char, NOT followed by more octal digits)
910 // OR \ooo (three octal digits). Distinguish by
911 // looking at the next two characters.
912 if i + 3 < end {
913 let o2: i64 = A.src[t.start + i + 2]
914 let o3: i64 = A.src[t.start + i + 3]
915 if o2 >= 0x30 {
916 if o2 <= 0x37 {
917 if o3 >= 0x30 {
918 if o3 <= 0x37 {
919 let b: i64 = ((nx - 0x30) << 6) |
920 ((o2 - 0x30) << 3) |
921 (o3 - 0x30)
922 emit_byte(A, b)
923 i = i + 4
924 continue
925 }
926 }
927 }
928 }
929 }
930 emit_byte(A, 0x00); i = i + 2; continue
931 }
932 // \ooo for 1..3 -- treat first digit as octal escape.
933 if nx >= 0x31 {
934 if nx <= 0x37 {
935 if i + 3 < end {
936 let o2: i64 = A.src[t.start + i + 2]
937 let o3: i64 = A.src[t.start + i + 3]
938 if o2 >= 0x30 {
939 if o2 <= 0x37 {
940 if o3 >= 0x30 {
941 if o3 <= 0x37 {
942 let b: i64 = ((nx - 0x30) << 6) |
943 ((o2 - 0x30) << 3) |
944 (o3 - 0x30)
945 emit_byte(A, b)
946 i = i + 4
947 continue
948 }
949 }
950 }
951 }
952 }
953 }
954 }
955 // Unknown escape: pass through both bytes (gas warns;
956 // we keep the backslash + literal char).
957 emit_byte(A, c)
958 emit_byte(A, nx)
959 i = i + 2
960 continue
961 }
962 }
963 emit_byte(A, c)
964 i = i + 1
965 }
966 emit_byte(A, 0)
967 return 0
968}
969
970// `.word N` -- emit one 32-bit little-endian word. Used by
971// rv_emit_simd_vdot_i16 (and future SIMD/SIMT lowerings) to emit
972// pre-encoded RVV instructions without needing a full vector
973// mnemonic parser in nxasm. Bits-up sovereign path: nx_riscv.nx
974// pre-computes the 32-bit RVV encoding (substituting in the
975// run-time register IDs) and hands the assembler an opaque word.
976//
977// Supports multiple comma-separated words on one line, matching the
978// `.byte` shape, so the codegen can pack a whole RVV chain into a
979// single directive if desired.
980func handle_word(A: *Asm, t: *AsmTok) -> i64 {
981 var more: i64 = 1
982 while more {
983 var err: i64 = 0
984 let v: i64 = expect_int(A, t, &err)
985 if err != 0 { return -1 }
986 emit_u32(A, v)
987 let save: i64 = A.pos
988 next_tok(A, t)
989 if t.kind == 3 {
990 if t.val == 0x2C { more = 1 }
991 if t.val != 0x2C { A.pos = save; more = 0 }
992 }
993 if t.kind != 3 { A.pos = save; more = 0 }
994 }
995 return 0
996}
997
998// Ignored directive: eat the rest of the line (everything till \n).
999// Detects via tokens already having different lines; simpler: eat
1000// tokens until we hit a token whose start column is after a newline
1001// in source. Crudest but works: consume tokens while the next char
1002// in src is NOT a newline.
1003func skip_to_newline(A: *Asm) -> i64 {
1004 while A.pos < A.src_len {
1005 let c: i64 = A.src[A.pos]
1006 if c == 0x0A { return 0 }
1007 A.pos = A.pos + 1
1008 }
1009 return 0
1010}
1011
1012// ===== mnemonic dispatch ============================================
1013
1014func assemble_mnemonic(A: *Asm, t: *AsmTok, off: i64, n: i64) -> i64 {
1015 // 1-char mnemonics: "j label"
1016 if n == 1 {
1017 if A.src[off] == 0x6A { return parse_j_label(A, t, 0) }
1018 }
1019 if n == 2 {
1020 if s_eq2(A.src, off, n, 0x6C, 0x69) { return parse_li(A, t) } // li
1021 if s_eq2(A.src, off, n, 0x6D, 0x76) { return parse_mv(A, t) } // mv
1022 if s_eq2(A.src, off, n, 0x6C, 0x64) { return parse_mem(A, t, 0, 3) } // ld
1023 if s_eq2(A.src, off, n, 0x73, 0x64) { return parse_mem(A, t, 1, 3) } // sd
1024 if s_eq2(A.src, off, n, 0x73, 0x62) { return parse_mem(A, t, 1, 0) } // sb (store byte)
1025 if s_eq2(A.src, off, n, 0x73, 0x68) { return parse_mem(A, t, 1, 1) } // sh (store halfword)
1026 if s_eq2(A.src, off, n, 0x73, 0x77) { return parse_mem(A, t, 1, 2) } // sw (store word)
1027 if s_eq2(A.src, off, n, 0x6C, 0x61) { return parse_la(A, t) } // la
1028 if s_eq2(A.src, off, n, 0x6F, 0x72) { return parse_rrr(A, t, 0x33, 6, 0) } // or
1029 }
1030 if n == 3 {
1031 if s_eq3(A.src, off, n, 0x72, 0x65, 0x74) { return parse_ret(A, t) }
1032 if s_eq3(A.src, off, n, 0x61, 0x64, 0x64) { return parse_rrr(A, t, 0x33, 0, 0) } // add
1033 if s_eq3(A.src, off, n, 0x73, 0x75, 0x62) { return parse_rrr(A, t, 0x33, 0, 0x20) } // sub
1034 if s_eq3(A.src, off, n, 0x6A, 0x61, 0x6C) { return parse_j_label(A, t, 1) } // jal
1035 if s_eq3(A.src, off, n, 0x61, 0x6E, 0x64) { return parse_rrr(A, t, 0x33, 7, 0) } // and
1036 if s_eq3(A.src, off, n, 0x78, 0x6F, 0x72) { return parse_rrr(A, t, 0x33, 4, 0) } // xor
1037 if s_eq3(A.src, off, n, 0x6D, 0x75, 0x6C) { return parse_rrr(A, t, 0x33, 0, 1) } // mul (M-ext)
1038 if s_eq3(A.src, off, n, 0x64, 0x69, 0x76) { return parse_rrr(A, t, 0x33, 4, 1) } // div
1039 if s_eq3(A.src, off, n, 0x72, 0x65, 0x6D) { return parse_rrr(A, t, 0x33, 6, 1) } // rem
1040 if s_eq3(A.src, off, n, 0x73, 0x6C, 0x6C) { return parse_rrr(A, t, 0x33, 1, 0) } // sll
1041 if s_eq3(A.src, off, n, 0x73, 0x72, 0x6C) { return parse_rrr(A, t, 0x33, 5, 0) } // srl
1042 if s_eq3(A.src, off, n, 0x73, 0x72, 0x61) { return parse_rrr(A, t, 0x33, 5, 0x20) } // sra
1043 if s_eq3(A.src, off, n, 0x73, 0x6C, 0x74) { return parse_rrr(A, t, 0x33, 2, 0) } // slt
1044 if s_eq3(A.src, off, n, 0x6C, 0x77, 0x75) { return parse_mem(A, t, 0, 6) } // lwu
1045 if s_eq3(A.src, off, n, 0x6E, 0x6F, 0x70) { return enc_i(0x13, 0, 0, 0, 0) } // nop
1046 // B-type branches (Task #36). funct3: 0=beq, 1=bne, 4=blt, 5=bge.
1047 if s_eq3(A.src, off, n, 0x62, 0x65, 0x71) { return parse_branch(A, t, 0) } // beq
1048 if s_eq3(A.src, off, n, 0x62, 0x6E, 0x65) { return parse_branch(A, t, 1) } // bne
1049 if s_eq3(A.src, off, n, 0x62, 0x6C, 0x74) { return parse_branch(A, t, 4) } // blt
1050 if s_eq3(A.src, off, n, 0x62, 0x67, 0x65) { return parse_branch(A, t, 5) } // bge
1051 }
1052 if n == 4 {
1053 if s_eq4(A.src, off, n, 0x61, 0x64, 0x64, 0x69) { return parse_rri(A, t, 0x13, 0) } // addi
1054 if s_eq4(A.src, off, n, 0x62, 0x6E, 0x65, 0x7A) { return parse_branch_zero(A, t, 1) } // bnez
1055 if s_eq4(A.src, off, n, 0x62, 0x65, 0x71, 0x7A) { return parse_branch_zero(A, t, 0) } // beqz
1056 if s_eq4(A.src, off, n, 0x6A, 0x61, 0x6C, 0x72) { return parse_rri(A, t, 0x67, 0) } // jalr
1057 if s_eq4(A.src, off, n, 0x63, 0x61, 0x6C, 0x6C) { return parse_j_label(A, t, 1) } // call
1058 if s_eq4(A.src, off, n, 0x74, 0x61, 0x69, 0x6C) { return parse_tail(A, t) } // tail
1059 if s_eq4(A.src, off, n, 0x73, 0x65, 0x71, 0x7A) { return parse_seqz(A, t) } // seqz
1060 if s_eq4(A.src, off, n, 0x73, 0x6E, 0x65, 0x7A) { return parse_snez(A, t) } // snez
1061 if s_eq4(A.src, off, n, 0x78, 0x6F, 0x72, 0x69) { return parse_rri(A, t, 0x13, 4) } // xori
1062 if s_eq4(A.src, off, n, 0x61, 0x6E, 0x64, 0x69) { return parse_rri(A, t, 0x13, 7) } // andi
1063 if s_eq4(A.src, off, n, 0x73, 0x6C, 0x6C, 0x69) { return parse_rri(A, t, 0x13, 1) } // slli
1064 if s_eq4(A.src, off, n, 0x73, 0x72, 0x6C, 0x69) { return parse_rri(A, t, 0x13, 5) } // srli
1065 if s_eq4(A.src, off, n, 0x73, 0x72, 0x61, 0x69) { return parse_srai(A, t) } // srai -- funct6=0x10
1066 if s_eq4(A.src, off, n, 0x6C, 0x62, 0x75, 0x20) { return parse_mem(A, t, 0, 4) } // lbu (trailing space)
1067 // CSR pseudo-ops (Task #35). csr*=0x63 0x73 0x72 prefix.
1068 if s_eq4(A.src, off, n, 0x63, 0x73, 0x72, 0x72) { return parse_csrr(A, t) } // csrr rd, csr
1069 if s_eq4(A.src, off, n, 0x63, 0x73, 0x72, 0x77) { return parse_csr_write(A, t, 1) } // csrw csr, rs
1070 if s_eq4(A.src, off, n, 0x63, 0x73, 0x72, 0x73) { return parse_csr_write(A, t, 2) } // csrs csr, rs
1071 if s_eq4(A.src, off, n, 0x63, 0x73, 0x72, 0x63) { return parse_csr_write(A, t, 3) } // csrc csr, rs
1072 // B-type 4-letter mnemonics (Task #36). bltu=6, bgeu=7.
1073 if s_eq4(A.src, off, n, 0x62, 0x6C, 0x74, 0x75) { return parse_branch(A, t, 6) } // bltu
1074 if s_eq4(A.src, off, n, 0x62, 0x67, 0x65, 0x75) { return parse_branch(A, t, 7) } // bgeu
1075 }
1076 if n == 3 {
1077 // Fall-back 3-char mnemonics that start with 'l': lb, lh, lw, sh, sw
1078 // (duplicate table so length-3 loads land here after early matches).
1079 }
1080 if n == 5 {
1081 if s_eq5(A.src, off, n, 0x65, 0x63, 0x61, 0x6C, 0x6C) { return parse_ecall(A, t) } // ecall
1082 // CSR full forms (3-operand: rd, csr, rs).
1083 if s_eq5(A.src, off, n, 0x63, 0x73, 0x72, 0x72, 0x77) { return parse_csr_full(A, t, 1) } // csrrw
1084 if s_eq5(A.src, off, n, 0x63, 0x73, 0x72, 0x72, 0x73) { return parse_csr_full(A, t, 2) } // csrrs
1085 if s_eq5(A.src, off, n, 0x63, 0x73, 0x72, 0x72, 0x63) { return parse_csr_full(A, t, 3) } // csrrc
1086 }
1087 // System instructions (no operands): wfi, mret, sret, ebreak.
1088 if n == 3 {
1089 if s_eq3(A.src, off, n, 0x77, 0x66, 0x69) { return enc_i(0x73, 0, 0, 0, 0x105) } // wfi
1090 }
1091 if n == 4 {
1092 if s_eq4(A.src, off, n, 0x6D, 0x72, 0x65, 0x74) { return enc_i(0x73, 0, 0, 0, 0x302) } // mret
1093 if s_eq4(A.src, off, n, 0x73, 0x72, 0x65, 0x74) { return enc_i(0x73, 0, 0, 0, 0x102) } // sret
1094 }
1095 if n == 6 {
1096 let ebreak_pat: *u8 = "ebreak" as *u8
1097 if s_eq6(A.src, off, n, ebreak_pat, 6) { return enc_i(0x73, 0, 0, 0, 1) } // ebreak
1098 }
1099 // Short load/store dispatch (length 2-3, final byte 'b'/'h'/'w'/'u').
1100 if n == 2 {
1101 if A.src[off] == 0x6C {
1102 if A.src[off+1] == 0x62 { return parse_mem(A, t, 0, 0) } // lb
1103 if A.src[off+1] == 0x68 { return parse_mem(A, t, 0, 1) } // lh
1104 if A.src[off+1] == 0x77 { return parse_mem(A, t, 0, 2) } // lw
1105 }
1106 }
1107 if n == 3 {
1108 if s_eq3(A.src, off, n, 0x6C, 0x62, 0x75) { return parse_mem(A, t, 0, 4) } // lbu
1109 if s_eq3(A.src, off, n, 0x6C, 0x68, 0x75) { return parse_mem(A, t, 0, 5) } // lhu
1110 }
1111 return -1
1112}
1113
1114// ===== main assembly loop ===========================================
1115//
1116// Walk tokens line-by-line. Each line is either:
1117// * IDENT ":" -> label definition
1118// * IDENT starting with "." -> directive
1119// * IDENT otherwise -> mnemonic
1120// After handling, we skip rest-of-line (any remaining tokens) and
1121// move to next line.
1122
1123// Forward-decl: alloc is defined after assemble_pass.
1124func alloc(size: i64) -> *u8;
1125
1126func assemble_pass(A: *Asm, t: *AsmTok) -> i64 {
1127 A.pos = 0
1128 A.cur_addr = 0
1129 A.out_pos = 0
1130 var err: i64 = 0
1131 while err == 0 {
1132 next_tok(A, t)
1133 if t.kind == 0 { return 0 }
1134 if t.kind != 1 {
1135 // Unexpected leading token -- skip to newline and retry.
1136 skip_to_newline(A)
1137 continue
1138 }
1139 let off: i64 = t.start
1140 let n: i64 = t.len
1141
1142 // Label? Scan forward in src (NOT via lex_one, whose
1143 // token-struct pointer-write path has proved fragile).
1144 // Skip whitespace/newlines from A.pos; if the next non-ws
1145 // char is ':', we have a label. We then advance past the
1146 // ':' too.
1147 var is_label: i64 = 0
1148 var scan: i64 = A.pos
1149 while scan < A.src_len {
1150 let sc: i64 = A.src[scan]
1151 if sc == 0x20 { scan = scan + 1; continue }
1152 if sc == 0x09 { scan = scan + 1; continue }
1153 if sc == 0x0D { scan = scan + 1; continue }
1154 if sc == 0x3A {
1155 is_label = 1
1156 scan = scan + 1
1157 }
1158 // Note: we do NOT skip '\n' -- a ':' on a NEW line is
1159 // not part of this identifier's label.
1160 break
1161 }
1162 if is_label == 1 {
1163 if A.pass == 1 {
1164 label_define(A, off, n)
1165 }
1166 A.pos = scan
1167 continue
1168 }
1169
1170 // Directive? First char is '.' (ASCII 0x2E).
1171 //
1172 // Minimal v1 handling: swallow rest of line, regardless of
1173 // which directive. .byte / .asciz / etc are not emitted by
1174 // riscv.nx for the self-host pipeline today; when we need
1175 // them for string literals later, add per-directive handlers
1176 // here. Flag-and-fallthrough avoids the `continue`-inside-
1177 // nested-if pattern that NishiLang v1 lowers incorrectly.
1178 var is_directive: i64 = 0
1179 if A.src[off] == 0x2E {
1180 is_directive = 1
1181 // Dispatch the directives we actually emit; ignore others.
1182 // `.word N[, N...]` -- 32-bit raw word (SIMD/SIMT pre-
1183 // encoded instructions; lets nx_riscv.nx ship RVV without
1184 // requiring nxasm to know every vector mnemonic).
1185 // `.byte N[, N...]` and `.asciz "..."` -- globals dump.
1186 // Anything else (.text, .globl, .section, .cfi_*, .type,
1187 // .size, .option) -- skip to newline.
1188 if n == 5 {
1189 if s_eq5(A.src, off, n, 0x2E, 0x77, 0x6F, 0x72, 0x64) {
1190 handle_word(A, t)
1191 continue
1192 }
1193 if s_eq5(A.src, off, n, 0x2E, 0x62, 0x79, 0x74, 0x65) {
1194 handle_byte(A, t)
1195 continue
1196 }
1197 if s_eq5(A.src, off, n, 0x2E, 0x73, 0x6B, 0x69, 0x70) {
1198 handle_skip(A, t) // .skip N
1199 continue
1200 }
1201 if s_eq5(A.src, off, n, 0x2E, 0x7A, 0x65, 0x72, 0x6F) {
1202 handle_skip(A, t) // .zero N (same semantics as .skip)
1203 continue
1204 }
1205 }
1206 if n == 6 {
1207 if s_eq6(A.src, off, n, ".asciz" as *u8, 6) {
1208 handle_asciz(A, t)
1209 continue
1210 }
1211 if s_eq6(A.src, off, n, ".space" as *u8, 6) {
1212 handle_skip(A, t) // .space N (alias of .skip)
1213 continue
1214 }
1215 }
1216 skip_to_newline(A)
1217 }
1218
1219 if is_directive == 0 {
1220 // Mnemonic. Dispatch, emit word, move on.
1221 let w: i64 = assemble_mnemonic(A, t, off, n)
1222 if w < 0 {
1223 // Diagnostic on failure: dump the offending token name +
1224 // pass + cur_addr so the caller can localize the bug.
1225 sys_write(2, "nxasm: bad mnemonic '" as *u8, 21)
1226 var di: i64 = 0
1227 while di < n {
1228 sys_write(2, (((A.src as i64) + off + di) as *u8), 1)
1229 di = di + 1
1230 }
1231 sys_write(2, "' pass=" as *u8, 7)
1232 let pbuf: *u8 = sys_mmap(8)
1233 pbuf[0] = 0x30 + A.pass
1234 sys_write(2, pbuf, 1)
1235 sys_write(2, "\n" as *u8, 1)
1236 err = 1
1237 }
1238 if w >= 0 {
1239 emit_u32(A, w)
1240 }
1241 }
1242 }
1243 return err
1244}
1245
1246// ===== top-level =====================================================
1247
1248func alloc(size: i64) -> *u8 {
1249 // Bug fix 2026-05-20: was hardcoded __syscall(222, ...) which is
1250 // RV64 SYS_MMAP only -- segfaulted on x86_64 (syscall 222 is
1251 // invalid there). Now uses SYS_MMAP from nx_syscalls.nx which is
1252 // @ifdef-switched per arch (x86_64=9, RV64=222). Closes Task #33
1253 // x86_64 nxld silent-no-op.
1254 let raw: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0)
1255 return raw as *u8
1256}
1257
1258// One-shot stderr report: where pass-1 stopped + how many labels
1259// it found. Helps localize early-bail bugs in the tokenize loop
1260// (e.g. a NUL byte in src making next_tok return EOF too early).
1261func nxasm_diag_pass1(A: *Asm, src_len: i64) -> i64 {
1262 sys_write(2, "nxasm: pass1 stopped at pos=" as *u8, 28)
1263 let pbuf: *u8 = sys_mmap(32)
1264 var pn: i64 = A.pos
1265 var pk: i64 = 0
1266 if pn == 0 { pbuf[pk] = 0x30; pk = 1 }
1267 while pn > 0 {
1268 pbuf[pk] = 0x30 + (pn - (pn / 10) * 10)
1269 pn = pn / 10
1270 pk = pk + 1
1271 }
1272 var pj: i64 = pk - 1
1273 while pj >= 0 {
1274 sys_write(2, (((pbuf as i64) + pj) as *u8), 1)
1275 pj = pj - 1
1276 }
1277 sys_write(2, "/" as *u8, 1)
1278 let sbuf: *u8 = sys_mmap(32)
1279 var sn: i64 = src_len
1280 var sk: i64 = 0
1281 if sn == 0 { sbuf[sk] = 0x30; sk = 1 }
1282 while sn > 0 {
1283 sbuf[sk] = 0x30 + (sn - (sn / 10) * 10)
1284 sn = sn / 10
1285 sk = sk + 1
1286 }
1287 var sj: i64 = sk - 1
1288 while sj >= 0 {
1289 sys_write(2, (((sbuf as i64) + sj) as *u8), 1)
1290 sj = sj - 1
1291 }
1292 sys_write(2, " n_labels=" as *u8, 10)
1293 let lbuf: *u8 = sys_mmap(32)
1294 var ln: i64 = A.n_labels
1295 var lk: i64 = 0
1296 if ln == 0 { lbuf[lk] = 0x30; lk = 1 }
1297 while ln > 0 {
1298 lbuf[lk] = 0x30 + (ln - (ln / 10) * 10)
1299 ln = ln / 10
1300 lk = lk + 1
1301 }
1302 var lj: i64 = lk - 1
1303 while lj >= 0 {
1304 sys_write(2, (((lbuf as i64) + lj) as *u8), 1)
1305 lj = lj - 1
1306 }
1307 sys_write(2, "\n" as *u8, 1)
1308 return 0
1309}
1310
1311// Assemble `src_len` bytes starting at `src` into `out_buf`, returning
1312// the number of code bytes emitted (out_buf length), or -1 on error.
1313func assemble(src: *u8, src_len: i64, out_buf: *u8, out_cap: i64) -> i64 {
1314 // 32 bytes per Label * LABELS_CAP slots. Real-world nxc.s today
1315 // has ~5K labels; 8K gives 60% headroom. Bounds-checked in
1316 // label_define so an overflow now prints + returns -1 instead of
1317 // writing past the pool.
1318 let labels_raw: *u8 = alloc(32 * LABELS_CAP)
1319 let labels: *Label = labels_raw as *Label
1320
1321 let actx_raw: *u8 = alloc(80)
1322 let A: *Asm = actx_raw as *Asm
1323 A.src = src
1324 A.src_len = src_len
1325 A.pos = 0
1326 A.cur_addr = 0
1327 A.pass = 1
1328 A.out = out_buf
1329 A.out_pos = 0
1330 A.labels = labels
1331 A.n_labels = 0
1332
1333 let t_raw: *u8 = alloc(64)
1334 let t: *AsmTok = t_raw as *AsmTok
1335
1336 // Pass 1: discover labels + compute sizes.
1337 let e1: i64 = assemble_pass(A, t)
1338 // Diagnostic: report how far pass-1 walked vs total. Lets us see
1339 // whether the loop bailed (pos < src_len) or a label-table cap
1340 // was hit (n_labels at LABELS_CAP).
1341 nxasm_diag_pass1(A, src_len)
1342 if e1 != 0 { return -2 }
1343
1344 // Pass 2: emit bytes.
1345 A.pass = 2
1346 A.pos = 0
1347 A.cur_addr = 0
1348 A.out_pos = 0
1349 let e2: i64 = assemble_pass(A, t)
1350 if e2 != 0 { return -3 }
1351
1352 return A.out_pos
1353}
1354
1355
1356// Library only; self-test lives in nxasm_v2_test.nx.