code wiki / _hdl_build / rv64im_min_decoder.nx
rv64im_min_decoder.nx source
↩ module page · 291 lines · 11360 B
1// rv64im_min_decoder.nx -- first concrete use of the NishiHDL DSL.
2//
3// Decodes RV64I + M instructions into a sealed-enum opcode kind +
4// operand-extracted fields (rd, rs1, rs2, immediate). This is the
5// fetch-decode stage of the Tier A FPGA target (see
6// hdl/rv64im_min_target_spec.md for the silicon contract).
7//
8// Status: SEED. 2026-05-26. Decoder logic only; execute / mem /
9// writeback land in follow-on commits.
10//
11// Why decoder first:
12// 1. Smallest standalone module that exercises every NishiHDL
13// primitive (wires, modules, connect, sealed enums).
14// 2. Verifiable against ANY known RV64IM-binary -- correctness
15// can be ground-truthed before any execute logic exists.
16// 3. Decoder shape is identical across single-cycle, 5-stage,
17// and out-of-order pipelines; so this module survives any
18// future pipeline-style change.
19//
20// RV64I instruction format (per riscv-spec):
21// bits 31..25 funct7
22// bits 24..20 rs2
23// bits 19..15 rs1
24// bits 14..12 funct3
25// bits 11..7 rd
26// bits 6..0 opcode
27//
28// The seven base RV64I opcodes (low 7 bits):
29// 0110011 R-type add/sub/sll/slt/sltu/xor/srl/sra/or/and (also M ext)
30// 0010011 I-type addi/slti/sltiu/xori/ori/andi/slli/srli/srai
31// 0000011 Load lb/lh/lw/ld/lbu/lhu/lwu
32// 0100011 Store sb/sh/sw/sd
33// 1100011 Branch beq/bne/blt/bge/bltu/bgeu
34// 1101111 jal pc-relative jump-and-link
35// 1100111 jalr reg-indirect jump-and-link
36// 0110111 lui load upper immediate
37// 0010111 auipc add upper immediate to pc
38// 1110011 System ecall/ebreak/csrrw/csrrs/csrrc/csrrwi/csrrsi/csrrci
39// 0001111 Fence fence/fence.i
40//
41// RV64I-64 widening adds W-suffixed ops:
42// 0111011 R-type 32-bit addw/subw/sllw/srlw/sraw + M-ext mulw/divw/...
43// 0011011 I-type 32-bit addiw/slliw/srliw/sraiw
44//
45// M extension overloads opcode 0110011 with funct7=0000001:
46// mul / mulh / mulhsu / mulhu / div / divu / rem / remu
47// And opcode 0111011 with funct7=0000001:
48// mulw / divw / divuw / remw / remuw
49
50import "nx_syscalls.nx"
51import "nishi_hdl_primitives.nx"
52
53// ===== Decoded opcode kinds (sealed enum) =================================================
54//
55// Every RV64IM instruction collapses to one of these kinds. Sealed:
56// decoders that produce a kind >= NX_RV64IM_OP_N are bugs.
57
58const NX_RV64IM_OP_INVALID: i64 = 0 // illegal instruction (raises exc)
59
60// RV64I base
61const NX_RV64IM_OP_LUI: i64 = 1
62const NX_RV64IM_OP_AUIPC: i64 = 2
63const NX_RV64IM_OP_JAL: i64 = 3
64const NX_RV64IM_OP_JALR: i64 = 4
65const NX_RV64IM_OP_BRANCH: i64 = 5 // beq/bne/blt/bge/bltu/bgeu (funct3 disambiguates)
66const NX_RV64IM_OP_LOAD: i64 = 6 // lb/lh/lw/ld/lbu/lhu/lwu
67const NX_RV64IM_OP_STORE: i64 = 7 // sb/sh/sw/sd
68const NX_RV64IM_OP_OP_IMM: i64 = 8 // addi/slti/sltiu/xori/ori/andi/slli/srli/srai
69const NX_RV64IM_OP_OP: i64 = 9 // add/sub/sll/slt/sltu/xor/srl/sra/or/and
70const NX_RV64IM_OP_OP_IMM_32: i64 = 10 // addiw/slliw/srliw/sraiw
71const NX_RV64IM_OP_OP_32: i64 = 11 // addw/subw/sllw/srlw/sraw
72const NX_RV64IM_OP_FENCE: i64 = 12
73const NX_RV64IM_OP_SYSTEM: i64 = 13 // ecall/ebreak/csrrw/...
74
75// M extension (overlays OP / OP_32 when funct7=0000001)
76const NX_RV64IM_OP_M_MUL: i64 = 14 // mul/mulh/mulhsu/mulhu
77const NX_RV64IM_OP_M_DIV: i64 = 15 // div/divu/rem/remu
78const NX_RV64IM_OP_M_MUL_32: i64 = 16 // mulw
79const NX_RV64IM_OP_M_DIV_32: i64 = 17 // divw/divuw/remw/remuw
80
81// A extension (atomics): LR/SC + AMO* share opcode 0x2f; funct5 (inst[31:27]) disambiguates,
82// funct3 selects width (2 = .w, 3 = .d). Added 2026-07-18 to close the RV64A gap (F107h).
83const NX_RV64IM_OP_AMO: i64 = 18
84
85const NX_RV64IM_OP_N: i64 = 19
86
87func nx_rv64im_op_is_valid(op: i64) -> i64 {
88 if op < 0 { return 0 }
89 if op >= NX_RV64IM_OP_N { return 0 }
90 return 1
91}
92
93// ===== Bit-field extraction helpers =================================================
94//
95// These mirror the riscv-spec slice notation. Inputs are i64
96// (a 32-bit instruction fits comfortably; high bits are zero).
97
98func nx_rv64im_opcode(inst: i64) -> i64 {
99 return inst & 0x7f
100}
101
102func nx_rv64im_rd(inst: i64) -> i64 {
103 return (inst >> 7) & 0x1f
104}
105
106func nx_rv64im_funct3(inst: i64) -> i64 {
107 return (inst >> 12) & 0x7
108}
109
110func nx_rv64im_rs1(inst: i64) -> i64 {
111 return (inst >> 15) & 0x1f
112}
113
114func nx_rv64im_rs2(inst: i64) -> i64 {
115 return (inst >> 20) & 0x1f
116}
117
118func nx_rv64im_funct7(inst: i64) -> i64 {
119 return (inst >> 25) & 0x7f
120}
121
122// ===== Immediate decoders (per format) =================================================
123//
124// Each instruction format encodes its immediate differently; the
125// decoder produces sign-extended 64-bit immediates ready for the
126// execute stage. Sign extension uses arithmetic shift via the
127// upper-bit-mask trick (NishiLang i64 shift is logical).
128
129func nx_rv64im_imm_i(inst: i64) -> i64 {
130 // I-type: inst[31:20], sign-extended from bit 31.
131 let raw: i64 = (inst >> 20) & 0xfff
132 if (raw & 0x800) != 0 {
133 return raw | (0 - 4096) // sign-extend: bits 11..63 = 1
134 }
135 return raw
136}
137
138func nx_rv64im_imm_s(inst: i64) -> i64 {
139 // S-type: inst[31:25]<<5 | inst[11:7], sign-extended.
140 let hi: i64 = ((inst >> 25) & 0x7f) << 5
141 let lo: i64 = (inst >> 7) & 0x1f
142 let raw: i64 = hi | lo
143 if (raw & 0x800) != 0 {
144 return raw | (0 - 4096)
145 }
146 return raw
147}
148
149func nx_rv64im_imm_b(inst: i64) -> i64 {
150 // B-type: inst[31]<<12 | inst[7]<<11 | inst[30:25]<<5 | inst[11:8]<<1, sign-extended.
151 let b12: i64 = ((inst >> 31) & 0x1) << 12
152 let b11: i64 = ((inst >> 7) & 0x1) << 11
153 let b10_5: i64 = ((inst >> 25) & 0x3f) << 5
154 let b4_1: i64 = ((inst >> 8) & 0xf) << 1
155 let raw: i64 = b12 | b11 | b10_5 | b4_1
156 if (raw & 0x1000) != 0 {
157 return raw | (0 - 8192)
158 }
159 return raw
160}
161
162func nx_rv64im_imm_u(inst: i64) -> i64 {
163 // U-type (lui/auipc): the 20-bit immediate sits in inst[31:12], low 12 zero. RV64 SIGN-EXTENDS from bit 31 into
164 // bits [63:32] -- so a lui with bit 31 set is a NEGATIVE 64-bit value. (BUG FIX 2026-07-09: the old
165 // `(inst>>12)<<12` zero-extended, silently invisible in low bytes but WRONG for mulh/signed-div/slt/sra of a
166 // lui-derived negative -- caught by the QEMU differential fuzzer.)
167 let v: i64 = inst & 0xFFFFF000 // bits [31:12], [11:0]=0 (positive, < 2^32)
168 if (v & 0x80000000) != 0 { return v - 4294967296 } // sign-extend from bit 31 (subtract 2^32)
169 return v
170}
171
172func nx_rv64im_imm_j(inst: i64) -> i64 {
173 // J-type (jal): inst[31]<<20 | inst[19:12]<<12 | inst[20]<<11 | inst[30:21]<<1.
174 let b20: i64 = ((inst >> 31) & 0x1) << 20
175 let b19_12: i64 = ((inst >> 12) & 0xff) << 12
176 let b11: i64 = ((inst >> 20) & 0x1) << 11
177 let b10_1: i64 = ((inst >> 21) & 0x3ff) << 1
178 let raw: i64 = b20 | b19_12 | b11 | b10_1
179 if (raw & 0x100000) != 0 {
180 return raw | (0 - 2097152)
181 }
182 return raw
183}
184
185// ===== Top-level decode =================================================
186//
187// Single-cycle decode: given a 32-bit instruction word, returns the
188// sealed-enum kind code. Operand fields (rd, rs1, rs2, immediate)
189// are extracted by the helpers above and used by the execute stage.
190
191func nx_rv64im_decode_kind(inst: i64) -> i64 {
192 let op: i64 = nx_rv64im_opcode(inst)
193
194 if op == 0x37 { return NX_RV64IM_OP_LUI }
195 if op == 0x17 { return NX_RV64IM_OP_AUIPC }
196 if op == 0x6f { return NX_RV64IM_OP_JAL }
197 if op == 0x67 { return NX_RV64IM_OP_JALR }
198 if op == 0x63 { return NX_RV64IM_OP_BRANCH }
199 if op == 0x03 { return NX_RV64IM_OP_LOAD }
200 if op == 0x23 { return NX_RV64IM_OP_STORE }
201 if op == 0x13 { return NX_RV64IM_OP_OP_IMM }
202 if op == 0x1b { return NX_RV64IM_OP_OP_IMM_32 }
203 if op == 0x0f { return NX_RV64IM_OP_FENCE }
204 if op == 0x73 { return NX_RV64IM_OP_SYSTEM }
205 if op == 0x2f { return NX_RV64IM_OP_AMO } // A extension: lr/sc + amo* (funct5 disambiguates)
206
207 // R-type 64-bit: M extension uses funct7=0000001; rest is base I.
208 if op == 0x33 {
209 let f7: i64 = nx_rv64im_funct7(inst)
210 if f7 == 0x01 {
211 let f3: i64 = nx_rv64im_funct3(inst)
212 if f3 < 4 { return NX_RV64IM_OP_M_MUL }
213 return NX_RV64IM_OP_M_DIV
214 }
215 return NX_RV64IM_OP_OP
216 }
217
218 // R-type 32-bit (W-suffix): M extension shares the funct7=0000001 overlay.
219 if op == 0x3b {
220 let f7: i64 = nx_rv64im_funct7(inst)
221 if f7 == 0x01 {
222 let f3: i64 = nx_rv64im_funct3(inst)
223 if f3 == 0 { return NX_RV64IM_OP_M_MUL_32 }
224 return NX_RV64IM_OP_M_DIV_32
225 }
226 return NX_RV64IM_OP_OP_32
227 }
228
229 return NX_RV64IM_OP_INVALID
230}
231
232// ===== HDL-graph builder (DSL use) =================================================
233//
234// Builds the decoder as a NishiHDL module: takes a 32-bit input wire
235// (the instruction word from fetch), declares the per-field output
236// wires, and registers the decode logic on the module graph.
237//
238// V1 returns OK if the module-graph was built successfully; future
239// commits (nishi-sim, nishi-synth) traverse the module to emit
240// behaviour or gates.
241
242const NX_RV64IM_DECODER_WIDTH_INST: i64 = 32 // input instruction
243const NX_RV64IM_DECODER_WIDTH_OPKIND: i64 = 8 // sealed enum (fits 18 values)
244const NX_RV64IM_DECODER_WIDTH_REG: i64 = 5 // rs1/rs2/rd index
245const NX_RV64IM_DECODER_WIDTH_IMM: i64 = 64 // sign-extended
246
247// Handles published by nx_rv64im_decoder_build(); caller wires the
248// CPU's execute stage to these.
249struct NxRv64imDecoderPorts {
250 inst_in: i64 // input wire, 32-bit
251 op_kind: i64 // output wire, 8-bit (sealed enum)
252 rd: i64 // output wire, 5-bit
253 rs1: i64 // output wire, 5-bit
254 rs2: i64 // output wire, 5-bit
255 imm_signed: i64 // output wire, 64-bit
256}
257
258func nx_rv64im_decoder_build(m: *NxHdlModule, ports: *NxRv64imDecoderPorts) -> i64 {
259 let p_inst: i64 = nx_hdl_input(m, NX_RV64IM_DECODER_WIDTH_INST)
260 if p_inst < 0 { return p_inst }
261 let p_opkind: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_OPKIND)
262 if p_opkind < 0 { return p_opkind }
263 let p_rd: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_REG)
264 if p_rd < 0 { return p_rd }
265 let p_rs1: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_REG)
266 if p_rs1 < 0 { return p_rs1 }
267 let p_rs2: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_REG)
268 if p_rs2 < 0 { return p_rs2 }
269 let p_imm: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_IMM)
270 if p_imm < 0 { return p_imm }
271
272 ports.inst_in = p_inst
273 ports.op_kind = p_opkind
274 ports.rd = p_rd
275 ports.rs1 = p_rs1
276 ports.rs2 = p_rs2
277 ports.imm_signed = p_imm
278
279 // V1: the actual combinational logic that drives p_opkind /
280 // p_rd / p_rs1 / p_rs2 / p_imm from p_inst lives in the
281 // simulator (nishi-sim, future commit) and the netlist emitter
282 // (nishi-synth, future commit), reading from this declared
283 // module shape. Today the module is structural-only: ports
284 // exist, no driver edges yet.
285 //
286 // Once nishi-sim lands, each port gets a per-cycle evaluator
287 // that calls nx_rv64im_decode_kind / nx_rv64im_rd / etc. on
288 // the latched instruction word.
289
290 return NX_HDL_OK
291}