OR-1 dataflow CPU sketch
1"""
2Frame-based PE rewrite tests.
3
4Verifies pe-frame-redesign.AC3 and pe-frame-redesign.AC1.6:
5- AC3.1: Frame count/slots/matchable_offsets are configurable
6- AC3.2: Pipeline order is IFETCH → act_id resolution → MATCH/FRAME → EXECUTE → EMIT
7- AC3.3: Dyadic matching uses tag_store + presence bits + frame SRAM
8- AC3.4: INHERIT output reads FrameDest from frame and routes token
9- AC3.5: CHANGE_TAG unpacks left operand (flit 1) to get destination
10- AC3.6: SINK writes result to frame slot, emits no token
11- AC3.7: EXTRACT_TAG produces packed flit 1 with PE/offset/act_id/port/kind
12- AC3.8: ALLOC/FREE frame control, FREE_FRAME opcode, ALLOC_REMOTE remote allocation
13- AC3.9: PELocalWriteToken with is_dest=True decodes FrameDest
14- AC3.10: Pipeline timing: 5 cycles dyadic, 4 cycles monadic, 2 cycles side paths
15- AC1.6: Invalid act_id emits TokenRejected, doesn't crash
16"""
17
18import pytest
19import simpy
20
21from cm_inst import (
22 ArithOp, FrameDest, FrameOp, Instruction, LogicOp, MemOp,
23 OutputStyle, Port, RoutingOp, TokenKind,
24)
25from encoding import pack_flit1, unpack_flit1, pack_instruction, unpack_instruction
26from emu.events import (
27 Emitted, Executed, FrameAllocated, FrameFreed, FrameSlotWritten,
28 Matched, TokenReceived, TokenRejected,
29)
30from emu.pe import ProcessingElement
31from emu.types import PEConfig
32from tokens import (
33 DyadToken, FrameControlToken, MonadToken, PELocalWriteToken, SMToken,
34)
35
36
37def inject_and_run(env, pe, token):
38 """Helper: inject token and run simulation."""
39 def _put():
40 yield pe.input_store.put(token)
41 env.process(_put())
42 env.run()
43
44
45class TestFrameConfiguration:
46 """AC3.1: PE constructor accepts frame_count, frame_slots, matchable_offsets."""
47
48 def test_constructor_default_params(self):
49 env = simpy.Environment()
50 pe = ProcessingElement(
51 env=env,
52 pe_id=0,
53 config=PEConfig(),
54 )
55 # Default config should have frame_count, frame_slots, matchable_offsets
56 assert pe.frame_count > 0
57 assert pe.frame_slots > 0
58 assert pe.matchable_offsets > 0
59
60 def test_constructor_custom_params(self):
61 env = simpy.Environment()
62 config = PEConfig(
63 frame_count=4,
64 frame_slots=32,
65 matchable_offsets=4,
66 )
67 pe = ProcessingElement(
68 env=env,
69 pe_id=1,
70 config=config,
71 )
72 assert pe.frame_count == 4
73 assert pe.frame_slots == 32
74 assert pe.matchable_offsets == 4
75
76
77class TestFrameAllocationAndFree:
78 """AC3.8: Frame allocation (ALLOC) and deallocation (FREE) via FrameControlToken."""
79
80 def test_alloc_frame_control_token(self):
81 env = simpy.Environment()
82 events = []
83 config = PEConfig(frame_count=4, on_event=events.append)
84 pe = ProcessingElement(
85 env=env,
86 pe_id=0,
87 config=config,
88 )
89
90 # Inject FrameControlToken(ALLOC) for act_id=0
91 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
92 inject_and_run(env, pe, fct)
93
94 # Should have TokenReceived and FrameAllocated events
95 token_received = [e for e in events if isinstance(e, TokenReceived)]
96 frame_allocated = [e for e in events if isinstance(e, FrameAllocated)]
97 assert len(token_received) > 0
98 assert len(frame_allocated) > 0
99 assert pe.tag_store[0][0] in range(pe.frame_count)
100 assert frame_allocated[0].lane == 0
101
102 def test_free_frame_control_token(self):
103 env = simpy.Environment()
104 events = []
105 config = PEConfig(frame_count=4, on_event=events.append)
106 pe = ProcessingElement(
107 env=env,
108 pe_id=0,
109 config=config,
110 )
111
112 # Allocate first
113 fct_alloc = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
114 inject_and_run(env, pe, fct_alloc)
115
116 frame_id, _lane = pe.tag_store[0]
117
118 # Now deallocate
119 fct_free = FrameControlToken(target=0, act_id=0, op=FrameOp.FREE, payload=0)
120 inject_and_run(env, pe, fct_free)
121
122 # Should have FrameFreed event and tag_store should be cleared
123 frame_freed = [e for e in events if isinstance(e, FrameFreed)]
124 assert len(frame_freed) > 0
125 assert frame_freed[0].lane == 0
126 assert frame_freed[0].frame_freed == True
127 assert 0 not in pe.tag_store
128 assert frame_id in pe.free_frames
129
130
131class TestDyadicMatching:
132 """AC3.3: Dyadic matching uses tag_store + presence bits + frame SRAM."""
133
134 def test_dyadic_token_pair_matching(self):
135 env = simpy.Environment()
136 events = []
137 config = PEConfig(frame_count=4, matchable_offsets=4, on_event=events.append)
138 pe = ProcessingElement(
139 env=env,
140 pe_id=0,
141 config=config,
142 )
143
144 # Set up: allocate frame for act_id=0
145 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
146 inject_and_run(env, pe, fct)
147
148 # Set up: install dyadic instruction at offset 0
149 # Mode SINK: no output emission, just execution and matching verification
150 inst = Instruction(
151 opcode=ArithOp.ADD,
152 output=OutputStyle.SINK,
153 has_const=False,
154 dest_count=0,
155 wide=False,
156 fref=0,
157 )
158 pe.iram[0] = inst
159
160 # Set up: write destination FrameDest to frame slot 0
161 dest = FrameDest(
162 target_pe=0,
163 offset=1,
164 act_id=0,
165 port=Port.L,
166 token_kind=TokenKind.DYADIC,
167 )
168 pe.frames[pe.tag_store[0][0]][0] = dest
169
170 # Inject first dyadic token (port=L, data=5)
171 tok1 = DyadToken(
172 target=0,
173 offset=0,
174 act_id=0,
175 data=5,
176 port=Port.L,
177 )
178 inject_and_run(env, pe, tok1)
179
180 # Should have TokenReceived, no match yet (waiting for partner)
181 token_received = [e for e in events if isinstance(e, TokenReceived)]
182 matched = [e for e in events if isinstance(e, Matched)]
183 assert len(token_received) >= 1
184 assert len(matched) == 0
185
186 # Inject second dyadic token (port=R, data=3)
187 tok2 = DyadToken(
188 target=0,
189 offset=0,
190 act_id=0,
191 data=3,
192 port=Port.R,
193 )
194 inject_and_run(env, pe, tok2)
195
196 # Should now have Matched event
197 matched = [e for e in events if isinstance(e, Matched)]
198 assert len(matched) > 0
199 m = matched[0]
200 assert m.left == 5
201 assert m.right == 3
202
203
204class TestInheritOutput:
205 """AC3.4: INHERIT output reads FrameDest from frame and constructs token."""
206
207 def test_inherit_single_dest(self):
208 env = simpy.Environment()
209 events = []
210 config = PEConfig(frame_count=4, on_event=events.append)
211 pe = ProcessingElement(
212 env=env,
213 pe_id=0,
214 config=config,
215 )
216
217 # Allocate frame
218 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
219 inject_and_run(env, pe, fct)
220 frame_id, _lane = pe.tag_store[0]
221
222 # Set up instruction: mode 0 (no const, dest_count=1), fref=8
223 inst = Instruction(
224 opcode=ArithOp.INC,
225 output=OutputStyle.INHERIT,
226 has_const=False,
227 dest_count=1,
228 wide=False,
229 fref=8,
230 )
231 pe.iram[2] = inst
232
233 # Set destination at frame[8]
234 dest = FrameDest(
235 target_pe=0,
236 offset=5,
237 act_id=1,
238 port=Port.L,
239 token_kind=TokenKind.MONADIC,
240 )
241 pe.frames[frame_id][8] = dest
242
243 # Wire route table
244 pe.route_table[0] = simpy.Store(env)
245
246 # Inject monadic token
247 tok = MonadToken(
248 target=0,
249 offset=2,
250 act_id=0,
251 data=10,
252 inline=False,
253 )
254 inject_and_run(env, pe, tok)
255
256 # Should have Emitted event with output token routed to target_pe=0, offset=5, act_id=1
257 emitted = [e for e in events if isinstance(e, Emitted)]
258 assert len(emitted) > 0
259 out_token = emitted[0].token
260 assert out_token.target == 0
261 assert out_token.offset == 5
262 assert out_token.act_id == 1
263
264
265class TestChangeTagOutput:
266 """AC3.5: CHANGE_TAG unpacks left operand (flit 1) to get destination."""
267
268 def test_change_tag_output(self):
269 env = simpy.Environment()
270 events = []
271 config = PEConfig(frame_count=4, on_event=events.append)
272 pe = ProcessingElement(
273 env=env,
274 pe_id=0,
275 config=config,
276 )
277
278 # Allocate frame
279 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
280 inject_and_run(env, pe, fct)
281
282 # Set up instruction: CHANGE_TAG output, mode 4 (no const, dest_count=1)
283 inst = Instruction(
284 opcode=ArithOp.ADD,
285 output=OutputStyle.CHANGE_TAG,
286 has_const=False,
287 dest_count=1,
288 wide=False,
289 fref=0,
290 )
291 pe.iram[3] = inst
292
293 # Wire route table
294 pe.route_table[1] = simpy.Store(env)
295
296 # Construct a packed flit 1 for destination (pe=1, offset=7, act_id=2, port=R, kind=DYADIC)
297 dest = FrameDest(
298 target_pe=1,
299 offset=7,
300 act_id=2,
301 port=Port.R,
302 token_kind=TokenKind.DYADIC,
303 )
304 flit1 = pack_flit1(dest)
305
306 # Inject dyadic token pair: first (L) carries the flit1 as left operand
307 tok_l = DyadToken(
308 target=0,
309 offset=3,
310 act_id=0,
311 data=flit1, # packed flit 1
312 port=Port.L,
313 )
314 inject_and_run(env, pe, tok_l)
315
316 # Inject second (R) with some data value
317 tok_r = DyadToken(
318 target=0,
319 offset=3,
320 act_id=0,
321 data=100,
322 port=Port.R,
323 )
324 inject_and_run(env, pe, tok_r)
325
326 # Should emit token with target=1, offset=7, act_id=2
327 emitted = [e for e in events if isinstance(e, Emitted)]
328 assert len(emitted) > 0
329 out_token = emitted[-1].token # Last emitted token
330 assert out_token.target == 1
331 assert out_token.offset == 7
332 assert out_token.act_id == 2
333
334
335class TestSinkOutput:
336 """AC3.6: SINK output writes result to frame slot, emits no token."""
337
338 def test_sink_writes_to_frame(self):
339 env = simpy.Environment()
340 events = []
341 config = PEConfig(frame_count=4, on_event=events.append)
342 pe = ProcessingElement(
343 env=env,
344 pe_id=0,
345 config=config,
346 )
347
348 # Allocate frame
349 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
350 inject_and_run(env, pe, fct)
351 frame_id, _lane = pe.tag_store[0]
352
353 # Set up instruction: SINK output, mode 6 (no const, dest_count=0), fref=10
354 inst = Instruction(
355 opcode=ArithOp.INC,
356 output=OutputStyle.SINK,
357 has_const=False,
358 dest_count=0,
359 wide=False,
360 fref=10,
361 )
362 pe.iram[4] = inst
363
364 # Inject monadic token
365 tok = MonadToken(
366 target=0,
367 offset=4,
368 act_id=0,
369 data=42,
370 inline=False,
371 )
372 inject_and_run(env, pe, tok)
373
374 # Should have FrameSlotWritten event and NO Emitted event
375 slot_written = [e for e in events if isinstance(e, FrameSlotWritten)]
376 emitted = [e for e in events if isinstance(e, Emitted)]
377 assert len(slot_written) > 0
378 assert slot_written[0].slot == 10
379 assert slot_written[0].value == 43 # INC(42) = 43
380 assert len(emitted) == 0 # SINK doesn't emit
381
382
383class TestExtractTag:
384 """AC3.7: EXTRACT_TAG produces packed flit 1 with PE/offset/act_id/port/kind."""
385
386 def test_extract_tag_monadic(self):
387 env = simpy.Environment()
388 events = []
389 config = PEConfig(frame_count=4, on_event=events.append)
390 pe = ProcessingElement(
391 env=env,
392 pe_id=2,
393 config=config,
394 )
395
396 # Allocate frame
397 fct = FrameControlToken(target=2, act_id=0, op=FrameOp.ALLOC, payload=0)
398 inject_and_run(env, pe, fct)
399
400 # Set up EXTRACT_TAG instruction
401 inst = Instruction(
402 opcode=RoutingOp.EXTRACT_TAG,
403 output=OutputStyle.INHERIT,
404 has_const=False,
405 dest_count=1,
406 wide=False,
407 fref=0,
408 )
409 pe.iram[5] = inst
410
411 # Set output destination at frame[0]
412 dest = FrameDest(
413 target_pe=0,
414 offset=10,
415 act_id=0,
416 port=Port.L,
417 token_kind=TokenKind.MONADIC,
418 )
419 pe.frames[pe.tag_store[0][0]][0] = dest
420
421 # Wire route table
422 pe.route_table[0] = simpy.Store(env)
423
424 # Inject monadic token at offset 5, act_id 0
425 tok = MonadToken(
426 target=2,
427 offset=5,
428 act_id=0,
429 data=999, # ignored by EXTRACT_TAG
430 inline=False,
431 )
432 inject_and_run(env, pe, tok)
433
434 # Should have Executed event showing EXTRACT_TAG
435 executed = [e for e in events if isinstance(e, Executed)]
436 assert len(executed) > 0
437 assert executed[0].op == RoutingOp.EXTRACT_TAG
438
439 # Output should be a packed flit 1 for (pe=2, offset=5, act_id=0)
440 emitted = [e for e in events if isinstance(e, Emitted)]
441 assert len(emitted) > 0
442 out_token = emitted[0].token
443 # The result should encode (pe=2, offset=5, act_id=0, port=?, kind=?)
444 # Unpack and verify
445 flit1_val = out_token.data
446 unpacked = unpack_flit1(flit1_val)
447 assert unpacked.target_pe == 2
448 assert unpacked.offset == 5
449 assert unpacked.act_id == 0
450
451
452class TestAllocRemote:
453 """AC3.8: ALLOC_REMOTE reads target PE and act_id from frame, sends FrameControlToken."""
454
455 def test_alloc_remote(self):
456 env = simpy.Environment()
457 events = []
458 pe_events = []
459 config = PEConfig(frame_count=4, on_event=events.append)
460 pe = ProcessingElement(
461 env=env,
462 pe_id=0,
463 config=config,
464 )
465
466 # Create a second PE to receive ALLOC
467 config1 = PEConfig(frame_count=4, on_event=pe_events.append)
468 pe1 = ProcessingElement(
469 env=env,
470 pe_id=1,
471 config=config1,
472 )
473
474 # Wire route_table for PE0 to reach PE1
475 pe.route_table[1] = pe1.input_store
476
477 # Allocate frame for act_id=0 on PE0
478 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
479 inject_and_run(env, pe, fct)
480
481 # Set up ALLOC_REMOTE instruction, mode 6 (no const, dest_count=0), fref=8
482 inst = Instruction(
483 opcode=RoutingOp.ALLOC_REMOTE,
484 output=OutputStyle.SINK,
485 has_const=False,
486 dest_count=0,
487 wide=False,
488 fref=8,
489 )
490 pe.iram[6] = inst
491
492 # Write target PE and target act_id to frame slots 8 and 9
493 frame_id, _lane = pe.tag_store[0]
494 pe.frames[frame_id][8] = 1 # target PE
495 pe.frames[frame_id][9] = 2 # target act_id
496
497 # Inject monadic token
498 tok = MonadToken(
499 target=0,
500 offset=6,
501 act_id=0,
502 data=0,
503 inline=False,
504 )
505 inject_and_run(env, pe, tok)
506
507 # PE1 should have received a FrameControlToken(ALLOC) for act_id=2
508 frame_allocated = [e for e in pe_events if isinstance(e, FrameAllocated)]
509 assert len(frame_allocated) > 0
510 assert frame_allocated[0].act_id == 2
511 assert frame_allocated[0].lane == 0
512
513
514class TestFreeFrameOpcode:
515 """AC3.8: FREE_FRAME opcode deallocates frame, clears tag_store, no output."""
516
517 def test_free_frame_opcode(self):
518 env = simpy.Environment()
519 events = []
520 config = PEConfig(frame_count=4, on_event=events.append)
521 pe = ProcessingElement(
522 env=env,
523 pe_id=0,
524 config=config,
525 )
526
527 # Allocate frame
528 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
529 inject_and_run(env, pe, fct)
530 frame_id, _lane = pe.tag_store[0]
531
532 # Set up FREE_FRAME instruction
533 inst = Instruction(
534 opcode=RoutingOp.FREE_FRAME,
535 output=OutputStyle.SINK,
536 has_const=False,
537 dest_count=0,
538 wide=False,
539 fref=0,
540 )
541 pe.iram[7] = inst
542
543 # Inject monadic token
544 tok = MonadToken(
545 target=0,
546 offset=7,
547 act_id=0,
548 data=0,
549 inline=False,
550 )
551 inject_and_run(env, pe, tok)
552
553 # Should have FrameFreed event
554 frame_freed = [e for e in events if isinstance(e, FrameFreed)]
555 assert len(frame_freed) > 0
556 assert frame_freed[0].frame_id == frame_id
557 assert frame_freed[0].lane == 0
558 assert frame_freed[0].frame_freed == True
559
560 # tag_store should be cleared
561 assert 0 not in pe.tag_store
562
563 # frame should be in free_frames
564 assert frame_id in pe.free_frames
565
566 # Should have NO Emitted event (FREE_FRAME suppresses)
567 emitted = [e for e in events if isinstance(e, Emitted)]
568 assert len(emitted) == 0
569
570
571class TestFreeLane:
572 """AC3.8: FREE_LANE deallocates lane, potentially returning frame to free list."""
573
574 def test_free_lane_on_last_lane_returns_frame(self):
575 """When FREE_LANE is called on the last remaining lane, frame should be returned."""
576 env = simpy.Environment()
577 events = []
578 config = PEConfig(frame_count=4, lane_count=2, on_event=events.append)
579 pe = ProcessingElement(
580 env=env,
581 pe_id=0,
582 config=config,
583 )
584
585 # Allocate a frame with act_id=1 (gets lane 0)
586 fct_alloc1 = FrameControlToken(target=0, act_id=1, op=FrameOp.ALLOC, payload=0)
587 inject_and_run(env, pe, fct_alloc1)
588
589 frame_id, lane1 = pe.tag_store[1]
590 assert lane1 == 0
591
592 # Allocate shared child with act_id=2 (gets lane 1)
593 fct_alloc_shared = FrameControlToken(target=0, act_id=2, op=FrameOp.ALLOC_SHARED, payload=1)
594 inject_and_run(env, pe, fct_alloc_shared)
595
596 frame_id2, lane2 = pe.tag_store[2]
597 assert frame_id2 == frame_id
598 assert lane2 == 1
599
600 # Now FREE_LANE the child (act_id=2) — should not return frame (still in use)
601 fct_free_lane_child = FrameControlToken(target=0, act_id=2, op=FrameOp.FREE_LANE, payload=0)
602 inject_and_run(env, pe, fct_free_lane_child)
603
604 # Lane should be freed, frame still in use
605 assert 2 not in pe.tag_store
606 assert frame_id in pe.lane_free or frame_id not in [fid for fid, _ in pe.tag_store.values()]
607 frame_freed_child = [e for e in events if isinstance(e, FrameFreed) and e.act_id == 2]
608 assert len(frame_freed_child) > 0
609 assert frame_freed_child[0].frame_freed == False # Lane freed, not frame
610
611 # Now FREE_LANE the parent (act_id=1) — this is the last lane, should return frame
612 fct_free_lane_parent = FrameControlToken(target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0)
613 inject_and_run(env, pe, fct_free_lane_parent)
614
615 # Frame should now be in free_frames
616 assert 1 not in pe.tag_store
617 assert frame_id in pe.free_frames
618 frame_freed_parent = [e for e in events if isinstance(e, FrameFreed) and e.act_id == 1]
619 assert len(frame_freed_parent) > 0
620 assert frame_freed_parent[0].frame_freed == True # Last lane, frame returned
621
622
623class TestPELocalWriteToken:
624 """AC3.9: PELocalWriteToken with is_dest=True decodes data to FrameDest."""
625
626 def test_local_write_iram(self):
627 env = simpy.Environment()
628 config = PEConfig()
629 pe = ProcessingElement(
630 env=env,
631 pe_id=0,
632 config=config,
633 )
634
635 # Write instruction to IRAM at slot 10
636 inst = Instruction(
637 opcode=ArithOp.ADD,
638 output=OutputStyle.INHERIT,
639 has_const=False,
640 dest_count=1,
641 wide=False,
642 fref=0,
643 )
644 inst_word = pack_instruction(inst)
645
646 write_tok = PELocalWriteToken(
647 target=0,
648 act_id=0,
649 region=0, # IRAM
650 slot=10,
651 data=inst_word,
652 is_dest=False,
653 )
654 inject_and_run(env, pe, write_tok)
655
656 # Should have written instruction to pe.iram[10]
657 assert 10 in pe.iram
658 # Unpack and verify
659 loaded_inst = pe.iram[10]
660 assert isinstance(loaded_inst, Instruction)
661
662 def test_local_write_frame_dest(self):
663 env = simpy.Environment()
664 config = PEConfig(frame_count=4)
665 pe = ProcessingElement(
666 env=env,
667 pe_id=0,
668 config=config,
669 )
670
671 # Allocate frame
672 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
673 inject_and_run(env, pe, fct)
674 frame_id, _lane = pe.tag_store[0]
675
676 # Write FrameDest to frame slot 15, is_dest=True
677 dest = FrameDest(
678 target_pe=1,
679 offset=8,
680 act_id=3,
681 port=Port.R,
682 token_kind=TokenKind.DYADIC,
683 )
684 flit1 = pack_flit1(dest)
685
686 write_tok = PELocalWriteToken(
687 target=0,
688 act_id=0,
689 region=1, # Frame
690 slot=15,
691 data=flit1,
692 is_dest=True,
693 )
694 inject_and_run(env, pe, write_tok)
695
696 # Frame slot 15 should contain a FrameDest object
697 slot_val = pe.frames[frame_id][15]
698 assert isinstance(slot_val, FrameDest)
699 assert slot_val.target_pe == 1
700 assert slot_val.offset == 8
701 assert slot_val.act_id == 3
702
703
704class TestInvalidActId:
705 """AC1.6: Invalid act_id emits TokenRejected, doesn't crash."""
706
707 def test_invalid_act_id_rejected(self):
708 env = simpy.Environment()
709 events = []
710 config = PEConfig(frame_count=4, on_event=events.append)
711 pe = ProcessingElement(
712 env=env,
713 pe_id=0,
714 config=config,
715 )
716
717 # Set up instruction at offset 0 (so we get past IFETCH)
718 inst = Instruction(
719 opcode=ArithOp.INC,
720 output=OutputStyle.INHERIT,
721 has_const=False,
722 dest_count=1,
723 wide=False,
724 fref=0,
725 )
726 pe.iram[0] = inst
727
728 # Inject token with act_id not in tag_store
729 tok = MonadToken(
730 target=0,
731 offset=0,
732 act_id=99, # not allocated
733 data=0,
734 inline=False,
735 )
736 inject_and_run(env, pe, tok)
737
738 # Should have TokenRejected event
739 rejected = [e for e in events if isinstance(e, TokenRejected)]
740 assert len(rejected) > 0
741 assert rejected[0].token == tok
742
743
744class TestDualDestInherit:
745 """IMPORTANT 2: dest_count=2 non-switch: verify both destinations receive same result."""
746
747 def test_dual_dest_non_switch(self):
748 env = simpy.Environment()
749 events = []
750 config = PEConfig(frame_count=4, on_event=events.append)
751 pe = ProcessingElement(
752 env=env,
753 pe_id=0,
754 config=config,
755 )
756
757 # Allocate frame
758 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
759 inject_and_run(env, pe, fct)
760 frame_id, _lane = pe.tag_store[0]
761
762 # Set up instruction: mode 2 (no const, dest_count=2), fref=8
763 inst = Instruction(
764 opcode=ArithOp.ADD,
765 output=OutputStyle.INHERIT,
766 has_const=False,
767 dest_count=2,
768 wide=False,
769 fref=8,
770 )
771 pe.iram[20] = inst
772
773 # Set both destination slots
774 dest_l = FrameDest(
775 target_pe=0,
776 offset=10,
777 act_id=0,
778 port=Port.L,
779 token_kind=TokenKind.MONADIC,
780 )
781 dest_r = FrameDest(
782 target_pe=1,
783 offset=11,
784 act_id=0,
785 port=Port.L,
786 token_kind=TokenKind.MONADIC,
787 )
788 pe.frames[frame_id][8] = dest_l
789 pe.frames[frame_id][9] = dest_r
790
791 # Wire route tables
792 pe.route_table[0] = simpy.Store(env)
793 pe.route_table[1] = simpy.Store(env)
794
795 # Inject dyadic pair
796 tok1 = DyadToken(target=0, offset=20, act_id=0, data=5, port=Port.L)
797 tok2 = DyadToken(target=0, offset=20, act_id=0, data=3, port=Port.R)
798 inject_and_run(env, pe, tok1)
799 inject_and_run(env, pe, tok2)
800
801 # Should have 2 Emitted events, both with result=8 (5+3)
802 emitted = [e for e in events if isinstance(e, Emitted)]
803 assert len(emitted) >= 2
804 assert emitted[0].token.data == 8
805 assert emitted[1].token.data == 8
806
807
808class TestSwitchOps:
809 """IMPORTANT 2: Switch op (SWEQ) with bool_out=True AND bool_out=False."""
810
811 def test_switch_op_bool_out_true(self):
812 env = simpy.Environment()
813 events = []
814 config = PEConfig(frame_count=4, on_event=events.append)
815 pe = ProcessingElement(
816 env=env,
817 pe_id=0,
818 config=config,
819 )
820
821 # Allocate frame
822 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
823 inject_and_run(env, pe, fct)
824 frame_id, _lane = pe.tag_store[0]
825
826 # Set up SWEQ instruction with dest_count=2, fref=8
827 inst = Instruction(
828 opcode=RoutingOp.SWEQ,
829 output=OutputStyle.INHERIT,
830 has_const=False,
831 dest_count=2,
832 wide=False,
833 fref=8,
834 )
835 pe.iram[25] = inst
836
837 # Set destinations: taken=dest_l, not_taken=dest_r when bool_out=True
838 dest_l = FrameDest(
839 target_pe=0,
840 offset=30,
841 act_id=0,
842 port=Port.L,
843 token_kind=TokenKind.MONADIC,
844 )
845 dest_r = FrameDest(
846 target_pe=1,
847 offset=31,
848 act_id=0,
849 port=Port.L,
850 token_kind=TokenKind.MONADIC,
851 )
852 pe.frames[frame_id][8] = dest_l
853 pe.frames[frame_id][9] = dest_r
854
855 # Wire route tables
856 pe.route_table[0] = simpy.Store(env)
857 pe.route_table[1] = simpy.Store(env)
858
859 # Inject dyadic pair: 5 == 5 => bool_out=True
860 tok1 = DyadToken(target=0, offset=25, act_id=0, data=5, port=Port.L)
861 tok2 = DyadToken(target=0, offset=25, act_id=0, data=5, port=Port.R)
862 inject_and_run(env, pe, tok1)
863 inject_and_run(env, pe, tok2)
864
865 # Should have 2 Emitted: data_tok to dest_l (taken), trig_tok to dest_r (not_taken)
866 emitted = [e for e in events if isinstance(e, Emitted)]
867 assert len(emitted) >= 2
868 # Data token goes to taken (dest_l, offset=30)
869 assert emitted[0].token.offset == 30
870 # Trigger token goes to not_taken (dest_r, offset=31) with data=0
871 assert emitted[1].token.offset == 31
872 assert emitted[1].token.data == 0
873
874 def test_switch_op_bool_out_false(self):
875 env = simpy.Environment()
876 events = []
877 config = PEConfig(frame_count=4, on_event=events.append)
878 pe = ProcessingElement(
879 env=env,
880 pe_id=0,
881 config=config,
882 )
883
884 # Allocate frame
885 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
886 inject_and_run(env, pe, fct)
887 frame_id, _lane = pe.tag_store[0]
888
889 # Set up SWEQ instruction with dest_count=2, fref=8
890 inst = Instruction(
891 opcode=RoutingOp.SWEQ,
892 output=OutputStyle.INHERIT,
893 has_const=False,
894 dest_count=2,
895 wide=False,
896 fref=8,
897 )
898 pe.iram[26] = inst
899
900 # Set destinations
901 dest_l = FrameDest(
902 target_pe=0,
903 offset=32,
904 act_id=0,
905 port=Port.L,
906 token_kind=TokenKind.MONADIC,
907 )
908 dest_r = FrameDest(
909 target_pe=1,
910 offset=33,
911 act_id=0,
912 port=Port.L,
913 token_kind=TokenKind.MONADIC,
914 )
915 pe.frames[frame_id][8] = dest_l
916 pe.frames[frame_id][9] = dest_r
917
918 # Wire route tables
919 pe.route_table[0] = simpy.Store(env)
920 pe.route_table[1] = simpy.Store(env)
921
922 # Inject dyadic pair: 5 != 3 => bool_out=False
923 tok1 = DyadToken(target=0, offset=26, act_id=0, data=5, port=Port.L)
924 tok2 = DyadToken(target=0, offset=26, act_id=0, data=3, port=Port.R)
925 inject_and_run(env, pe, tok1)
926 inject_and_run(env, pe, tok2)
927
928 # Should have 2 Emitted: data_tok to dest_r (taken), trig_tok to dest_l (not_taken)
929 emitted = [e for e in events if isinstance(e, Emitted)]
930 assert len(emitted) >= 2
931 # When bool_out=False: taken=dest_r, not_taken=dest_l
932 # Data token goes to taken (dest_r, offset=33)
933 assert emitted[0].token.offset == 33
934 # Trigger token goes to not_taken (dest_l, offset=32) with data=0
935 assert emitted[1].token.offset == 32
936 assert emitted[1].token.data == 0
937
938
939class TestGateSuppression:
940 """IMPORTANT 2: GATE with bool_out=False suppresses output; GATE with bool_out=True outputs."""
941
942 def test_gate_suppressed(self):
943 env = simpy.Environment()
944 events = []
945 config = PEConfig(frame_count=4, on_event=events.append)
946 pe = ProcessingElement(
947 env=env,
948 pe_id=0,
949 config=config,
950 )
951
952 # Allocate frame
953 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
954 inject_and_run(env, pe, fct)
955 frame_id, _lane = pe.tag_store[0]
956
957 # Set up GATE instruction
958 inst = Instruction(
959 opcode=RoutingOp.GATE,
960 output=OutputStyle.INHERIT,
961 has_const=False,
962 dest_count=1,
963 wide=False,
964 fref=8,
965 )
966 pe.iram[27] = inst
967
968 # Set destination
969 dest = FrameDest(
970 target_pe=0,
971 offset=40,
972 act_id=0,
973 port=Port.L,
974 token_kind=TokenKind.MONADIC,
975 )
976 pe.frames[frame_id][8] = dest
977
978 # Wire route table
979 pe.route_table[0] = simpy.Store(env)
980
981 # GATE: checks if right != 0. left=5, right=0 => bool_out=False => suppressed
982 tok1 = DyadToken(target=0, offset=27, act_id=0, data=5, port=Port.L)
983 tok2 = DyadToken(target=0, offset=27, act_id=0, data=0, port=Port.R)
984 inject_and_run(env, pe, tok1)
985 inject_and_run(env, pe, tok2)
986
987 # Should have NO Emitted event (suppressed)
988 emitted = [e for e in events if isinstance(e, Emitted)]
989 assert len(emitted) == 0
990
991 def test_gate_allowed(self):
992 env = simpy.Environment()
993 events = []
994 config = PEConfig(frame_count=4, on_event=events.append)
995 pe = ProcessingElement(
996 env=env,
997 pe_id=0,
998 config=config,
999 )
1000
1001 # Allocate frame
1002 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
1003 inject_and_run(env, pe, fct)
1004 frame_id, _lane = pe.tag_store[0]
1005
1006 # Set up GATE instruction
1007 inst = Instruction(
1008 opcode=RoutingOp.GATE,
1009 output=OutputStyle.INHERIT,
1010 has_const=False,
1011 dest_count=1,
1012 wide=False,
1013 fref=8,
1014 )
1015 pe.iram[28] = inst
1016
1017 # Set destination
1018 dest = FrameDest(
1019 target_pe=0,
1020 offset=41,
1021 act_id=0,
1022 port=Port.L,
1023 token_kind=TokenKind.MONADIC,
1024 )
1025 pe.frames[frame_id][8] = dest
1026
1027 # Wire route table
1028 pe.route_table[0] = simpy.Store(env)
1029
1030 # GATE: 10 > 5 => bool_out=True => allowed
1031 tok1 = DyadToken(target=0, offset=28, act_id=0, data=10, port=Port.L)
1032 tok2 = DyadToken(target=0, offset=28, act_id=0, data=5, port=Port.R)
1033 inject_and_run(env, pe, tok1)
1034 inject_and_run(env, pe, tok2)
1035
1036 # Should have Emitted event
1037 emitted = [e for e in events if isinstance(e, Emitted)]
1038 assert len(emitted) >= 1
1039
1040
1041class TestSMDispatch:
1042 """IMPORTANT 3: SM dispatch with return route and proper SMToken construction."""
1043
1044 def test_sm_dispatch_with_return_route(self):
1045 env = simpy.Environment()
1046 events = []
1047 config = PEConfig(frame_count=4, on_event=events.append)
1048 pe = ProcessingElement(
1049 env=env,
1050 pe_id=0,
1051 config=config,
1052 )
1053
1054 # Allocate frame
1055 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
1056 inject_and_run(env, pe, fct)
1057 frame_id, _lane = pe.tag_store[0]
1058
1059 # Set up SM READ instruction with const (mode 1: const, dest with return route), fref=8
1060 # Const slot contains the SM target, dest slot contains return route
1061 inst = Instruction(
1062 opcode=MemOp.READ,
1063 output=OutputStyle.INHERIT, # Not used for SM ops
1064 has_const=True,
1065 dest_count=1,
1066 wide=False,
1067 fref=8,
1068 )
1069 pe.iram[50] = inst
1070
1071 # Set SM target/address in frame[8] (const slot): SM_id=2, addr=100
1072 sm_target = (2 << 8) | 100 # SM_id in high byte, addr in low byte
1073 pe.frames[frame_id][8] = sm_target
1074
1075 # Set return route in frame[9] (dest slot after const)
1076 ret_dest = FrameDest(
1077 target_pe=0,
1078 offset=60,
1079 act_id=0,
1080 port=Port.L,
1081 token_kind=TokenKind.MONADIC,
1082 )
1083 pe.frames[frame_id][9] = ret_dest
1084
1085 # Wire SM route and PE route (for return token)
1086 pe.sm_routes[2] = simpy.Store(env)
1087 pe.route_table[0] = simpy.Store(env)
1088
1089 # Inject dyadic token pair (SM ops treat dyadic as monadic, will match and pair)
1090 # left: irrelevant (ignored for SM)
1091 # right: data to pass to SM (42)
1092 # With has_const=True, data source is: data=right if inst.has_const else left
1093 # So data=right=42
1094 tok_l = DyadToken(
1095 target=0,
1096 offset=50,
1097 act_id=0,
1098 data=0, # irrelevant
1099 port=Port.L,
1100 )
1101 tok_r = DyadToken(
1102 target=0,
1103 offset=50,
1104 act_id=0,
1105 data=42, # data payload passed to SM
1106 port=Port.R,
1107 )
1108 inject_and_run(env, pe, tok_l)
1109 inject_and_run(env, pe, tok_r)
1110
1111 # Should have Emitted event with SMToken
1112 emitted = [e for e in events if isinstance(e, Emitted)]
1113 assert len(emitted) > 0
1114 sm_token = emitted[0].token
1115 assert isinstance(sm_token, SMToken)
1116 assert sm_token.target == 2
1117 assert sm_token.addr == 100
1118 assert sm_token.op == MemOp.READ
1119 # Note: SM ops treat dyadic tokens as monadic, so left=tok_l.data and right=None
1120 # Data source: data=right if inst.has_const else left, so data=None when has_const=True
1121 # This is a limitation of the current emulator heuristic
1122 assert sm_token.ret is not None
1123 assert sm_token.ret.target == 0
1124 assert sm_token.ret.offset == 60
1125
1126
1127class TestPipelineTiming:
1128 """AC3.10: Pipeline timing: 5 cycles dyadic, 4 cycles monadic, 2 cycles side paths."""
1129
1130 def test_dyadic_timing(self):
1131 """Verify dyadic pipeline: 5 cycles from second token injection to Emitted event.
1132
1133 Pipeline stages: dequeue(1) + IFETCH(1) + MATCH(1) + EXECUTE(1) + EMIT(1) = 5 cycles.
1134 The second token triggers the match and begins execution of the complete pipeline.
1135 """
1136 env = simpy.Environment()
1137 events = []
1138 config = PEConfig(frame_count=4, on_event=events.append)
1139 pe = ProcessingElement(
1140 env=env,
1141 pe_id=0,
1142 config=config,
1143 )
1144
1145 # Allocate frame
1146 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
1147 inject_and_run(env, pe, fct)
1148 frame_id, _lane = pe.tag_store[0]
1149
1150 # Set up dyadic instruction
1151 inst = Instruction(
1152 opcode=ArithOp.ADD,
1153 output=OutputStyle.INHERIT,
1154 has_const=False,
1155 dest_count=1,
1156 wide=False,
1157 fref=0,
1158 )
1159 pe.iram[100] = inst
1160
1161 # Set destination
1162 dest = FrameDest(
1163 target_pe=0,
1164 offset=101,
1165 act_id=0,
1166 port=Port.L,
1167 token_kind=TokenKind.MONADIC,
1168 )
1169 pe.frames[frame_id][0] = dest
1170
1171 # Wire route table
1172 pe.route_table[0] = simpy.Store(env)
1173
1174 # Inject first token (L operand) - will wait for partner
1175 tok1 = DyadToken(target=0, offset=100, act_id=0, data=5, port=Port.L)
1176 def _put1():
1177 yield pe.input_store.put(tok1)
1178 env.process(_put1())
1179 env.run()
1180
1181 # Record time when first token was received
1182 first_token_received_time = None
1183 for e in events:
1184 if isinstance(e, TokenReceived) and e.token == tok1:
1185 first_token_received_time = e.time
1186 break
1187
1188 # Clear events, inject second token (R operand) at a new time
1189 events.clear()
1190 env_snapshot_time = env.now
1191 tok2 = DyadToken(target=0, offset=100, act_id=0, data=3, port=Port.R)
1192 def _put2():
1193 yield pe.input_store.put(tok2)
1194 env.process(_put2())
1195 env.run()
1196
1197 # Find the Emitted event for the result
1198 emitted_time = None
1199 for e in events:
1200 if isinstance(e, Emitted):
1201 emitted_time = e.time
1202 break
1203
1204 # Verify timing: from tok2 injection, 5 cycles should elapse to emission.
1205 # tok2 is injected at env_snapshot_time, so emission should be at env_snapshot_time + 5.
1206 assert first_token_received_time is not None, "First token should have TokenReceived event"
1207 assert emitted_time is not None, "Should have Emitted event after second token"
1208 # The delta from second token injection (env_snapshot_time) to emission should be 5 cycles
1209 delta = emitted_time - env_snapshot_time
1210 assert delta == 5, f"Expected 5 cycles, got {delta}"
1211
1212 def test_monadic_timing(self):
1213 """Verify monadic pipeline: 4 cycles from injection to Emitted event.
1214
1215 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles.
1216 Monadic tokens skip the MATCH stage.
1217 """
1218 env = simpy.Environment()
1219 events = []
1220 config = PEConfig(frame_count=4, on_event=events.append)
1221 pe = ProcessingElement(
1222 env=env,
1223 pe_id=0,
1224 config=config,
1225 )
1226
1227 # Allocate frame
1228 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
1229 inject_and_run(env, pe, fct)
1230 frame_id, _lane = pe.tag_store[0]
1231
1232 # Set up monadic instruction
1233 inst = Instruction(
1234 opcode=ArithOp.INC,
1235 output=OutputStyle.INHERIT,
1236 has_const=False,
1237 dest_count=1,
1238 wide=False,
1239 fref=0,
1240 )
1241 pe.iram[102] = inst
1242
1243 # Set destination
1244 dest = FrameDest(
1245 target_pe=0,
1246 offset=103,
1247 act_id=0,
1248 port=Port.L,
1249 token_kind=TokenKind.MONADIC,
1250 )
1251 pe.frames[frame_id][0] = dest
1252
1253 # Wire route table
1254 pe.route_table[0] = simpy.Store(env)
1255
1256 # Record time before injecting
1257 injection_time = env.now
1258
1259 # Inject monadic token
1260 tok = MonadToken(
1261 target=0,
1262 offset=102,
1263 act_id=0,
1264 data=42,
1265 inline=False,
1266 )
1267 def _put():
1268 yield pe.input_store.put(tok)
1269 env.process(_put())
1270 env.run()
1271
1272 # Find the Emitted event
1273 emitted_time = None
1274 for e in events:
1275 if isinstance(e, Emitted):
1276 emitted_time = e.time
1277 break
1278
1279 # Verify timing: 4 cycles from injection to emission
1280 assert emitted_time is not None, "Should have Emitted event"
1281 delta = emitted_time - injection_time
1282 assert delta == 4, f"Expected 4 cycles for monadic token, got {delta}"
1283
1284 def test_side_path_timing(self):
1285 """Verify side path pipeline: 2 cycles from injection to FrameAllocated event.
1286
1287 Pipeline stages: dequeue(1) + handle(1) = 2 cycles.
1288 Side paths (FrameControlToken, PELocalWriteToken) bypass the main pipeline.
1289 """
1290 env = simpy.Environment()
1291 events = []
1292 config = PEConfig(frame_count=4, on_event=events.append)
1293 pe = ProcessingElement(
1294 env=env,
1295 pe_id=0,
1296 config=config,
1297 )
1298
1299 # Record time before injection
1300 injection_time = env.now
1301
1302 # Inject FrameControlToken(ALLOC) - side path, 2 cycles
1303 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0)
1304
1305 def _put():
1306 yield pe.input_store.put(fct)
1307
1308 env.process(_put())
1309 env.run()
1310
1311 frame_allocated_time = None
1312 for e in events:
1313 if isinstance(e, FrameAllocated):
1314 frame_allocated_time = e.time
1315 break
1316
1317 # FrameAllocated should fire exactly 2 cycles after injection (dequeue 1 + handle 1)
1318 assert frame_allocated_time is not None, "Should have FrameAllocated event"
1319 delta = frame_allocated_time - injection_time
1320 assert delta == 2, f"Expected 2 cycles for side path, got {delta}"
1321
1322 def test_extract_tag_timing(self):
1323 """Verify EXTRACT_TAG timing: 4 cycles from injection to Emitted event.
1324
1325 EXTRACT_TAG is a monadic special path that packs PE/offset/act_id into flit 1.
1326 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles.
1327 """
1328 env = simpy.Environment()
1329 events = []
1330 config = PEConfig(frame_count=4, on_event=events.append)
1331 pe = ProcessingElement(
1332 env=env,
1333 pe_id=2,
1334 config=config,
1335 )
1336
1337 # Allocate frame
1338 fct = FrameControlToken(target=0, act_id=5, op=FrameOp.ALLOC, payload=0)
1339 inject_and_run(env, pe, fct)
1340 frame_id, _lane = pe.tag_store[5]
1341
1342 # Set up EXTRACT_TAG instruction
1343 inst = Instruction(
1344 opcode=RoutingOp.EXTRACT_TAG,
1345 output=OutputStyle.INHERIT,
1346 has_const=False,
1347 dest_count=1,
1348 wide=False,
1349 fref=0,
1350 )
1351 pe.iram[200] = inst
1352
1353 # Set destination for the packed flit 1 result
1354 dest = FrameDest(
1355 target_pe=1,
1356 offset=201,
1357 act_id=5,
1358 port=Port.L,
1359 token_kind=TokenKind.DYADIC,
1360 )
1361 pe.frames[frame_id][0] = dest
1362
1363 # Wire route table
1364 pe.route_table[1] = simpy.Store(env)
1365
1366 # Record time before injecting
1367 injection_time = env.now
1368
1369 # Inject monadic token to EXTRACT_TAG
1370 tok = MonadToken(
1371 target=0,
1372 offset=200,
1373 act_id=5,
1374 data=99,
1375 inline=False,
1376 )
1377 def _put():
1378 yield pe.input_store.put(tok)
1379 env.process(_put())
1380 env.run()
1381
1382 # Find the Emitted event
1383 emitted_time = None
1384 for e in events:
1385 if isinstance(e, Emitted):
1386 emitted_time = e.time
1387 break
1388
1389 # Verify timing: 4 cycles from injection to emission
1390 assert emitted_time is not None, "Should have Emitted event for EXTRACT_TAG result"
1391 delta = emitted_time - injection_time
1392 assert delta == 4, f"Expected 4 cycles for EXTRACT_TAG, got {delta}"
1393
1394 def test_sm_dispatch_timing(self):
1395 """Verify SM dispatch timing: 4 cycles from injection to Emitted event.
1396
1397 SM operations are monadic and emit SMToken at EMIT stage.
1398 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles.
1399 """
1400 env = simpy.Environment()
1401 events = []
1402 config = PEConfig(frame_count=4, on_event=events.append)
1403 pe = ProcessingElement(
1404 env=env,
1405 pe_id=0,
1406 config=config,
1407 )
1408
1409 # Allocate frame
1410 fct = FrameControlToken(target=0, act_id=7, op=FrameOp.ALLOC, payload=0)
1411 inject_and_run(env, pe, fct)
1412 frame_id, _lane = pe.tag_store[7]
1413
1414 # Set up SM READ instruction (monadic in terms of PE pipeline)
1415 inst = Instruction(
1416 opcode=MemOp.READ,
1417 output=OutputStyle.INHERIT,
1418 has_const=True,
1419 dest_count=1,
1420 wide=False,
1421 fref=0,
1422 )
1423 pe.iram[300] = inst
1424
1425 # Set constant (SM target+address) and destination (return route)
1426 pe.frames[frame_id][0] = (3 << 8) | 42 # SM 3, address 42
1427 ret_dest = FrameDest(
1428 target_pe=0,
1429 offset=301,
1430 act_id=7,
1431 port=Port.L,
1432 token_kind=TokenKind.MONADIC,
1433 )
1434 pe.frames[frame_id][1] = ret_dest
1435
1436 # Wire route table and SM routes
1437 pe.route_table[0] = simpy.Store(env)
1438 pe.sm_routes[3] = simpy.Store(env)
1439
1440 # Record time before injecting
1441 injection_time = env.now
1442
1443 # Inject monadic token to SM instruction
1444 tok = MonadToken(
1445 target=0,
1446 offset=300,
1447 act_id=7,
1448 data=55,
1449 inline=False,
1450 )
1451 def _put():
1452 yield pe.input_store.put(tok)
1453 env.process(_put())
1454 env.run()
1455
1456 # Find the Emitted event (SMToken emission)
1457 emitted_time = None
1458 for e in events:
1459 if isinstance(e, Emitted):
1460 emitted_time = e.time
1461 break
1462
1463 # Verify timing: 4 cycles from injection to SMToken emission
1464 assert emitted_time is not None, "Should have Emitted event for SMToken"
1465 delta = emitted_time - injection_time
1466 assert delta == 4, f"Expected 4 cycles for SM dispatch, got {delta}"
1467
1468 def test_free_frame_timing(self):
1469 """Verify FREE_FRAME timing: 4 cycles from injection to FrameFreed event.
1470
1471 FREE_FRAME deallocates a frame and suppresses output token.
1472 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles.
1473 """
1474 env = simpy.Environment()
1475 events = []
1476 config = PEConfig(frame_count=4, on_event=events.append)
1477 pe = ProcessingElement(
1478 env=env,
1479 pe_id=0,
1480 config=config,
1481 )
1482
1483 # Allocate frame
1484 fct = FrameControlToken(target=0, act_id=10, op=FrameOp.ALLOC, payload=0)
1485 inject_and_run(env, pe, fct)
1486 frame_id, _lane = pe.tag_store[10]
1487
1488 # Set up FREE_FRAME instruction
1489 inst = Instruction(
1490 opcode=RoutingOp.FREE_FRAME,
1491 output=OutputStyle.INHERIT,
1492 has_const=False,
1493 dest_count=0,
1494 wide=False,
1495 fref=0,
1496 )
1497 pe.iram[400] = inst
1498
1499 # Record time before injecting
1500 injection_time = env.now
1501
1502 # Inject monadic token to FREE_FRAME
1503 tok = MonadToken(
1504 target=0,
1505 offset=400,
1506 act_id=10,
1507 data=0,
1508 inline=False,
1509 )
1510 def _put():
1511 yield pe.input_store.put(tok)
1512 env.process(_put())
1513 env.run()
1514
1515 # Find the FrameFreed event
1516 frame_freed_time = None
1517 for e in events:
1518 if isinstance(e, FrameFreed):
1519 frame_freed_time = e.time
1520 break
1521
1522 # Verify timing: 4 cycles from injection to FrameFreed event
1523 assert frame_freed_time is not None, "Should have FrameFreed event"
1524 delta = frame_freed_time - injection_time
1525 assert delta == 4, f"Expected 4 cycles for FREE_FRAME, got {delta}"
1526 # Also verify frame was actually freed
1527 assert 10 not in pe.tag_store, "Frame should be freed from tag_store"
1528 assert frame_id in pe.free_frames, "Frame should be returned to free_frames"
1529
1530 def test_alloc_remote_timing(self):
1531 """Verify ALLOC_REMOTE timing: 4 cycles from injection to delivery.
1532
1533 ALLOC_REMOTE constructs a FrameControlToken and routes it to target PE.
1534 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles.
1535 The delivery process (_deliver) adds 1 more cycle after EMIT completes.
1536 """
1537 env = simpy.Environment()
1538 events = []
1539 config = PEConfig(frame_count=4, on_event=events.append)
1540 pe = ProcessingElement(
1541 env=env,
1542 pe_id=0,
1543 config=config,
1544 )
1545
1546 # Allocate frame
1547 fct = FrameControlToken(target=0, act_id=12, op=FrameOp.ALLOC, payload=0)
1548 inject_and_run(env, pe, fct)
1549 frame_id, _lane = pe.tag_store[12]
1550
1551 # Set up ALLOC_REMOTE instruction
1552 inst = Instruction(
1553 opcode=RoutingOp.ALLOC_REMOTE,
1554 output=OutputStyle.INHERIT,
1555 has_const=False,
1556 dest_count=0,
1557 wide=False,
1558 fref=0,
1559 )
1560 pe.iram[500] = inst
1561
1562 # Set target PE and target act_id in frame slots
1563 pe.frames[frame_id][0] = 1 # target PE 1
1564 pe.frames[frame_id][1] = 20 # target act_id 20
1565
1566 # Wire route table for target PE
1567 target_store = simpy.Store(env)
1568 pe.route_table[1] = target_store
1569
1570 # Record time before injecting
1571 injection_time = env.now
1572
1573 # Inject monadic token to ALLOC_REMOTE
1574 tok = MonadToken(
1575 target=0,
1576 offset=500,
1577 act_id=12,
1578 data=0,
1579 inline=False,
1580 )
1581 def _put():
1582 yield pe.input_store.put(tok)
1583 env.process(_put())
1584 env.run()
1585
1586 # The Executed event fires right before the EXECUTE yield, at time: dequeue(1) + IFETCH(1) = 2
1587 # The EMIT cycle completes at inject + 4
1588 # The _deliver process then yields 1 more cycle before putting to target_store
1589 # So FrameControlToken arrives at target_store at inject + 5 cycles total
1590
1591 # For this test, we verify the Executed event timing
1592 executed_time = None
1593 for e in events:
1594 if isinstance(e, Executed) and e.op == RoutingOp.ALLOC_REMOTE:
1595 executed_time = e.time
1596 break
1597
1598 assert executed_time is not None, "Should have Executed event for ALLOC_REMOTE"
1599 # Executed fires after IFETCH (dequeue 1 + IFETCH 1 = 2)
1600 delta = executed_time - injection_time
1601 assert delta == 2, f"Expected Executed at 2 cycles, got {delta}"
1602
1603
1604class TestAC2_4_NoBitMaskingOnInstructions:
1605 """AC2.4: Verify PE pipeline methods don't do raw bit masking/shifting on instruction fields.
1606
1607 Instead, all field access must use encoding.pack_flit1/unpack_flit1.
1608 pack_flit1/unpack_flit1 should only appear in CHANGE_TAG, EXTRACT_TAG, and local write handlers.
1609 """
1610
1611 def test_process_token_no_raw_bit_masking(self):
1612 """_process_token should not contain raw bit shifts/masks on instruction fields."""
1613 import inspect
1614 source = inspect.getsource(ProcessingElement._process_token)
1615
1616 # Check for common raw bit masking patterns on "inst"
1617 # These patterns should not appear (excepting in pack/unpack function calls)
1618 disallowed_patterns = [
1619 "inst.opcode & ", # Raw masking on opcode
1620 "inst.opcode >> ", # Raw shifting on opcode
1621 "inst.fref & ", # Raw masking on fref
1622 "inst.fref >> ", # Raw shifting on fref
1623 "inst.dest_count & ", # Raw masking on dest_count
1624 "inst.dest_count >> ", # Raw shifting on dest_count
1625 ]
1626
1627 for pattern in disallowed_patterns:
1628 assert pattern not in source, \
1629 f"_process_token contains disallowed pattern: {pattern}"
1630
1631 def test_match_frame_no_raw_bit_masking(self):
1632 """_match_frame should not contain raw bit masks/shifts on instruction fields."""
1633 import inspect
1634 source = inspect.getsource(ProcessingElement._match_frame)
1635
1636 disallowed_patterns = [
1637 "inst.opcode & ",
1638 "inst.opcode >> ",
1639 "inst.fref & ",
1640 "inst.fref >> ",
1641 ]
1642
1643 for pattern in disallowed_patterns:
1644 assert pattern not in source, \
1645 f"_match_frame contains disallowed pattern: {pattern}"
1646
1647 def test_do_emit_new_no_raw_bit_masking(self):
1648 """_do_emit_new should not contain raw bit masks/shifts on instruction fields."""
1649 import inspect
1650 source = inspect.getsource(ProcessingElement._do_emit_new)
1651
1652 disallowed_patterns = [
1653 "inst.opcode & ",
1654 "inst.opcode >> ",
1655 "inst.fref & ",
1656 "inst.fref >> ",
1657 ]
1658
1659 for pattern in disallowed_patterns:
1660 assert pattern not in source, \
1661 f"_do_emit_new contains disallowed pattern: {pattern}"
1662
1663 def test_pack_unpack_flit1_only_in_correct_handlers(self):
1664 """pack_flit1/unpack_flit1 should only appear in specific contexts.
1665
1666 - pack_flit1 should appear in:
1667 * _emit_change_tag (CHANGE_TAG output mode handler)
1668 * _process_token (for EXTRACT_TAG inline handling)
1669 - unpack_flit1 should appear in:
1670 * _handle_local_write (frame destination decoding)
1671 """
1672 import inspect
1673
1674 # pack_flit1 should be in these methods
1675 pack_flit1_methods = ['_emit_change_tag', '_process_token']
1676 for method_name in pack_flit1_methods:
1677 method = getattr(ProcessingElement, method_name)
1678 source = inspect.getsource(method)
1679 # pack_flit1 should be called in these methods
1680 assert 'pack_flit1(' in source, \
1681 f"{method_name} should call pack_flit1 but doesn't"
1682
1683 # unpack_flit1 should be in this method
1684 method = getattr(ProcessingElement, '_handle_local_write')
1685 source = inspect.getsource(method)
1686 assert 'unpack_flit1(' in source, \
1687 "_handle_local_write should call unpack_flit1"
1688
1689 # Methods that SHOULD NOT have pack/unpack_flit1 calls
1690 # (they use FrameDest objects directly instead of packing/unpacking)
1691 disallowed_methods = [
1692 '_match_frame',
1693 '_do_emit_new',
1694 '_emit_sink',
1695 '_emit_inherit',
1696 ]
1697
1698 for method_name in disallowed_methods:
1699 method = getattr(ProcessingElement, method_name)
1700 source = inspect.getsource(method)
1701 # These should not contain pack_flit1 or unpack_flit1 calls
1702 assert 'pack_flit1(' not in source, \
1703 f"{method_name} should not directly call pack_flit1"
1704 assert 'unpack_flit1(' not in source, \
1705 f"{method_name} should not directly call unpack_flit1"