OR-1 dataflow CPU sketch
1"""Boundary functions for pack/unpack between semantic types and 16-bit hardware words.
2
3This module encodes/decodes tokens and instructions using the exact hardware wire format
4from architecture-overview.md and design-notes/alu-and-output-design.md.
5
6Instruction word format: [type:1][opcode:5][mode:3][wide:1][fref:6]
7Flit 1 (routing/header flit) format depends on token kind (DYADIC, MONADIC, INLINE, SM).
8"""
9
10from __future__ import annotations
11
12from cm_inst import (
13 ALUOp, ArithOp, FrameDest, Instruction, LogicOp, MemOp,
14 OutputStyle, Port, RoutingOp, TokenKind,
15)
16from tokens import (
17 CMToken, DyadToken, FrameControlToken, MonadToken,
18 PELocalWriteToken, PEToken, SMToken, Token,
19)
20
21
22def _encode_mode(output: OutputStyle, has_const: bool, dest_count: int) -> int:
23 """Encode OutputStyle + has_const + dest_count into 3-bit mode field.
24
25 Follows the mode table from design-notes/alu-and-output-design.md.
26
27 Validation:
28 - INHERIT requires dest_count 1 or 2
29 - CHANGE_TAG requires dest_count == 1 (dynamic routing from left operand)
30 - SINK requires dest_count == 0 (no output routing)
31 """
32 const_bit = int(has_const)
33 if output == OutputStyle.INHERIT:
34 if dest_count == 1:
35 return 0b000 | const_bit # mode 0 or 1
36 elif dest_count == 2:
37 return 0b010 | const_bit # mode 2 or 3
38 raise ValueError(f"INHERIT requires dest_count 1 or 2, got {dest_count}")
39 elif output == OutputStyle.CHANGE_TAG:
40 if dest_count != 1:
41 raise ValueError(f"CHANGE_TAG requires dest_count == 1, got {dest_count}")
42 return 0b100 | const_bit # mode 4 or 5
43 elif output == OutputStyle.SINK:
44 if dest_count != 0:
45 raise ValueError(f"SINK requires dest_count == 0, got {dest_count}")
46 return 0b110 | const_bit # mode 6 or 7
47 raise ValueError(f"Unknown OutputStyle: {output}")
48
49
50def _decode_mode(mode: int) -> tuple[OutputStyle, bool, int]:
51 """Decode 3-bit mode field into (OutputStyle, has_const, dest_count)."""
52 has_const = bool(mode & 0b001)
53 if not (mode & 0b100):
54 # modes 0-3: INHERIT
55 dest_count = 2 if (mode & 0b010) else 1
56 return OutputStyle.INHERIT, has_const, dest_count
57 elif not (mode & 0b010):
58 # modes 4-5: CHANGE_TAG
59 # dest_count=1 is nominal — CHANGE_TAG reads destination from the left
60 # operand (packed flit 1), not from frame slots. The PE ignores dest_count
61 # for CHANGE_TAG; this value exists only for round-trip consistency.
62 return OutputStyle.CHANGE_TAG, has_const, 1
63 else:
64 # modes 6-7: SINK
65 return OutputStyle.SINK, has_const, 0
66
67
68def _encode_opcode(opcode: ALUOp | MemOp) -> tuple[int, int]:
69 """Return (type_bit, 5-bit opcode).
70
71 Python IntEnum values match hardware encoding directly.
72 MemOp uses type_bit=1 with its own independent 5-bit opcode space.
73 ALUOp (ArithOp, LogicOp, RoutingOp) uses type_bit=0.
74 """
75 if isinstance(opcode, MemOp):
76 return 1, int(opcode) & 0x1F
77 return 0, int(opcode) & 0x1F
78
79
80def _decode_opcode(type_bit: int, raw_opcode: int) -> ALUOp | MemOp:
81 """Decode type_bit + 5-bit opcode into Python enum."""
82 if type_bit:
83 return MemOp(raw_opcode)
84 for cls in (ArithOp, LogicOp, RoutingOp):
85 try:
86 return cls(raw_opcode)
87 except ValueError:
88 continue
89 raise ValueError(f"Unknown ALU opcode: {raw_opcode}")
90
91
92def pack_instruction(inst: Instruction) -> int:
93 """Convert semantic Instruction to 16-bit hardware word.
94
95 Format: [type:1][opcode:5][mode:3][wide:1][fref:6]
96 """
97 type_bit, opcode_bits = _encode_opcode(inst.opcode)
98 mode_bits = _encode_mode(inst.output, inst.has_const, inst.dest_count)
99 wide_bit = int(inst.wide)
100 fref_bits = inst.fref & 0x3F
101 return (type_bit << 15) | (opcode_bits << 10) | (mode_bits << 7) | (wide_bit << 6) | fref_bits
102
103
104def unpack_instruction(word: int) -> Instruction:
105 """Convert 16-bit hardware word to semantic Instruction."""
106 type_bit = (word >> 15) & 1
107 opcode_raw = (word >> 10) & 0x1F
108 mode = (word >> 7) & 0x07
109 wide = bool((word >> 6) & 1)
110 fref = word & 0x3F
111
112 opcode = _decode_opcode(type_bit, opcode_raw)
113 output, has_const, dest_count = _decode_mode(mode)
114 return Instruction(
115 opcode=opcode,
116 output=output,
117 has_const=has_const,
118 dest_count=dest_count,
119 wide=wide,
120 fref=fref,
121 )
122
123
124def pack_flit1(dest: FrameDest) -> int:
125 """Pack structured FrameDest to 16-bit flit 1 value.
126
127 Uses the exact hardware bit layout from architecture-overview.md.
128
129 Format by token_kind:
130 DYADIC: [00][port:1][PE:2][offset:8][act_id:3] = 16 bits
131 MONADIC: [010][PE:2][offset:8][act_id:3] = 16 bits
132 INLINE: [011][PE:2][10][offset:7][spare:2] = 16 bits
133 """
134 if dest.token_kind == TokenKind.DYADIC:
135 # [00][port:1][PE:2][offset:8][act_id:3]
136 return (
137 ((dest.port & 0x1) << 13)
138 | ((dest.target_pe & 0x3) << 11)
139 | ((dest.offset & 0xFF) << 3)
140 | (dest.act_id & 0x7)
141 )
142 elif dest.token_kind == TokenKind.MONADIC:
143 # [010][PE:2][offset:8][act_id:3]
144 return (
145 (0b010 << 13)
146 | ((dest.target_pe & 0x3) << 11)
147 | ((dest.offset & 0xFF) << 3)
148 | (dest.act_id & 0x7)
149 )
150 else:
151 # INLINE: [011][PE:2][10][offset:7][spare:2]
152 return (
153 (0b011 << 13)
154 | ((dest.target_pe & 0x3) << 11)
155 | (0b10 << 9)
156 | ((dest.offset & 0x7F) << 2)
157 )
158
159
160def unpack_flit1(flit1: int) -> FrameDest:
161 """Unpack 16-bit flit 1 value to structured FrameDest."""
162 top2 = (flit1 >> 14) & 0x3
163 if top2 == 0b00:
164 # DYADIC WIDE
165 return FrameDest(
166 target_pe=(flit1 >> 11) & 0x3,
167 offset=(flit1 >> 3) & 0xFF,
168 act_id=flit1 & 0x7,
169 port=Port((flit1 >> 13) & 0x1),
170 token_kind=TokenKind.DYADIC,
171 )
172 elif (flit1 >> 13) == 0b010:
173 # MONADIC NORMAL
174 return FrameDest(
175 target_pe=(flit1 >> 11) & 0x3,
176 offset=(flit1 >> 3) & 0xFF,
177 act_id=flit1 & 0x7,
178 port=Port.L,
179 token_kind=TokenKind.MONADIC,
180 )
181 else:
182 # MONADIC INLINE: [011][PE:2][10][offset:7][spare:2]
183 return FrameDest(
184 target_pe=(flit1 >> 11) & 0x3,
185 offset=(flit1 >> 2) & 0x7F,
186 act_id=0,
187 port=Port.L,
188 token_kind=TokenKind.INLINE,
189 )
190
191
192def flit_count(flit1: int) -> int:
193 """Given flit 1 (the first/header flit), return total flit count for this packet."""
194 if flit1 & 0x8000:
195 # SM token: bit[15]=1. Standard = 2 flits, CAS/EXT = 3.
196 return 2
197 prefix3 = (flit1 >> 13) & 0x7
198 if prefix3 <= 0b001:
199 # Dyadic wide (00x): flit 1 + flit 2 (data)
200 return 2
201 elif prefix3 == 0b010:
202 # Monadic normal: flit 1 + flit 2 (data)
203 return 2
204 elif prefix3 == 0b011:
205 sub = (flit1 >> 9) & 0x3
206 if sub == 0b10:
207 # Monadic inline: 1 flit only (no data flit)
208 return 1
209 else:
210 # Frame control, PE-local write: flit 1 + flit 2
211 return 2
212 return 2
213
214
215def pack_token(token: Token) -> list[int]:
216 """Encode a token as a sequence of 16-bit flits.
217
218 Uses the exact hardware wire format. Flit 1 is the routing/header flit.
219 Used for T0 storage (EXEC reads these back) and future binary output.
220
221 Note: SMToken MemOps are currently limited to 3 bits (values 0-7) in the
222 wire format. Tier 2 MemOps (RD_DEC=8, CMP_SW=9, RAW_READ=10, SET_PAGE=11,
223 WRITE_IMM=12) would be silently truncated and are not yet supported.
224 """
225 if isinstance(token, DyadToken):
226 dest = FrameDest(
227 target_pe=token.target,
228 offset=token.offset,
229 act_id=token.act_id,
230 port=token.port,
231 token_kind=TokenKind.DYADIC,
232 )
233 flit1 = pack_flit1(dest)
234 flit2 = token.data & 0xFFFF
235 return [flit1, flit2]
236 elif isinstance(token, MonadToken):
237 kind = TokenKind.INLINE if token.inline else TokenKind.MONADIC
238 dest = FrameDest(
239 target_pe=token.target,
240 offset=token.offset,
241 act_id=token.act_id,
242 port=Port.L,
243 token_kind=kind,
244 )
245 flit1 = pack_flit1(dest)
246 if token.inline:
247 return [flit1] # monadic inline: 1 flit only
248 flit2 = token.data & 0xFFFF
249 return [flit1, flit2]
250 elif isinstance(token, SMToken):
251 # SM: [1][SM_id:2][op:3][addr:10] (tier 1 layout)
252 # Tier 2 MemOps (values > 7) cannot fit in 3 bits and are not yet supported
253 if int(token.op) > 7:
254 raise ValueError(
255 f"SMToken MemOp {token.op} (value {int(token.op)}) exceeds 3-bit encoding limit. "
256 f"Tier 2 MemOps (RD_DEC=8, CMP_SW=9, RAW_READ=10, SET_PAGE=11, WRITE_IMM=12) "
257 f"are not yet supported in pack_token."
258 )
259 flit1 = (1 << 15) | ((token.target & 0x3) << 13) | ((int(token.op) & 0x7) << 10) | (token.addr & 0x3FF)
260 flit2 = (token.data or 0) & 0xFFFF
261 return [flit1, flit2]
262 raise ValueError(f"Cannot pack token type: {type(token).__name__}")
263
264
265def unpack_token(flits: list[int]) -> Token:
266 """Decode a flit sequence into a Token object.
267
268 Flit 1 (flits[0]) is the header/routing flit. Decodes using hardware format.
269 """
270 flit1 = flits[0]
271
272 if flit1 & 0x8000:
273 # SM token: [1][SM_id:2][op:3][addr:10]
274 sm_id = (flit1 >> 13) & 0x3
275 op = MemOp((flit1 >> 10) & 0x7)
276 addr = flit1 & 0x3FF
277 return SMToken(
278 target=sm_id,
279 addr=addr,
280 op=op,
281 flags=None,
282 data=flits[1] if len(flits) > 1 else 0,
283 ret=None,
284 )
285
286 dest = unpack_flit1(flit1)
287
288 if dest.token_kind == TokenKind.DYADIC:
289 return DyadToken(
290 target=dest.target_pe,
291 offset=dest.offset,
292 act_id=dest.act_id,
293 data=flits[1] if len(flits) > 1 else 0,
294 port=dest.port,
295 )
296 elif dest.token_kind == TokenKind.INLINE:
297 return MonadToken(
298 target=dest.target_pe,
299 offset=dest.offset,
300 act_id=dest.act_id,
301 data=0,
302 inline=True,
303 )
304 else:
305 # MONADIC normal
306 return MonadToken(
307 target=dest.target_pe,
308 offset=dest.offset,
309 act_id=dest.act_id,
310 data=flits[1] if len(flits) > 1 else 0,
311 inline=False,
312 )