OR-1 dataflow CPU sketch
1"""
2Tests for EXEC opcode and IRAMWriteToken bootstrap functionality.
3
4Verifies acceptance criteria:
5- token-migration.AC2.1: IRAMWriteToken routes to target PE via network (isinstance CMToken)
6- token-migration.AC2.4: IRAMWriteToken with invalid target PE raises or is dropped
7- token-migration.AC5.1: EXEC reads Token objects from T0 and injects them
8- token-migration.AC5.2: Injected tokens are processed normally by target PEs/SMs
9- token-migration.AC5.3: EXEC can load a program (IRAM + seed tokens) and execute correctly
10- token-migration.AC5.4: EXEC on empty T0 region is a no-op
11"""
12
13import pytest
14import simpy
15
16from cm_inst import ALUInst, Addr, MemOp, Port, RoutingOp
17from emu import build_topology
18from emu.types import PEConfig, SMConfig
19from sm_mod import Presence
20from tokens import DyadToken, IRAMWriteToken, MonadToken, SMToken
21
22
23class TestAC2_1IRAMWriteTokenRouting:
24 """AC2.1: IRAMWriteToken routes to target PE via network (isinstance CMToken)."""
25
26 def test_iram_write_token_routes_to_target_pe_via_system_inject(self):
27 """IRAMWriteToken is routed to correct target PE when injected via system.inject()."""
28 env = simpy.Environment()
29
30 sys = build_topology(
31 env,
32 [
33 PEConfig(pe_id=0, iram={}),
34 PEConfig(pe_id=1, iram={}),
35 PEConfig(pe_id=2, iram={}),
36 ],
37 [],
38 )
39
40 # Create IRAMWriteToken targeting PE 2
41 inst = ALUInst(
42 op=RoutingOp.CONST,
43 dest_l=Addr(a=0, port=Port.L, pe=0),
44 dest_r=None,
45 const=0x1234,
46 )
47 iram_token = IRAMWriteToken(
48 target=2, # Target PE 2
49 offset=10,
50 ctx=0,
51 data=0,
52 instructions=(inst,),
53 )
54
55 # Inject via system.inject() which appends to PE 2's input_store.items directly
56 sys.inject(iram_token)
57
58 # Verify token arrived at PE 2's input_store (inject appends directly to items)
59 assert len(sys.pes[2].input_store.items) > 0
60 received = sys.pes[2].input_store.items[0]
61 assert isinstance(received, IRAMWriteToken)
62 assert received.target == 2
63 assert received.offset == 10
64
65 # PE 0 and PE 1 should not have received the token
66 assert len(sys.pes[0].input_store.items) == 0
67 assert len(sys.pes[1].input_store.items) == 0
68
69 def test_iram_write_token_multiple_targets(self):
70 """Multiple IRAMWriteTokens can route to different target PEs."""
71 env = simpy.Environment()
72
73 sys = build_topology(
74 env,
75 [
76 PEConfig(pe_id=0, iram={}),
77 PEConfig(pe_id=1, iram={}),
78 ],
79 [],
80 )
81
82 # Create two IRAMWriteTokens targeting different PEs
83 inst0 = ALUInst(op=RoutingOp.CONST, dest_l=None, dest_r=None, const=100)
84 inst1 = ALUInst(op=RoutingOp.CONST, dest_l=None, dest_r=None, const=200)
85
86 token0 = IRAMWriteToken(target=0, offset=0, ctx=0, data=0, instructions=(inst0,))
87 token1 = IRAMWriteToken(target=1, offset=5, ctx=0, data=0, instructions=(inst1,))
88
89 # Inject both tokens
90 sys.inject(token0)
91 sys.inject(token1)
92
93 # Verify routing via direct items inspection
94 assert len(sys.pes[0].input_store.items) == 1
95 assert len(sys.pes[1].input_store.items) == 1
96 assert sys.pes[0].input_store.items[0].offset == 0
97 assert sys.pes[1].input_store.items[0].offset == 5
98
99
100class TestAC2_4IRAMWriteTokenInvalidTarget:
101 """AC2.4: IRAMWriteToken with invalid target PE raises or is dropped."""
102
103 def test_iram_write_token_invalid_target_raises_key_error(self):
104 """IRAMWriteToken with non-existent target PE raises KeyError via system.send()."""
105 env = simpy.Environment()
106
107 # Create topology with only PE 0
108 sys = build_topology(
109 env,
110 [PEConfig(pe_id=0, iram={})],
111 [],
112 )
113
114 # Create IRAMWriteToken targeting non-existent PE 5
115 inst = ALUInst(op=RoutingOp.CONST, dest_l=None, dest_r=None, const=0x5555)
116 iram_token = IRAMWriteToken(
117 target=5, # PE 5 does not exist
118 offset=0,
119 ctx=0,
120 data=0,
121 instructions=(inst,),
122 )
123
124 # Attempting to send should raise KeyError
125 def process_token():
126 yield from sys.send(iram_token)
127
128 with pytest.raises(KeyError):
129 env.process(process_token())
130 env.run(until=100)
131
132
133class TestAC5_1ExecInjectsTokens:
134 """AC5.1: EXEC reads Token objects from T0 and injects them into the network via send()."""
135
136 def test_exec_injects_single_token_to_pe(self):
137 """EXEC at T0 address reads a DyadToken and injects it via send() which triggers SimPy events."""
138 env = simpy.Environment()
139
140 sys = build_topology(
141 env,
142 [
143 PEConfig(pe_id=0, iram={}),
144 PEConfig(pe_id=1, iram={}),
145 ],
146 [SMConfig(sm_id=0, cell_count=512, tier_boundary=256)],
147 )
148
149 # Create a DyadToken to be injected by EXEC
150 seed_token = DyadToken(
151 target=1,
152 offset=0,
153 ctx=0,
154 data=0x4567,
155 port=Port.L,
156 gen=0,
157 wide=False,
158 )
159
160 # Pre-populate T0 with the token
161 sys.sms[0].t0_store.append(seed_token)
162 sys.sms[0].system = sys
163
164 def test_sequence():
165 # SM0 executes EXEC at T0 address 256 (t0_idx=0)
166 exec_token = SMToken(target=0, addr=256, op=MemOp.EXEC, flags=None, data=None, ret=None)
167 yield sys.sms[0].input_store.put(exec_token)
168
169 env.process(test_sequence())
170 env.run(until=100)
171
172 # Verify token was injected via send() - it will be consumed by PE1's process
173 # and stored in matching_store. The key is that send() triggers the get() event.
174 assert sys.pes[1].matching_store[0][0].occupied is True
175 assert sys.pes[1].matching_store[0][0].data == 0x4567
176 assert sys.pes[1].matching_store[0][0].port == Port.L
177
178 def test_exec_injects_multiple_tokens(self):
179 """EXEC at T0 address reads multiple tokens and injects them in order via send().
180
181 Verifies that send() properly wakes up pending get() operations, allowing multiple
182 tokens to be delivered in sequence through SimPy's event system.
183 """
184 env = simpy.Environment()
185
186 sys = build_topology(
187 env,
188 [
189 PEConfig(pe_id=0, iram={}),
190 PEConfig(pe_id=1, iram={}),
191 ],
192 [
193 SMConfig(sm_id=0, cell_count=512, tier_boundary=256),
194 SMConfig(sm_id=1, cell_count=512, tier_boundary=256),
195 ],
196 )
197
198 # Create multiple SMTokens to be injected (write operations don't require IRAM)
199 token1 = SMToken(target=1, addr=100, op=MemOp.WRITE, flags=None, data=0x1111, ret=None)
200 token2 = SMToken(target=1, addr=101, op=MemOp.WRITE, flags=None, data=0x2222, ret=None)
201
202 # Pre-populate T0
203 sys.sms[0].t0_store.extend([token1, token2])
204 sys.sms[0].system = sys
205
206 def test_sequence():
207 exec_token = SMToken(target=0, addr=256, op=MemOp.EXEC, flags=None, data=None, ret=None)
208 yield sys.sms[0].input_store.put(exec_token)
209
210 env.process(test_sequence())
211 env.run(until=100)
212
213 # Verify both tokens were injected via send() and processed by SM1
214 # Both WRITE operations should have updated SM1's cells
215 assert sys.sms[1].cells[100].pres == Presence.FULL
216 assert sys.sms[1].cells[100].data_l == 0x1111
217 assert sys.sms[1].cells[101].pres == Presence.FULL
218 assert sys.sms[1].cells[101].data_l == 0x2222
219
220
221class TestAC5_2ExecTokensProcessedNormally:
222 """AC5.2: Injected tokens are processed normally by target PEs/SMs."""
223
224 def test_injected_dyad_token_received_by_pe(self):
225 """DyadToken injected by EXEC via send() is received and processed by target PE."""
226 env = simpy.Environment()
227
228 sys = build_topology(
229 env,
230 [
231 PEConfig(pe_id=0, iram={}),
232 PEConfig(pe_id=1, iram={}),
233 ],
234 [SMConfig(sm_id=0, cell_count=512, tier_boundary=256)],
235 )
236
237 # Create dyad token to be injected by EXEC
238 token_l = DyadToken(target=1, offset=0, ctx=0, data=0xABCD, port=Port.L, gen=0, wide=False)
239
240 # Pre-populate T0
241 sys.sms[0].t0_store.append(token_l)
242 sys.sms[0].system = sys
243
244 def test_sequence():
245 exec_token = SMToken(target=0, addr=256, op=MemOp.EXEC, flags=None, data=None, ret=None)
246 yield sys.sms[0].input_store.put(exec_token)
247
248 env.process(test_sequence())
249 env.run(until=100)
250
251 # Verify PE1 received and processed the token via matching_store
252 assert sys.pes[1].matching_store[0][0].occupied is True
253 assert sys.pes[1].matching_store[0][0].data == 0xABCD
254 assert sys.pes[1].matching_store[0][0].port == Port.L
255
256
257class TestAC5_3BootstrapProgram:
258 """AC5.3: EXEC can load a program (IRAM writes + seed tokens) from T0 that executes correctly."""
259
260 def test_bootstrap_with_iram_write_and_seed_tokens(self):
261 """Full bootstrap: T0 contains IRAMWriteToken and seed tokens, EXEC loads and runs them.
262
263 Tests the FULL SimPy execution chain:
264 - Populate T0 with IRAMWriteToken + seed DyadToken pair
265 - Send EXEC SMToken to SM
266 - Run env.run()
267 - Assert on pe.output_log containing the expected ALU result
268 """
269 env = simpy.Environment()
270
271 sys = build_topology(
272 env,
273 [
274 PEConfig(pe_id=0, iram={}), # PE0 starts empty, will be loaded by bootstrap
275 PEConfig(pe_id=1, iram={}), # PE1 is output receiver
276 ],
277 [SMConfig(sm_id=0, cell_count=512, tier_boundary=256)],
278 )
279
280 # Create instruction to be loaded: CONST(0xABCD) to PE1
281 const_inst = ALUInst(
282 op=RoutingOp.CONST,
283 dest_l=Addr(a=0, port=Port.L, pe=1),
284 dest_r=None,
285 const=0xABCD,
286 )
287
288 # Create IRAMWriteToken to load instruction at offset 0
289 iram_write = IRAMWriteToken(
290 target=0,
291 offset=0,
292 ctx=0,
293 data=0,
294 instructions=(const_inst,),
295 )
296
297 # Create seed MonadToken to trigger the loaded instruction at PE0
298 seed_token = MonadToken(
299 target=0,
300 offset=0,
301 ctx=0,
302 data=0,
303 inline=False,
304 )
305
306 # Pre-populate T0 with bootstrap sequence
307 sys.sms[0].t0_store.append(iram_write)
308 sys.sms[0].t0_store.append(seed_token)
309 sys.sms[0].system = sys
310
311 def test_sequence():
312 # Trigger EXEC to bootstrap
313 exec_token = SMToken(target=0, addr=256, op=MemOp.EXEC, flags=None, data=None, ret=None)
314 yield sys.sms[0].input_store.put(exec_token)
315
316 env.process(test_sequence())
317 env.run(until=200)
318
319 # Verify FULL SimPy execution chain:
320 # 1. PE0 should have received and processed IRAMWriteToken
321 assert 0 in sys.pes[0].iram, "Instruction not loaded into IRAM by bootstrap"
322 assert sys.pes[0].iram[0].op == RoutingOp.CONST
323 assert sys.pes[0].iram[0].const == 0xABCD
324
325 # 2. PE0 should have received and processed seed token, producing output
326 assert len(sys.pes[0].output_log) > 0, \
327 "PE0 did not produce output; seed token may not have triggered IRAM execution"
328
329 # 3. The output should be routed to PE1 with the CONST value
330 output_to_pe1 = [t for t in sys.pes[0].output_log if t.target == 1]
331 assert len(output_to_pe1) > 0, "PE0 did not route output to PE1"
332 assert output_to_pe1[0].data == 0xABCD, f"Expected output data 0xABCD, got {output_to_pe1[0].data}"
333
334
335class TestAC5_4ExecOnEmptyT0:
336 """AC5.4: EXEC on empty T0 region is a no-op."""
337
338 def test_exec_on_addr_beyond_t0_store_length_is_noop(self):
339 """EXEC at address beyond t0_store length produces no output."""
340 env = simpy.Environment()
341
342 sys = build_topology(
343 env,
344 [PEConfig(pe_id=0, iram={})],
345 [SMConfig(sm_id=0, cell_count=512, tier_boundary=256)],
346 )
347
348 # t0_store is empty, EXEC on T0 address beyond current length
349 def test_sequence():
350 exec_token = SMToken(target=0, addr=300, op=MemOp.EXEC, flags=None, data=None, ret=None)
351 yield sys.sms[0].input_store.put(exec_token)
352
353 env.process(test_sequence())
354 env.run(until=100)
355
356 # Verify no tokens were injected (output stores remain unchanged)
357 assert len(sys.pes[0].input_store.items) == 0
358
359 def test_exec_on_empty_t0_index(self):
360 """EXEC at T0 index with no token is a no-op."""
361 env = simpy.Environment()
362
363 sys = build_topology(
364 env,
365 [PEConfig(pe_id=0, iram={})],
366 [SMConfig(sm_id=0, cell_count=512, tier_boundary=256)],
367 )
368
369 # Pre-populate t0_store with one token
370 sys.sms[0].t0_store.append(MonadToken(target=0, offset=0, ctx=0, data=100, inline=False))
371
372 # EXEC at index 5 which is beyond current t0_store length (1)
373 def test_sequence():
374 exec_token = SMToken(target=0, addr=261, op=MemOp.EXEC, flags=None, data=None, ret=None) # t0_idx = 5
375 yield sys.sms[0].input_store.put(exec_token)
376
377 env.process(test_sequence())
378 env.run(until=100)
379
380 # Verify only the pre-existing token remains in PE0 input_store (no injection happened)
381 # The pre-existing token at index 0 should not be re-injected by EXEC at index 5
382 assert len(sys.pes[0].input_store.items) == 0