code wiki / _hdl_build / nx_driver_shapes.nx

nx_driver_shapes.nx source

↩ module page · 266 lines · 16356 B

1// SUPERSEDED 2026-06-13 (no-sprawl consolidation): the CANONICAL driver-from-spec path is the 2// op-list-driven nx_drv_proto_emit.nx (a new device class = a new op-list SPEC, zero emitter change). 3// This shape library + its consumer nx_drvgen are KEPT only as the byte-identical virtio re-emit 4// regression reference (NVMEGATE stage=B PROOF-1). See nx_drvgen.nx header + nvme_oplist_virt.spec. 5// nx_driver_shapes.nx -- the DEVICE-AGNOSTIC driver shape library (X-DRV-W1 stage A). This is 6// the emitter-of-emitters substrate for driver bring-up: a tiny rv64 mini-encoder plus four 7// SPEC-DATA-DRIVEN emit-op families that, between them, express the structure of ANY register- 8// programmed device-init driver. ZERO virtio (or NVMe) identity is baked here -- every offset 9// and constant an op touches arrives as a PARAMETER, pulled by the caller from a generic field 10// table. The device identity lives entirely in the spec; this library only knows how to turn 11// (offset, value) tuples into bytes. The four shapes: 12// 13// WIRE_TLV verify "reg-at-offset == const" / write "const -> reg-at-offset". The bring-up 14// handshake is a list of WIRE_TLV ops (virtio MagicValue/Version/DeviceID verifies 15// + Status ORs; NVMe would be CAP/VS verifies + AQA/ASQ/ACQ writes). Provided by 16// ds_emit_verify / ds_emit_verify_u / ds_emit_regwrite. 17// COMMAND_QUEUE bind a queue (write base/PFN regs) + kick a doorbell/notify + read an instrument 18// register back and verify it. virtio's QueuePFN+QueueNotify and NVMe's ASQ/ACQ+ 19// SQyTDBL are the SAME op over different spec rows -- the caller threads the bind 20// regs, the kick reg/val, and the result reg/expected. The read-back verify is 21// ds_emit_verify / ds_emit_verify_u; the kick is ds_emit_regwrite. 22// STATE_MACHINE an enable/progress step that is EITHER monotonic-OR-write (virtio Status) OR 23// write-then-poll-until-bit-set (NVMe CC.EN -> CSTS.RDY). ds_emit_regwrite covers 24// the OR-write half; ds_emit_poll_until_set synthesises the poll-loop control flow 25// (a real backward branch) from a spec field -- the structure comes from DATA, not 26// from code (the eoe / X-AUT-006d principle). Virtio uses only the OR-write half; 27// the poll half is exercised by the 2nd device class (stage B). 28// STRUCT_WALK store struct fields at base_reg + memoff with width w, driven by a field table 29// (off, width, value). virtio's 16-byte split-virtqueue descriptor and NVMe's 30// 64-byte SQE are the SAME op over different field tables. Provided by 31// ds_emit_memstore (and ds_li32 / ds_li32u for materialising a struct base addr). 32// 33// Byte-reproducibility: every op keeps the two-pass measure/backfill invariant of the original 34// virtio emitter (instruction COUNT identical across passes; only immediates differ), so a forward 35// fail-branch target is structurally derived, never typed. This file is lifted VERBATIM (byte-for- 36// byte logic) from nx_virtio_hs_emit's encoder so the refactored virtio emitter that now CALLS 37// these ops re-emits its image byte-identically (the stage-A regression guard). 38// Sovereign: pure integer arithmetic, no syscalls beyond what the importer pulls in via 39// nx_syscalls. license_tier: ORIGINAL 40import "nx_syscalls.nx" 41 42// ---- generic target-platform map (qemu-virt rv64; hardware facts, not device identity) ---- 43const DS_UART: i64 = 0x10000000 // NS16550A THR (write a byte = transmit a transcript char) 44const DS_FIN: i64 = 0x100000 // SiFive test finisher (write to exit the machine cleanly) 45const DS_PASS: i64 = 0x5555 // FINISHER_PASS low half -> clean halt 46// rv64 register numbers the shape ops use (a fixed calling convention, device-agnostic) 47const DS_X0: i64 = 0 48const DS_T0: i64 = 5 // UART base 49const DS_T1: i64 = 6 // scratch / transcript byte 50const DS_T2: i64 = 7 // device base 51const DS_T3: i64 = 28 // loaded register value (actual) 52const DS_T4: i64 = 29 // expected constant / poll mask 53const DS_T5: i64 = 30 // finisher base / struct-base scratch 54 55// ---- the rv64 mini-encoder (one form per function; integer-only, struct-free) ---- 56func ds_lui(rd: i64, imm20: i64) -> i64 { return ((imm20 & 0xFFFFF) << 12) | (rd << 7) | 0x37 } 57func ds_addi(rd: i64, rs1: i64, imm: i64) -> i64 { return ((imm & 0xFFF) << 20) | (rs1 << 15) | (rd << 7) | 0x13 } 58// load; f3=2 -> LW (sign-extended 32-bit), f3=6 -> LWU (zero-extended). imm I-type (signed 12-bit). 59func ds_load(rd: i64, rs1: i64, f3: i64, imm: i64) -> i64 { return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x03 } 60// store; f3=0 -> SB, f3=1 -> SH, f3=2 -> SW. imm S-type split (signed 12-bit). 61func ds_store(rs2: i64, rs1: i64, f3: i64, imm: i64) -> i64 { 62 let hi: i64 = ((imm >> 5) & 0x7f) << 25 63 let lo: i64 = (imm & 0x1f) << 7 64 return hi | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | lo | 0x23 65} 66// B-type branch; f3=0 -> BEQ, f3=1 -> BNE. imm in bytes (signed, multiple of 2). 67func ds_branch(rs1: i64, rs2: i64, f3: i64, imm: i64) -> i64 { 68 let b12: i64 = ((imm >> 12) & 0x1) << 31 69 let b11: i64 = ((imm >> 11) & 0x1) << 7 70 let b10_5: i64 = ((imm >> 5) & 0x3f) << 25 71 let b4_1: i64 = ((imm >> 1) & 0xf) << 8 72 return b12 | b10_5 | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | b4_1 | b11 | 0x63 73} 74// J-type jal; imm in bytes (signed, multiple of 2). 75func ds_jal(rd: i64, imm: i64) -> i64 { 76 let b20: i64 = ((imm >> 20) & 0x1) << 31 77 let b19_12: i64 = ((imm >> 12) & 0xff) << 12 78 let b11: i64 = ((imm >> 11) & 0x1) << 20 79 let b10_1: i64 = ((imm >> 1) & 0x3ff) << 21 80 return b20 | b10_1 | b11 | b19_12 | (rd << 7) | 0x6f 81} 82// R-type AND (funct3=7, funct7=0): rd = rs1 & rs2. 83func ds_and(rd: i64, rs1: i64, rs2: i64) -> i64 { return (rs2 << 20) | (rs1 << 15) | (7 << 12) | (rd << 7) | 0x33 } 84// I-type shift-immediate (OP-IMM, opcode 0x13): f3=1 -> SLLI, f3=5 -> SRLI. rv64 shamt is 6 bits. 85func ds_shift(rd: i64, rs1: i64, f3: i64, shamt: i64) -> i64 { return ((shamt & 0x3f) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x13 } 86 87func ds_w32(buf: *u8, off: i64, w: i64) -> i64 { 88 buf[off] = (w & 0xff) as u8 89 buf[off+1] = ((w >> 8) & 0xff) as u8 90 buf[off+2] = ((w >> 16) & 0xff) as u8 91 buf[off+3] = ((w >> 24) & 0xff) as u8 92 return off + 4 93} 94 95// load a full 32-bit constant into rd: lui hi + addi lo (sign-corrected). 2 words. On rv64 lui 96// sign-extends bit 31, so a bit-31-set value leaves rd = 0xFFFFFFFF_xxxxxxxx -- correct for MMIO 97// consts / feature masks (sign-extended compares) but WRONG for a true positive RAM address 98// (use ds_li32u for those). 99func ds_li32(buf: *u8, off: i64, rd: i64, val: i64) -> i64 { 100 var hi: i64 = (val >> 12) & 0xFFFFF 101 var lo: i64 = val & 0xFFF 102 if lo >= 0x800 { lo = lo - 0x1000; hi = (hi + 1) & 0xFFFFF } 103 var o: i64 = ds_w32(buf, off, ds_lui(rd, hi)) 104 o = ds_w32(buf, o, ds_addi(rd, rd, lo)) 105 return o 106} 107 108// load a ZERO-EXTENDED 32-bit constant (a true positive guest address) into rd. lui+addi then 109// slli rd,rd,32 ; srli rd,rd,32 clears the sign-extended upper 32 bits so a bit-31-set address 110// materialises positive. 4 words -- the canonical rv64 idiom for a 32-bit phys addr with bit 31 set. 111func ds_li32u(buf: *u8, off: i64, rd: i64, val: i64) -> i64 { 112 var o: i64 = ds_li32(buf, off, rd, val) 113 o = ds_w32(buf, o, ds_shift(rd, rd, 1, 32)) // slli rd,rd,32 114 o = ds_w32(buf, o, ds_shift(rd, rd, 5, 32)) // srli rd,rd,32 115 return o 116} 117 118// ---- transcript primitive: emit n bytes from s over the UART (t0 = UART base). 2 words/char. ---- 119func ds_emit_str(buf: *u8, off: i64, s: *u8, n: i64) -> i64 { 120 var o: i64 = off 121 var i: i64 = 0 122 while i < n { 123 o = ds_w32(buf, o, ds_addi(DS_T1, DS_X0, s[i] as i64)) 124 o = ds_w32(buf, o, ds_store(DS_T1, DS_T0, 0, 0)) // sb t1,0(t0) 125 i = i + 1 126 } 127 return o 128} 129 130// ==== SHAPE: WIRE_TLV ==== 131// verify reg-at-offset == expected, else branch to fail_off (the finisher block). 132// lw t3, regoff(t2) ; li t4, expected ; bne t3,t4, (fail_off - pc). 4 words. 133func ds_emit_verify(buf: *u8, off: i64, regoff: i64, expected: i64, fail_off: i64) -> i64 { 134 var o: i64 = ds_w32(buf, off, ds_load(DS_T3, DS_T2, 2, regoff)) 135 o = ds_li32(buf, o, DS_T4, expected) 136 let pc: i64 = o 137 o = ds_w32(buf, o, ds_branch(DS_T3, DS_T4, 1, fail_off - pc)) 138 return o 139} 140 141// verify reg-at-offset == expected for an UNSIGNED 32-bit value (e.g. a guest phys addr whose bit 142// 31 is set), else branch to fail_off. Uses LWU (f3=6, zero-extend), so the EXPECTED CONSTANT MUST 143// BE MATERIALISED ZERO-EXTENDED TOO -- ds_li32u (li32 ; slli 32 ; srli 32), never ds_li32. 144// 6 words. The count is still VALUE-INDEPENDENT, so the two-pass measure/backfill invariant holds 145// exactly as before; only the constant 4 changed, and that constant was never the invariant. 146// 147// ROOT CAUSE, FIXED HERE 2026-08-15 -- and it was ALREADY DIAGNOSED AND FIXED IN THE SIBLING: 148// rv64 LUI sign-extends from bit 31. The sovereign sim was CORRECTED to sign-extend on 2026-07-09 149// (rv64im_min_decoder nx_rv64im_imm_u), which silently turned every `verifyu` against a bit-31-set 150// constant into a compare of a ZERO-extended load against a NEGATIVE constant -- a bne that can 151// NEVER be equal. The header comment above used to assert "the lui-built POSITIVE expected", which 152// stopped being true that day and then read as justification for the bug. 153// nx_drv_proto_emit.nx hit exactly this, diagnosed it on 2026-08-07 and fixed it in dp_op_verify -- 154// but the fix never reached THIS library, so nx_virtio_hs_emit (which delegates every byte to these 155// ds_* ops) kept emitting the broken compare and _k_r2_001b3_gate stayed RED at the same boundary. 156// ******A FIX THAT LIVES IN ONE ORGAN AND NOT ITS SIBLING IS HALF A FIX, AND THE HALF THAT IS MISSING 157// IS INVISIBLE UNTIL SOMETHING RUNS IT. Measured the same day: two emitters, one corrected, one not, 158// failing at the byte-identical transcript boundary. 159// **A COMMENT THAT OUTLIVES THE FACT IT ASSERTS BECOMES A FALSE CLAIM WITH A TRUSTED BYLINE. 160func ds_emit_verify_u(buf: *u8, off: i64, regoff: i64, expected: i64, fail_off: i64) -> i64 { 161 var o: i64 = ds_w32(buf, off, ds_load(DS_T3, DS_T2, 6, regoff)) 162 o = ds_li32u(buf, o, DS_T4, expected) 163 let pc: i64 = o 164 o = ds_w32(buf, o, ds_branch(DS_T3, DS_T4, 1, fail_off - pc)) 165 return o 166} 167 168// write a value into a MMIO register (a Status latch ORs writes; a config reg latches the value). 169// li t1,val ; sw t1, regoff(t2). 3 words. 170func ds_emit_regwrite(buf: *u8, off: i64, regoff: i64, val: i64) -> i64 { 171 var o: i64 = ds_li32(buf, off, DS_T1, val) 172 o = ds_w32(buf, o, ds_store(DS_T1, DS_T2, 2, regoff)) 173 return o 174} 175 176// confirm (reg & mask) != 0, else branch to fail_off (a WIRE_TLV "bit stuck" check). 177// lw t3, regoff(t2) ; li t4, mask ; and t3,t3,t4 ; beq t3,x0, fail. 5 words. 178func ds_emit_checkbit(buf: *u8, off: i64, regoff: i64, mask: i64, fail_off: i64) -> i64 { 179 var o: i64 = ds_w32(buf, off, ds_load(DS_T3, DS_T2, 2, regoff)) 180 o = ds_li32(buf, o, DS_T4, mask) 181 o = ds_w32(buf, o, ds_and(DS_T3, DS_T3, DS_T4)) 182 let pc: i64 = o 183 o = ds_w32(buf, o, ds_branch(DS_T3, DS_X0, 0, fail_off - pc)) 184 return o 185} 186 187// write a value into a MMIO register at an ABSOLUTE address (not a 12-bit offset from the 188// device base). Needed when a register lives FAR from the base (NVMe's doorbell page at 189// base+0x1000 and the CqPeek instrument at base+0x1008 are beyond the rv64 sw/lw signed 190// 12-bit immediate range of +/-2047). Materialise the full address into a scratch register 191// (DS_T5) and store at offset 0. li t5,regaddr ; li t1,val ; sw t1,0(t5). 4 words. Generic: 192// the caller threads regaddr = base + regoff (the device identity is data, not code). 193func ds_emit_regwrite_far(buf: *u8, off: i64, regaddr: i64, val: i64) -> i64 { 194 var o: i64 = ds_li32(buf, off, DS_T5, regaddr) 195 o = ds_li32(buf, o, DS_T1, val) 196 o = ds_w32(buf, o, ds_store(DS_T1, DS_T5, 2, 0)) 197 return o 198} 199 200// verify reg-at-ABSOLUTE-address == expected (unsigned, LWU), else branch to fail_off. The FAR 201// twin of ds_emit_verify_u for a register beyond the 12-bit immediate range (NVMe's CqPeek at 202// base+0x1008). li t5,regaddr ; lwu t3,0(t5) ; li t4,expected ; bne t3,t4, fail. 6 words. 203func ds_emit_verify_far(buf: *u8, off: i64, regaddr: i64, expected: i64, fail_off: i64) -> i64 { 204 var o: i64 = ds_li32(buf, off, DS_T5, regaddr) 205 o = ds_w32(buf, o, ds_load(DS_T3, DS_T5, 6, 0)) 206 o = ds_li32(buf, o, DS_T4, expected) 207 let pc: i64 = o 208 o = ds_w32(buf, o, ds_branch(DS_T3, DS_T4, 1, fail_off - pc)) 209 return o 210} 211 212// ==== SHAPE: STRUCT_WALK ==== 213// store a 32-bit immediate into RAM at base_reg + memoff (f3=2 -> SW, f3=1 -> SH, f3=0 -> SB). 214// li t1,val ; sw/sh/sb t1, memoff(base_reg). 3 words. (base_reg already holds the struct addr.) 215func ds_emit_memstore(buf: *u8, off: i64, base_reg: i64, memoff: i64, f3: i64, val: i64) -> i64 { 216 var o: i64 = ds_li32(buf, off, DS_T1, val) 217 o = ds_w32(buf, o, ds_store(DS_T1, base_reg, f3, memoff)) 218 return o 219} 220 221// ==== SHAPE: STATE_MACHINE (poll half) ==== 222// write-then-poll-until-bit-set: the device-enable progress step that virtio does NOT have but 223// NVMe (CC.EN -> CSTS.RDY) does. Emits a REAL backward branch (a poll loop): the read of the 224// status reg, the AND with the ready mask, and a BEQ back to the read while the bit is clear. 225// li t1, enable_val ; sw t1, en_off(t2) -- write the enable 226// L: lw t3, rdy_off(t2) ; li t4, rdy_mask ; and t3,t3,t4 ; beq t3,x0, L (backward branch). 8 words. 227// The backward branch (negative B-type immediate) is the structural fingerprint that distinguishes 228// a polled state machine from virtio's straight-line forward-only handshake -- constant-lifting a 229// forward-only image provably cannot introduce a loop (the eoe / X-AUT-006d argument). 230func ds_emit_poll_until_set(buf: *u8, off: i64, en_off: i64, en_val: i64, rdy_off: i64, rdy_mask: i64) -> i64 { 231 var o: i64 = ds_li32(buf, off, DS_T1, en_val) 232 o = ds_w32(buf, o, ds_store(DS_T1, DS_T2, 2, en_off)) // sw enable -> en reg 233 let loop_pc: i64 = o // L: 234 o = ds_w32(buf, o, ds_load(DS_T3, DS_T2, 2, rdy_off)) // lw t3, rdy(t2) 235 o = ds_li32(buf, o, DS_T4, rdy_mask) // li t4, mask 236 o = ds_w32(buf, o, ds_and(DS_T3, DS_T3, DS_T4)) // and t3,t3,t4 237 let pc: i64 = o 238 o = ds_w32(buf, o, ds_branch(DS_T3, DS_X0, 0, loop_pc - pc)) // beq t3,x0, L (backward: loop_pc - pc < 0) 239 return o 240} 241 242// poll-until-bit-set over a MEMORY location pointed at by base_reg (NOT a device register). 243// virtio has no such loop; NVMe's CQE phase-tag poll (spin on the completion entry in guest RAM 244// until the device sets the phase bit) does. No enable-write half -- the device-side post already 245// happened (the doorbell DMA ran on the kick); this is purely the driver SPINNING on the result. 246// L: lw t3, poll_off(base_reg) ; li t4, mask ; and t3,t3,t4 ; beq t3,x0, L (backward branch). 5 words. 247// Same STATE_MACHINE family as ds_emit_poll_until_set -- a real backward B-type immediate, the 248// structural fingerprint a forward-only image (virtio) provably cannot carry (eoe / X-AUT-006d). 249func ds_emit_poll_mem_until_set(buf: *u8, off: i64, base_reg: i64, poll_off: i64, mask: i64) -> i64 { 250 let loop_pc: i64 = off // L: 251 var o: i64 = ds_w32(buf, off, ds_load(DS_T3, base_reg, 2, poll_off)) // lw t3, poll(base_reg) 252 o = ds_li32(buf, o, DS_T4, mask) // li t4, mask 253 o = ds_w32(buf, o, ds_and(DS_T3, DS_T3, DS_T4)) // and t3,t3,t4 254 let pc: i64 = o 255 o = ds_w32(buf, o, ds_branch(DS_T3, DS_X0, 0, loop_pc - pc)) // beq t3,x0, L (backward) 256 return o 257} 258 259// ==== finisher: li t5,FIN ; li t1,PASS ; sw t1,0(t5) -> clean halt ; jal x0,0 spin. 6 words. ==== 260func ds_emit_finisher(buf: *u8, off: i64) -> i64 { 261 var o: i64 = ds_li32(buf, off, DS_T5, DS_FIN) 262 o = ds_li32(buf, o, DS_T1, DS_PASS) 263 o = ds_w32(buf, o, ds_store(DS_T1, DS_T5, 2, 0)) 264 o = ds_w32(buf, o, ds_jal(DS_X0, 0)) 265 return o 266}