code wiki / (root) / nx_types.nx

nx_types.nx source

↩ module page · 639 lines · 36017 B

1// types.nx -- shared struct definitions for the NishiLang self-host. 2// 3// Each port file (ir.nx / opt.nx / parse.nx / regalloc.nx / riscv.nx) 4// used to duplicate these declarations verbatim. They now `import 5// "types.nx"` instead so a layout tweak only has to happen in one 6// place; this prevents drift between files. 7// STUB(types, never): "drift" wording above is descriptive (not a 8// landmine); keeps audit_stubs.sh quiet on the design intent. 9// 10// Content: IR shapes (Type, Value, Instr, BasicBlock, Function, 11// Module) plus the regalloc/riscv-shared ValueLoc. Keep this file 12// *declarations only* -- no functions, no main -- so files that 13// include it still compile as independent benches with their own 14// self-test `main`s. 15// 16// Pool record sizes (used by callers doing pointer arithmetic): 17// Value : 48 bytes 18// Instr : 128 bytes (12 i64 operand/link slots + 4 ptrs) 19// BasicBlock : 96 bytes 20// ValueLoc : 16 bytes 21// If any struct below grows, grep callers for `* 48`, `* 96`, 22// `* 128`, `* 16` and update in lockstep. 23 24// ---- Types --------------------------------------------------------- 25 26struct Type { 27 kind: i64, // TypeKind: 0 void, 1 bool, 2 i8, 3 i16, 28 // 4 i32, 5 i64, 6 ptr, 7 struct, 29 // 8 param (generic type variable) 30 size: i64, // bytes 31 align: i64, 32 pointee: *Type, // for PTR + ARRAY 33 34 // TY_STRUCT detail. `name_bytes` points at the declared name 35 // (not owned by this struct -- token-table-relative). `fields` 36 // is a heap-allocated array of StructField records; n_fields is 37 // the count, and the struct's `size` is the sum of field sizes 38 // (no padding yet -- every field is currently 8 bytes). 39 name_bytes: *u8, 40 name_len: i64, 41 fields: *StructField, 42 n_fields: i64, 43 44 // TY_STRUCT templates: generic type parameters declared via 45 // `struct Name<T, E> { ... }`. Stored as an inline-length 46 // array of *u8 name pointers. Zero for non-generic structs. 47 // Monomorphization at instantiation time (instantiate_generic_n 48 // in parse.nx) consults this list to substitute TY_PARAM fields 49 // against concrete argument types. 50 n_type_params: i64, 51 type_params: *i64, // array of *u8 name ptrs (opaque i64 each) 52 53 // TY_PARAM detail: the declared parameter name this type 54 // represents. Used by clone_type_substituting to match against 55 // the template's type_params list during instantiation. 56 param_name: *u8, 57 param_name_len: i64, 58 59 // SIGNEDNESS of subword loads (2026-07-10 debt fix): the IR has no unsigned kinds -- `u8` and `i8` 60 // both mint TY_I8 -- so backends could not distinguish them and DIVERGED: x86 zero-extended every 61 // subword load (wrong for *i8/*i16/*i32 -- witness: SIMD hsum ~4e9 garbage on negative lanes), 62 // RV64 sign-extended every TY_I* load (wrong for *u8 -- would corrupt the x509 0xA0 witness). 63 // `sext`=1 marks a type minted from a SIGNED annotation (i8/i16/i32 via alloc_type_s) -> subword 64 // loads sign-extend; sext=0 (u8/u16/u32, untyped) -> zero-extend. 8-byte loads don't extend. 65 sext: i64, 66} 67 68// One field inside a TY_STRUCT. Layout is sequential by declaration 69// order; offsets assigned at struct construction time (ir_type_struct 70// _add_field). 71// STUB(types, T#types-001): every field is treated as 8-byte i64 72// /pointer. Means i32, i8, struct-by-value, packed fields silently 73// compute wrong offsets. Pairs with the parse_stmt_let TY_I64 74// hardcode and the *u8 stride bug in parse_field_chain. 75// Plan: per-field byte-size + alignment; codegen picks load/store 76// width from field.size. 77// Closes when: any program with non-i64 fields needs to round-trip 78// correctly (e.g. probe.nx writing 'o','k','\n' to a *u8 buffer). 79struct StructField { 80 name_bytes: *u8, 81 name_len: i64, 82 ty: *Type, 83 offset: i64, // bytes from struct start 84} 85 86// ---- SSA value ----------------------------------------------------- 87 88struct Value { 89 id: i64, 90 kind: i64, // ValueKind: 0 CONST_INT, 1 PARAM, 2 INSTR 91 ty: *Type, 92 const_int: i64, // for CONST_INT 93 param_index: i64, // for PARAM 94 instr: *Instr, // for INSTR 95} 96 97// ---- SSA instruction ---------------------------------------------- 98// 99// Doubly-linked inside its BasicBlock. Inline operand slots op0..3 100// cover every opcode we emit today. 101 102struct Instr { 103 op: i64, // Opcode 104 result: i64, // ValueId (0 for void instructions) 105 ty: *Type, 106 n_operands: i64, 107 op0: i64, 108 op1: i64, 109 op2: i64, 110 op3: i64, 111 op4: i64, // Extra slots 4-7 cover OP_SYSCALL (ECALL) 112 op5: i64, // which takes up to 7 operands: syscall 113 op6: i64, // number + 6 args (Linux RV64 ABI). 114 op7: i64, 115 op8: i64, // Slots 8-15 added 2026-05-20 (Task #93) 116 op9: i64, // for OP_CALL arity > 8. Substrate has 117 op10: i64, // 11-arg AEAD calls (nx_tls13_record_*_v2) 118 op11: i64, // and other complex orchestrators. 119 op12: i64, // Stride moves 128 -> 192; alloc_instr + 120 op13: i64, // instrs-pool sizing updated in nx_ir.nx. 121 op14: i64, 122 op15: i64, 123 op16: i64, // Slots 16-23 added 2026-06-18 for OP_CALL 124 op17: i64, // arity > 16 (bi-predicted H.264 chroma recon 125 op18: i64, // needs ~18 args). Stride moves 192 -> 256; 126 op19: i64, // alloc_instr + instrs-pool sizing + all 127 op20: i64, // operand getters/setters/escape-scans updated 128 op21: i64, // in lockstep (nx_ir, nx_opt, nx_x86_regalloc, 129 op22: i64, // nx_parse). Same pattern as the 8->16 bump. 130 op23: i64, 131 callee: *Function, 132 parent: *BasicBlock, 133 prev: *Instr, 134 next: *Instr, 135} 136 137// ---- basic block -------------------------------------------------- 138 139struct BasicBlock { 140 id: i64, 141 head: *Instr, 142 tail: *Instr, 143 parent: *Function, 144 n_preds: i64, 145 n_succs: i64, 146 // Inline fixed pred/succ slots for small CFGs. 147 pred0: *BasicBlock, 148 pred1: *BasicBlock, 149 pred2: *BasicBlock, 150 succ0: *BasicBlock, 151 succ1: *BasicBlock, 152} 153 154// ---- function ----------------------------------------------------- 155// 156// All pools (values / blocks / instrs) live in one backing arena. 157// Function holds base pointers + counts + caps. 158 159struct Function { 160 name_start: i64, // byte offset in module's name table 161 name_len: i64, 162 ret_ty: *Type, 163 n_params: i64, 164 165 values: *Value, 166 n_values: i64, 167 values_cap: i64, 168 169 blocks: *BasicBlock, 170 n_blocks: i64, 171 blocks_cap: i64, 172 173 instrs: *Instr, 174 n_instrs: i64, 175 instrs_cap: i64, 176 177 entry: *BasicBlock, 178 179 // ARGUMENT TYPE CHECKING (2026-08-01). Bit i = "parameter i is a POINTER". 180 // Bit 63 = "this mask is KNOWN" -- a forward stub whose parameter list has not been 181 // parsed yet leaves the whole field 0, and the call site skips checking rather than 182 // inventing a signature (the same trap the arity check hit before 183 // prepass_count_params existed). 184 // 185 // WHY A BITMASK AND NOT AN ARRAY: ir_function_new hardcodes `f.values = (base + 128)`, 186 // so this header MUST fit in 128 bytes. The 14 fields above are already 112, leaving 187 // room for EXACTLY ONE more i64. A second field would start at offset 120+8 = 128 and 188 // silently overlap the values array. Parameters are capped at 8, so 8 bits is ample. 189 // ⚠If you ever need another Function field, raise the 128 in ir_function_new FIRST. 190 param_ptr_mask: i64, 191} 192 193// ---- module ------------------------------------------------------- 194 195// Bytes per Function slot in a Module's function pool. Read by BOTH allocators (parse_module in 196// nx_parse.nx, ir_module_new in nx_ir.nx) AND by the appender (ir_new_function), because a stride 197// written three times is three strides and only the one nobody updates decides where the write lands. 198const NX_MODULE_FN_STRIDE: i64 = 176 199// Bytes per Global slot, same reasoning: it is used to SIZE the globals pool and again to OFFSET past 200// the function pool to reach it, so the two arithmetic sites must read one number. 201const NX_MODULE_GLOBAL_STRIDE: i64 = 80 202 203struct Module { 204 name: *u8, 205 functions: *Function, 206 n_functions: i64, 207 // Capacity of `functions`, in slots. SET BY EVERY ALLOCATOR AND CHECKED BY ir_new_function. 208 // It was declared here and left unwired: ir_module_new populated it, parse_module did not, and 209 // nothing read it -- a guard three-quarters built, which is indistinguishable from no guard. 210 fn_cap: i64, 211 212 // Module-level data: string literals + `static NAME: T` storage. 213 // Backends walk this list after emitting functions, dropping each 214 // global into .rodata (strings, read-only data) or .bss (zero-init 215 // writable) based on flags on the Global record. 216 globals: *Global, 217 n_globals: i64, 218 globals_cap: i64, 219} 220 221// One module-level data item. `is_string=1` means the bytes should 222// be printed as `.asciz`; else raw `.byte` listing. `zero_init=1` 223// overrides both -- emit `.zero len` in `.bss` and skip the bytes. 224// `writable=1` chooses `.data` section for non-zero-init globals; 225// default (both flags 0) is `.rodata`. 226struct Global { 227 id: i64, 228 name_bytes: *u8, // stable label; null for anonymous literals 229 name_len: i64, 230 bytes: *u8, // payload; null when zero_init 231 len: i64, 232 is_string: i64, 233 zero_init: i64, 234 writable: i64, 235} 236 237// ---- regalloc/riscv shared result --------------------------------- 238 239struct ValueLoc { 240 kind: i64, // 0 = REGISTER, 1 = SPILLED 241 idx: i64, // register index OR sp-relative byte offset 242} 243 244// ---- named integer constants -------------------------------------- 245// 246// Port files used to compare `inst.op == 1` and `v.kind == 0`, 247// forcing every reader to consult a comment table. The constants 248// below name every integer the ports actually dispatch on. They 249// exist as `const` (not `enum`) deliberately -- these values are 250// used as plain i64 in arithmetic, switches, and array indices. 251// Tagged enums with payloads are reserved for sum types (Option, 252// Result). 253// 254// Invariant: opcode numbers match ir.h's Opcode enum and C-side 255// emission; changing one side requires changing both. 256 257// Opcode: Opcode enum in ir.h --------------------------------------- 258 259const OP_ADD: i64 = 1 260const OP_SUB: i64 = 2 261const OP_MUL: i64 = 3 262const OP_DIV_S: i64 = 4 263const OP_DIV_U: i64 = 5 264const OP_REM_S: i64 = 6 265const OP_REM_U: i64 = 7 266const OP_NEG: i64 = 9 267const OP_AND: i64 = 10 268const OP_OR: i64 = 11 269const OP_XOR: i64 = 12 270const OP_SHL: i64 = 13 271const OP_SHR_S: i64 = 14 272const OP_SHR_U: i64 = 15 273const OP_NOT: i64 = 16 274// Bit rotates (x86 ROLQ/RORQ; rv64 rol/ror Zbb). Emitted from 275// nx_parse.nx when `__rotl64(v,n)` / `__rotr64(v,n)` is seen -- C 276// bootstrap parity (parse.c OP_ROTL64/OP_ROTR64). Two operands: 277// op0 = value, op1 = count (low 6 bits used by the hardware). 278// MUST be a real op, not shift+or: the native parser previously 279// lacked these, so `__rotr64(x,n)` fell through to an unresolved 280// call that silently returned op0 (a no-op rotate) -- this broke 281// SHA-512/Ed25519/TLS under native codegen (SITES-LIVE 2026-05-27). 282const OP_ROTL64: i64 = 27 283const OP_ROTR64: i64 = 28 284// Address-of an alloca-backed local: `&x`. Unop; op0 = the alloca 285// value-id. Lowered via the AS-ADDRESS path (leaq slot(%rbp)) NOT the 286// auto-load (movq) path -- bare `&x` previously returned the raw alloca 287// value-id which the codegen auto-loaded, yielding the slot CONTENTS as 288// a bogus pointer -> SEGV (latent in nx_jose's `&off`). Result type is 289// a pointer to the local's type. 290const OP_ADDR_OF: i64 = 29 291// Scalar bit unops -- C bootstrap parity (ir.h OP_BSWAP64/CLZ32/CTZ32/ 292// POPCNT64). Single-operand; op0 = value. Same silent-no-op hazard 293// as the rotates if left unported (parse_primary_call return-0). 294// BSWAP64 -> bswapq (i486 1989+, universal) 295// POPCNT64 -> popcntq (SSE4.2, 2008+) 296// CLZ32 -> lzcntl (32 if 0) (BMI1, 2013+) 297// CTZ32 -> tzcntl (32 if 0) (BMI1, 2013+) 298const OP_BSWAP64: i64 = 35 299const OP_CLZ32: i64 = 36 300const OP_CTZ32: i64 = 37 301const OP_POPCNT64: i64 = 38 302// Atomic intrinsics -- C bootstrap parity (ir.h OP_ATOMIC_*). Memory 303// order is the LAST operand (NX_MO_* const 0..5). Native x86 backend 304// emits the CONSERVATIVE-STRONG lowering for every order (always 305// correct on x86 TSO; never weaker than requested): 306// LOAD -> movq (acquire-strong) STORE -> xchgq (seq-cst) 307// CAS -> lock cmpxchgq + sete FAA -> lock xaddq 308// FENCE -> mfence 309const OP_ATOMIC_LOAD_I64: i64 = 39 310const OP_ATOMIC_STORE_I64: i64 = 46 311const OP_ATOMIC_CAS_I64: i64 = 47 312const OP_ATOMIC_FAA_I64: i64 = 48 313const OP_ATOMIC_FENCE: i64 = 49 314// __thread_clone(stack_top, entry_fn, ctx) -> child_tid (C bootstrap 315// parity, ir.h OP_THREAD_CLONE). Lowers to SYS_clone(56) + child 316// trampoline. 3 operands. 317const OP_THREAD_CLONE: i64 = 64 318const OP_EQ: i64 = 20 319const OP_NE: i64 = 21 320const OP_LT_S: i64 = 22 321const OP_LE_S: i64 = 23 322const OP_GT_S: i64 = 24 323const OP_GE_S: i64 = 25 324const OP_RETURN: i64 = 30 325const OP_BR: i64 = 31 326const OP_BR_COND: i64 = 32 327const OP_CALL: i64 = 33 328const OP_TAIL_CALL: i64 = 34 329// Indirect call `fp(args)` through a func-pointer VALUE: op0 = fn-ptr, op1.. = args, n_operands = n_args+1 330// (so args cap at 23). Distinct opcode from OP_CALL (whose callee is a static *Function) so inlining/tail-call 331// correctly ignore it while purity/load-hoist/rax-track/inline-reject barriers treat it as a call. Value 144 332// = next free after OP_MUL256_WIDE=143 (the 34..143 band is taken). 333const OP_CALL_INDIRECT: i64 = 144 334const OP_COPY: i64 = 40 335const OP_LOAD: i64 = 41 336const OP_STORE: i64 = 42 337const OP_ALLOCA: i64 = 43 338const OP_GEP: i64 = 44 339const OP_SYSCALL: i64 = 45 // ECALL: op0 = syscall number, op1..op6 = args 340 341// Floating-point arithmetic (RV64F + RV64D extensions). Same 342// semantics as integer counterparts but operates on f-registers 343// + IEEE 754 binary32/binary64 values. Codegen lowering in 344// runtime/riscv.nx is pending separate commits; opcodes reserved 345// here so the IR can carry the intent. 346const OP_FADD: i64 = 50 347const OP_FSUB: i64 = 51 348const OP_FMUL: i64 = 52 349const OP_FDIV: i64 = 53 350const OP_FNEG: i64 = 54 351const OP_FCAST_I_TO_F: i64 = 55 // int -> float 352const OP_FCAST_F_TO_I: i64 = 56 // float -> int (truncate) 353const OP_F32X4_DOT: i64 = 134 // packed f32x4 dot: (a:*f32[4]) . (b:*f32[4]) -> f32 scalar (x86 SSE movups+mulps+hsum) -- the compute-physics lever 354const OP_F32X8_DOT: i64 = 135 // packed f32x8 dot: (a:*f32[8]) . (b:*f32[8]) -> f32 scalar (x86 AVX2 vmovups+vmulps 8-wide + vextractf128 + hsum) -- 2x the SSE lever 355const OP_F32X8_FMA: i64 = 136 // (acc:*f32[8]) += (a:*f32[8]) * (b:*f32[8]) FUSED (vfmadd231ps) -- vector accumulate, NO per-chunk hsum 356const OP_F32X8_HSUM: i64 = 137 // (acc:*f32[8]) -> f32 horizontal sum (vextractf128 + SSE hsum) -- called ONCE per dot 357const OP_I16X16_MADD: i64 = 138 // (acc:*i32[8]) += vpmaddwd((a:*i16[16]),(b:*i16[16])) -- NO-FLOAT integer dot, EXACT + deterministic 358const OP_I8DOT32: i64 = 145 // __f32_i8dot32(a:*i8[32], b:*f32[32]) -> f32: dot of 32 sign-extended int8 with 32 f32 (SSE pmovsxbd+cvtdq2ps+mulps+addps, unrolled x8, hsum once). The Q8_0/quantized dequant-dot lever (2026-07-08). 359const OP_Q5UNPACK32: i64 = 146 // __q5_unpack32(qhqs:*u8[20], out:*i8[32], consts:*u8[80]) -> 0: SSE unpack of a Q5_0 block (qh[4]+qs[16]) -> 32 signed int8 (nibble|(qh_bit<<4))-16, in A-order. pand/psrlw nibble + pshufb/pcmpeqb qh-bit-spread. Then __f32_i8dot32(out,A)*d = Q5_0 dequant-dot. The 79%-weight lever (2026-07-08). 360const OP_Q4KUNPACK32S: i64 = 151 // __q4k_unpack32s(qs:*u8[32], out:*i16[64], scpack:i64) -> 0: AVX2 unpack of one Q4_K sub-block pair (32 packed nibble bytes) into 64 i16 lanes ALREADY SCALED -- out[0..31] = (qs[k] & 15) * (scpack & 0xFFFF), out[32..63] = (qs[k] >> 4) * ((scpack >> 16) & 0xFFFF), lane k = byte k. vpmovzxbw widen + vpsrlw/vpand nibble split + vpmullw by a vpbroadcastw scale; no memory constants (the 0x000F mask is built from vpcmpeqd+vpsrlw). Replaces the scalar u64 spread+multiply the LM4 resident-Q4_K decode paid per row per token (2026-09-02). 361const OP_Q4KSBDOT: i64 = 152 // __q4k_sb_dot(sb:*u8[144], col:*i16[256], scpre:*i64[8], out:*i64[5]) -> 0: ONE Q4_K super-block, whole: the 12 scale bytes decoded in registers, every sub-block pair unpacked+scaled (the __q4k_unpack32s sequence) and vpmaddwd-accumulated straight from the activation lanes in memory, out[0..3] = the 8 i32 accumulator lanes (sum over the block of sc_sub*q*col), out[4] = sum over sub-blocks of m_sub*scpre[sub] (the dmin term). 4 operands. Built because the serve profile showed the scalar code AROUND four per-group intrinsic calls cost more than the arithmetic inside them (2026-09-02, LM4c third cut). 362const OP_Q8BLKDOT: i64 = 154 // __q8blk_i16dot(codes:*i8[32], x:*i16[32]) -> i64: one Q8_0 block's int8 x i16 dot (search R0s-b), the codes sign-extended IN REGISTER (vpmovsxbw x2 + vpmaddwd x2 + vpaddd + the widened int64 hsum). EXACT + deterministic: a lane holds four products of at most 127*32767. 363const OP_I16DOT: i64 = 153 // __i16_dot(a:*i16[n], b:*i16[n], n:i64) -> i64: sum of a[i]*b[i] over n lanes (n a POSITIVE multiple of 16, the caller's contract), 364 // the int32x8 accumulator register-resident for the whole call and summed once (R0r-b 2026-09-17: the per-madd memory round trip 365 // of OP_I16X16_MADD bounded the batched prefill at 450 us per row). EXACT + deterministic; the caller bounds n so no int32 lane 366 // overflows (nx_nofloat_llm NF_CHUNK_K). x86-64 only, like every SIMD builtin here. 367const OP_I8DOT32A: i64 = 147 // __f32_i8dot32a(a:*i8[32], b:*f32[32]) -> f32: AVX2 256-bit twin of OP_I8DOT32 -- 4 blocks of 8 lanes (vpmovsxbd+vcvtdq2ps+vmulps), TWO accumulators (ymm4/ymm5) to break the serial vaddps chain, combine + one hsum. ~2x the SSE dot. NOT bit-identical to OP_I8DOT32 (different summation order); argmax-robust. The AVX2 dequant-dot lever (2026-07-10, after gcc proved 6x codegen headroom). 368const OP_I8FMA32: i64 = 148 // __f32_i8fma32(a:*i8[32], b:*f32[32], d_bits:i64, acc:*f32[8]) -> 0: DEFERRED-HSUM block: acc[8] += d * (sext(a)·b), 8-lane AVX2 vfmadd231ps, NO hsum. The caller keeps a persistent 8-lane acc across all k/32 blocks (broadcasting the per-block scale d) and hsums ONCE per output (__f32x8_hsum) -- kills 27/28 per-block hsums = the cold-forward matmul lever (2026-07-10). 4 operands. 369const OP_Q8ROWDOT: i64 = 149 // __f32_q8row_dot(qbuf_row:*u8, a_row:*f32, nblocks:i64) -> f32: MONOLITHIC Q8_0 row dot. Loops all nblocks 34-byte blocks with a REGISTER-resident 8-lane ymm6 accumulator (NO memory round-trip, NO per-block hsum), F16C vcvtph2ps for each block's f16 scale d, vfmadd231ps d*(int8.a), ONE hsum at the end. The real cold-forward matmul lever (2026-07-10) -- kills BOTH the 28 per-block hsums AND the deferred-hsum memory round-trip. 3 operands; internal loop (unique label = fn+rid). 370const OP_CRC32: i64 = 139 // __crc32_u64(crc,data): SSE4.2 CRC-32C accumulate (x86 crc32q). Pure; 2 i64 operands, i64 result. 371const OP_PDEP: i64 = 140 // __pdep64(src,mask): BMI2 parallel bit DEPOSIT (x86 pdep). Pure; 2 i64 operands, i64 result. 372const OP_PEXT: i64 = 141 // __pext64(src,mask): BMI2 parallel bit EXTRACT (x86 pext). Pure; 2 i64 operands, i64 result. 373const OP_SHA256_NI_BLOCK: i64 = 142 // hardware SHA-NI: one full SHA-256 block compression IN PLACE. 374 // op0=state ptr (*u32[8], a..h, in/out), op1=block ptr (*u8[64] raw 375 // big-endian msg), op2=K ptr (*u32[64] round constants). 3 operands, 376 // i64 result (0). Lowers to punpck/pshufd state arrange + pshufb 377 // byteswap + 16x (sha256msg1/msg2 + 2x sha256rnds2). Software 378 // sha256_compress remains the oracle/fallback (CPUID-gated caller). 379const OP_MUL256_WIDE: i64 = 143 // __mul256_wide(dst,a,b): fused 4x64-limb schoolbook multiply of the 380 // 256-bit little-endian integers *a,*b -> the 512-bit product in *dst 381 // (8 x u64). op0=dst ptr (*u64[8], out), op1=a ptr (*u64[4]), op2=b ptr 382 // (*u64[4]). 3 operands, i64 result (0). Lowers to the ADX/BMI2 dual- 383 // carry kernel (mulx + adcx[CF chain] + adox[OF chain]). The pure 8x32 384 // u256_mul_wide stays the byte-exact oracle (difftest-gated). 385const OP_FCAST_F32_TO_F64: i64 = 57 386const OP_FCAST_F64_TO_F32: i64 = 58 387const OP_FSQRT: i64 = 150 // scalar float sqrt (sqrtsd/sqrtss); unary, result type = operand float type 388const OP_AES128_ENC_BLOCK: i64 = 59 // hardware AES-NI: encrypt 16B block in place (op0=state ptr, op1=roundkeys ptr) 389// hardware CLMUL (PCLMULQDQ): carry-less multiply two selected 64-bit halves of *op0 and *op1, 390// 128-bit result written back to *op0 IN PLACE. The half-select is baked into the op (imm8): 391// LL = op0.lo * op1.lo (imm 0x00) HH = op0.hi * op1.hi (imm 0x11) 392// LH = op0.lo * op1.hi (imm 0x10) HL = op0.hi * op1.lo (imm 0x01) 393// These four are the building blocks of a full 128x128 GF(2) multiply (GHASH core). 394const OP_CLMUL_LL: i64 = 65 395const OP_CLMUL_HH: i64 = 66 396const OP_CLMUL_LH: i64 = 67 397const OP_CLMUL_HL: i64 = 68 398const OP_FEQ: i64 = 60 399const OP_FNE: i64 = 61 400const OP_FLT: i64 = 62 401const OP_FLE: i64 = 63 402 403// Kernel / bare-metal intrinsics. Emitted from parse.nx when the 404// corresponding `__wfi()`, `__csrr(csr)`, `__csrw(csr, v)`, `__fence()` 405// identifier is seen; lowered by riscv.nx to the literal RV64 406// privileged instruction. Each has zero or two operands: 407// OP_WFI — no operands 408// OP_CSR_READ — op0 = csr number (must be constant) 409// OP_CSR_WRITE — op0 = csr number (const), op1 = value 410// OP_FENCE — no operands (full memory fence) 411// OP_MRET — no operands (M-mode return; ends trap handler) 412// 413// NOTE: numbering moved out of 46-50 because OP_FADD/FSUB/FMUL/FDIV 414// live at 50-53. Prior to this renumber OP_MRET (=50) collided with 415// OP_FADD (=50) -- a silent miscompile trap if a function ever mixed 416// trap-return with float arithmetic. Kernel intrinsics now 70-74; 417// the full opcode table layout is: 418// 1-15 integer arith + bitwise 419// 16-29 unary + compare 420// 30-44 control flow + memory 421// 45 ECALL 422// 46-49 reserved (formerly kernel intrinsics; do not reuse until 423// we are sure no old .nx source is still mid-build) 424// 50-63 float (FADD..FLE) 425// 70-74 kernel intrinsics (WFI / CSR / FENCE / MRET) 426// RVV vector extension opcodes (RV64V, ratified 2021). 427// Scoped set for v0.0.1: element-wise vector arith + load/store. 428// Each op takes full-vector operands; element type + vector length 429// live on the Value's type (TY_V<T>). Codegen in riscv.nx emits 430// vsetvli + the v-form instruction; regalloc draws from a new v-reg 431// pool (v0..v31, pool indices 300..331). 432// 433// Why 80-99: keeps RVV cleanly separate from the scalar FP band 434// (50-63) and the kernel intrinsics (70-74) so the dispatch 435// switches stay clear. 436const OP_VADD: i64 = 80 // element-wise add 437const OP_VSUB: i64 = 81 438const OP_VMUL: i64 = 82 439const OP_VDIV: i64 = 83 // signed int / float 440const OP_VFADD: i64 = 84 // fp variant (vfadd.vv) 441const OP_VFSUB: i64 = 85 442const OP_VFMUL: i64 = 86 443const OP_VFDIV: i64 = 87 444const OP_VLE: i64 = 88 // vector load element (vle32.v / vle64.v) 445const OP_VSE: i64 = 89 // vector store element 446const OP_VSETVLI: i64 = 90 // explicit vl/vtype setup 447const OP_VMV_S_X: i64 = 91 // scalar broadcast into vector 448const OP_VREDSUM: i64 = 92 // reduction sum (for softmax denom, norm) 449// Width-specific SIMD ops (port from nxc2/ir.h OP_SIMD_*). These 450// carry the lane-width in their identity, unlike the scalar-tagged 451// OP_VADD which leans on Value.type. Sovereign parse surface lands 452// in nx_parse.nx; codegen in nx_riscv.nx. Bits-up roll-out: start 453// with vdot (the widening ML kernel), then min/max, sat, shifts. 454const OP_SIMD_VDOT_I16_X16: i64 = 93 // (a:i16x16) . (b:i16x16) -> i64 scalar 455// i16x16 per-lane min/max + horizontal reductions. Each takes 456// *i64 pointers to packed-i16 source, lowers to vmin.vv / vmax.vv 457// (per-lane) or vredmin.vs / vredmax.vs (horizontal scalar). 458// Sign-extended scalar return via vmv.x.s + slli/srai. 459const OP_SIMD_VREDUCE_MIN_I16_X16: i64 = 94 // (v:*i64) -> i64 sign-ext min 460const OP_SIMD_VREDUCE_MAX_I16_X16: i64 = 95 // (v:*i64) -> i64 sign-ext max 461// Saturating signed add. Per-lane sat(a+b) clipped to INT16_MIN..MAX. 462// Returns a packed-i16 result via *i64 output pointer (caller alloc). 463const OP_SIMD_VSADD_I16_X16: i64 = 96 // (a:*i64, b:*i64, out:*i64) -> void 464// Per-lane signed saturating sub. Same shape as vsadd. 465const OP_SIMD_VSSUB_I16_X16: i64 = 97 466// Per-lane unsigned saturating add/sub. Required for image RGBA 467// channel clamps where signed sat would wrap at 32767. 468const OP_SIMD_VSADDU_I16_X16: i64 = 98 469const OP_SIMD_VSSUBU_I16_X16: i64 = 99 470// Per-lane min/max + basic arith for i16x16. Each takes 471// (a:*i64, b:*i64, out:*i64) -> void via in-place store. 472const OP_SIMD_VMIN_LANE_I16_X16: i64 = 100 473const OP_SIMD_VMAX_LANE_I16_X16: i64 = 101 474const OP_SIMD_VADD_LANE_I16_X16: i64 = 102 475const OP_SIMD_VSUB_LANE_I16_X16: i64 = 103 476const OP_SIMD_VMUL_LANE_I16_X16: i64 = 104 477// Per-lane immediate-count shifts for i16x16. Shape: 478// (a:*i64, count:i64, out:*i64) -> void 479// vsra is arithmetic (sign-extend top bits), vsrl logical 480// (zero-fill). Distinction preserved at SIMD layer per the 481// nxasm srai-aliased-to-srli bug class. 482const OP_SIMD_VSLL_I16_X16: i64 = 105 483const OP_SIMD_VSRL_I16_X16: i64 = 106 484const OP_SIMD_VSRA_I16_X16: i64 = 107 485// Horizontal sum of 16 i16 lanes -> i64 scalar (widening reduce). 486// (v:*i64) -> i64 487// Uses vwredsum.vs to widen i16 -> i32 before accumulation; the 488// final scalar fits any 16-lane i16 sum (max |sum| = 16 * 32767 = 489// ~524k, well within i32 range). Sign-extended on extraction. 490const OP_SIMD_VREDUCE_SUM_I16_X16: i64 = 108 491// Broadcast scalar i64 into all 16 i16 lanes, store to *out. 492// (scalar:i64, out:*i64) -> void 493const OP_SIMD_VBROADCAST_I16_X16: i64 = 109 494// ---- i8x32 SIMD ops (32 lanes of 8-bit per 256-bit vector) ---- 495// Shape (a:*i64, b:*i64, out:*i64) -> void for lane-wise binops. 496// Useful for AES round bytes, INT8 quantized ML, byte-string ops. 497// All take *i64 pointers but interpret as packed 32-byte arrays. 498const OP_SIMD_VADD_I8_X32: i64 = 110 499const OP_SIMD_VSUB_I8_X32: i64 = 111 500const OP_SIMD_VSADD_I8_X32: i64 = 112 // signed saturating 501const OP_SIMD_VSSUB_I8_X32: i64 = 113 // signed saturating 502// Horizontal sum of 32 i8 lanes -> i64 (widening to i16 accumulator, 503// max |sum| = 32 * 127 = 4064, fits in i16). 504// (v:*i64) -> i64 505const OP_SIMD_VREDUCE_SUM_I8_X32: i64 = 114 506// Broadcast scalar i64 (low 8 bits) into all 32 i8 lanes, store *out. 507// (scalar:i64, out:*i64) -> void 508const OP_SIMD_VBROADCAST_I8_X32: i64 = 115 509// ---- i32x8 SIMD ops (8 lanes of 32-bit per 256-bit vector) ---- 510// Useful for ML token IDs, image pixel ops, hash indexes, INT32 511// dot products. Shape (a:*i64, b:*i64, out:*i64) -> void for 512// lane-wise binops; reduce/broadcast match their i8/i16 cousins. 513const OP_SIMD_VADD_I32_X8: i64 = 116 514const OP_SIMD_VSUB_I32_X8: i64 = 117 515const OP_SIMD_VMUL_I32_X8: i64 = 118 516const OP_SIMD_VSADD_I32_X8: i64 = 119 517const OP_SIMD_VSSUB_I32_X8: i64 = 120 518// Horizontal sum of 8 i32 lanes -> i64 (widening: max |sum| = 519// 8 * 2^31 ~ 2^34, needs i64 accumulator). 520// (v:*i64) -> i64 521const OP_SIMD_VREDUCE_SUM_I32_X8: i64 = 121 522// Broadcast scalar i64 (low 32 bits) into all 8 i32 lanes, store *out. 523// (scalar:i64, out:*i64) -> void 524const OP_SIMD_VBROADCAST_I32_X8: i64 = 122 525// ---- i64x4 SIMD ops (4 lanes of 64-bit per 256-bit vector) ---- 526const OP_SIMD_VADD_I64_X4: i64 = 123 527const OP_SIMD_VSUB_I64_X4: i64 = 124 528const OP_SIMD_VMUL_I64_X4: i64 = 125 529const OP_SIMD_VSADD_I64_X4: i64 = 126 530const OP_SIMD_VSSUB_I64_X4: i64 = 127 531const OP_SIMD_VREDUCE_SUM_I64_X4: i64 = 128 532const OP_SIMD_VBROADCAST_I64_X4: i64 = 129 533 534// Cycle-accurate timestamp counter (x86 rdtsc / rv64 rdcycle). Zero-arg 535// intrinsic `__rdtsc()` -> reads the CPU's monotonic cycle counter into a 536// 64-bit result. The keystone for MEASURED (not asserted) hot-path 537// performance: bracket a code region with two reads, subtract for cycles. 538const OP_RDTSC: i64 = 130 539const OP_UMULHI: i64 = 131 // unsigned 64x64 -> high 64 bits (x86 mulq); G2 wide-multiply 540const OP_ADC_ACC: i64 = 132 // G3 native add-with-carry: (hi:lo) += into a 3-word *acc (addq;adcq;adcq) 541 // INVARIANT: op0 (acc_ptr) MUST stay an escaping nx_scratch pointer, 542 // never an OP_ALLOCA [i64;3] (mem2reg promotion would break it). 543const OP_CPUID_EBX: i64 = 133 // x86 cpuid(leaf=op0, subleaf=op1) -> EBX (feature register). For the 544 // BMI2/ADX gate (FIX-10): cpuid(7,0):EBX bit-8=BMI2, bit-19=ADX. Pure. 545 546const OP_WFI: i64 = 70 547const OP_CSR_READ: i64 = 71 548const OP_CSR_WRITE: i64 = 72 549const OP_FENCE: i64 = 73 550const OP_MRET: i64 = 74 551 552// ValueKind: Value.kind --------------------------------------------- 553 554const VK_CONST_INT: i64 = 0 555const VK_PARAM: i64 = 1 556const VK_INSTR: i64 = 2 557// Address of a module-level global (string literal, static data). 558// const_int field stores the global id (index into m.globals); 559// codegen lowers via `la <reg>, .Lg<id>` instead of `li`. 560// Closes T#selfhost-003. 561const VK_GLOBAL: i64 = 3 562// Address of a named function -- const_int holds the *Function; rematerialised inline as 563// `leaq <fn.name>(%rip), %reg` (like VK_GLOBAL but the symbol is the function's own label). 564const VK_FUNC_ADDR: i64 = 4 565 566// TypeKind: Type.kind ----------------------------------------------- 567// 568// Prefix is TY_ (not TK_) so it can't collide with TOKEN kinds used 569// by lex.nx / parse.nx which conventionally read as `t.kind == TK_*`. 570 571const TY_VOID: i64 = 0 572const TY_BOOL: i64 = 1 573const TY_I8: i64 = 2 574const TY_I16: i64 = 3 575const TY_I32: i64 = 4 576const TY_I64: i64 = 5 577const TY_PTR: i64 = 6 578const TY_STRUCT: i64 = 7 579const TY_PARAM: i64 = 8 // generic type variable (T, E) inside a template 580// Floating-point types -- require RV64F + RV64D extension support 581// in the codegen. Scaffolded; full lowering pending separate 582// commits in runtime/riscv.nx + runtime/regalloc.nx (f-reg pool). 583const TY_F32: i64 = 9 // single precision (RV64F) 584const TY_F64: i64 = 10 // double precision (RV64D) 585// Fixed-size STACK array `[N]T` -- frame-allocated aggregate (like a stack struct). 586// pointee = element Type; size = N * elem.size. A bare array name DECAYS to its 587// address (leaq), and arr[i] indexes via GEP(base-as-address, i*elem.size). Zero heap, 588// per-call (no data race), freed on return. Planned since the Type.pointee "for ARRAY" note. 589const TY_ARRAY: i64 = 11 590// Function-pointer type `func(T,...)->R` -- 8 bytes (a code address), like a pointer. 591// pointee = return Type (reuses the PTR/ARRAY pointee slot); params not stored (MVP: no arity check). 592// A bare function name or `&fn` yields a VK_FUNC_ADDR value; calling a func-typed local = indirect call. 593const TY_FUNC: i64 = 12 594// Slice type `[]T` -- a length-carrying view. THE point: a bare *T carries no length, so 595// `p[i]` has nothing to be checked against; 99.8% of the corpus indexes pointers, which is why 596// the [N]T bounds check alone reached almost nothing. A slice pairs the pointer with its length 597// so the SAME check works on heap/mmap memory. 598// 599// Representation: the slice VALUE is an 8-byte HANDLE (hence size 8, so it flows through the 600// ordinary scalar local/param/return paths with no aggregate-ABI work) pointing at a 2-word 601// header: [0] = data pointer, [8] = length in ELEMENTS. pointee = element Type, as for PTR/ARRAY. 602// ⚠THESE THREE ARE LOAD-BEARING AND WERE ABSENT FROM THE NAS BUILDROOT COPY UNTIL 2026-07-30 (seq1393, 603// found by a sibling once the undefined-identifier seal turned silent-0 resolution into a hard error). 604// Keeping their failure modes recorded here, because the values look arbitrary and are not: 605// TY_SLICE undefined -> silently 0 = TY_VOID, so []T was built AS VOID and every `ty.kind==TY_SLICE` 606// test was really a void test; the feature only appeared to work because both sides of the 607// comparison shared the same wrong value. 608// NX_SLICE_HDR_BYTES 0 -> the header alloca reserved ZERO bytes, so the header aliased adjacent stack. 609// NX_SLICE_LEN_OFFSET 0 -> the length was stored ON TOP of the data pointer, so every bounds check 610// compared the index against a POINTER value and ALWAYS PASSED. That is memory-UNSAFE, not merely 611// wrong, and it is why a bounds-check feature can look present while checking nothing. 612// The values are the layout the emitting code already assumes -- not new policy. Do not "tidy" them. 613const TY_SLICE: i64 = 13 614const NX_SLICE_HDR_BYTES: i64 = 16 // {data, len} 615const NX_SLICE_LEN_OFFSET: i64 = 8 // byte offset of len within the header 616 617// ValueLoc.kind ----------------------------------------------------- 618 619const VL_REGISTER: i64 = 0 620const VL_SPILLED: i64 = 1 621// Rematerialised inline at every use site (constants, global addrs, 622// alloca addresses). riscv.nx's materialise() recomputes via the 623// special-case paths; idx is unused. Skipped by linear-scan so it 624// doesn't consume a register slot. 625const VL_REMAT: i64 = 2 626// F14 fix: alloca slot. idx holds the sp-relative byte offset of 627// the alloca's stack home. materialise() emits `addi reg, sp, idx` 628// at every use site, mirroring the C anchor's behaviour. Without 629// this, alloca pointers competed for caller-clobbered t-regs and 630// were clobbered by intermediate `slt`/`add` instructions across 631// loop iterations. See bench/ir_diff_corpus/regression_alloca_ 632// remat_param_step_loop.nx for the minimum failing case. 633const VL_ALLOCA: i64 = 3 634 635// Value.kind ------------------------------------------------------- 636// (Mirrors the inline comment on the kind field in struct Value.) 637const VAL_CONST: i64 = 0 638const VAL_PARAM: i64 = 1 639const VAL_INSTR: i64 = 2