OR-1 dataflow CPU sketch
1"""
2Tests for ProcessingElement with frame-based redesign.
3
4Verifies:
5- AC3.1: Frame configuration (frame_count, frame_slots, matchable_offsets)
6- AC3.2: Pipeline order for dyadic and monadic tokens
7- AC3.3: Dyadic matching using tag_store and presence bits
8- AC3.4: INHERIT output mode reads FrameDest from frame
9- AC3.5: CHANGE_TAG mode extracts destination from left operand
10- AC3.6: SINK mode writes result to frame slot
11- AC3.8: Frame allocation/deallocation via FrameControlToken
12- AC1.1-AC1.9: Original AC tests adapted to new model
13"""
14
15import pytest
16import simpy
17from hypothesis import given
18
19from cm_inst import (
20 ArithOp, FrameDest, FrameOp, Instruction, LogicOp, MemOp,
21 OutputStyle, Port, RoutingOp, TokenKind,
22)
23from emu.pe import ProcessingElement
24from emu.types import PEConfig
25from tests.conftest import dyad_token
26from tokens import DyadToken, MonadToken
27
28
29def inject_and_run(env, pe, token):
30 """Helper: inject token and run simulation."""
31 def _put():
32 yield pe.input_store.put(token)
33 env.process(_put())
34 env.run()
35
36
37def inject_two_and_run(env, pe, token1, token2):
38 """Helper: inject two tokens and run simulation."""
39 def _put():
40 yield pe.input_store.put(token1)
41 yield pe.input_store.put(token2)
42 env.process(_put())
43 env.run()
44
45
46class TestFrameConfiguration:
47 """AC3.1: Frame configuration is applied correctly."""
48
49 def test_default_frame_config(self):
50 """PE accepts default frame configuration."""
51 env = simpy.Environment()
52 config = PEConfig()
53 pe = ProcessingElement(env=env, pe_id=0, config=config)
54
55 assert pe.frame_count > 0
56 assert pe.frame_slots > 0
57 assert pe.matchable_offsets > 0
58
59 def test_custom_frame_config(self):
60 """PE accepts custom frame configuration."""
61 env = simpy.Environment()
62 config = PEConfig(
63 frame_count=4,
64 frame_slots=32,
65 matchable_offsets=4,
66 )
67 pe = ProcessingElement(env=env, pe_id=0, config=config)
68
69 assert pe.frame_count == 4
70 assert pe.frame_slots == 32
71 assert pe.matchable_offsets == 4
72
73
74class TestMonadicBypass:
75 """AC1.1: Monadic token bypasses matching store."""
76
77 def test_monad_immediate_execution(self):
78 """AC1.1: Monadic token executes immediately without matching."""
79 env = simpy.Environment()
80
81 # PASS instruction (monadic safe, no operands needed)
82 pass_inst = Instruction(
83 opcode=RoutingOp.PASS,
84 output=OutputStyle.INHERIT,
85 has_const=False,
86 dest_count=1,
87 wide=False,
88 fref=8,
89 )
90
91 # Set up destination in frame slot
92 dest = FrameDest(
93 target_pe=1, offset=0, act_id=0,
94 port=Port.L, token_kind=TokenKind.MONADIC,
95 )
96
97 config = PEConfig(
98 pe_id=0,
99 iram={0: pass_inst},
100 initial_frames={0: {8: dest}},
101 initial_tag_store={0: (0, 0)},
102 )
103
104 pe = ProcessingElement(env=env, pe_id=0, config=config)
105 output_store = simpy.Store(env, capacity=10)
106 pe.route_table[1] = output_store
107
108 # Inject monadic token
109 token = MonadToken(target=0, offset=0, act_id=0, data=0x1234, inline=False)
110 inject_and_run(env, pe, token)
111
112 # Should produce one output
113 assert len(output_store.items) == 1
114 out = output_store.items[0]
115 assert isinstance(out, MonadToken)
116 assert out.data == 0x1234
117
118
119class TestDyadicMatching:
120 """AC1.2, AC1.3: Dyadic token matching store behavior."""
121
122 def test_first_dyadic_no_fire(self):
123 """AC1.2: First dyadic token stores in matching, no output."""
124 env = simpy.Environment()
125
126 add_inst = Instruction(
127 opcode=ArithOp.ADD,
128 output=OutputStyle.INHERIT,
129 has_const=False,
130 dest_count=1,
131 wide=False,
132 fref=8,
133 )
134
135 dest = FrameDest(
136 target_pe=1, offset=0, act_id=0,
137 port=Port.L, token_kind=TokenKind.DYADIC,
138 )
139
140 config = PEConfig(
141 pe_id=0,
142 iram={0: add_inst},
143 initial_frames={0: {8: dest}},
144 initial_tag_store={0: (0, 0)},
145 )
146
147 pe = ProcessingElement(env=env, pe_id=0, config=config)
148 output_store = simpy.Store(env, capacity=10)
149 pe.route_table[1] = output_store
150
151 # First dyadic token
152 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1111, port=Port.L)
153
154 inject_and_run(env, pe, token_l)
155
156 # No output from first token
157 assert len(output_store.items) == 0
158 # Matching store should have the operand
159 frame_id, _lane = pe.tag_store[0]
160 assert pe.presence[frame_id][0][0] is True
161
162 def test_second_dyadic_fires_left_first(self):
163 """AC1.3: Second dyadic token fires when partner found (L then R)."""
164 env = simpy.Environment()
165
166 add_inst = Instruction(
167 opcode=ArithOp.ADD,
168 output=OutputStyle.INHERIT,
169 has_const=False,
170 dest_count=1,
171 wide=False,
172 fref=8,
173 )
174
175 dest = FrameDest(
176 target_pe=1, offset=0, act_id=0,
177 port=Port.L, token_kind=TokenKind.DYADIC,
178 )
179
180 config = PEConfig(
181 pe_id=0,
182 iram={0: add_inst},
183 initial_frames={0: {8: dest}},
184 initial_tag_store={0: (0, 0)},
185 )
186
187 pe = ProcessingElement(env=env, pe_id=0, config=config)
188 output_store = simpy.Store(env, capacity=10)
189 pe.route_table[1] = output_store
190
191 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1111, port=Port.L)
192 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x2222, port=Port.R)
193
194 inject_two_and_run(env, pe, token_l, token_r)
195
196 # Should produce one output: 0x1111 + 0x2222 = 0x3333
197 assert len(output_store.items) == 1
198 assert output_store.items[0].data == 0x3333
199
200 def test_second_dyadic_fires_right_first(self):
201 """AC1.3: Second dyadic fires, operands ordered by port (R then L)."""
202 env = simpy.Environment()
203
204 add_inst = Instruction(
205 opcode=ArithOp.ADD,
206 output=OutputStyle.INHERIT,
207 has_const=False,
208 dest_count=1,
209 wide=False,
210 fref=8,
211 )
212
213 dest = FrameDest(
214 target_pe=1, offset=0, act_id=0,
215 port=Port.L, token_kind=TokenKind.DYADIC,
216 )
217
218 config = PEConfig(
219 pe_id=0,
220 iram={0: add_inst},
221 initial_frames={0: {8: dest}},
222 initial_tag_store={0: (0, 0)},
223 )
224
225 pe = ProcessingElement(env=env, pe_id=0, config=config)
226 output_store = simpy.Store(env, capacity=10)
227 pe.route_table[1] = output_store
228
229 # Inject R then L (reversed order)
230 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x2222, port=Port.R)
231 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1111, port=Port.L)
232
233 inject_two_and_run(env, pe, token_r, token_l)
234
235 # Should still compute correctly: ADD(0x1111, 0x2222) = 0x3333
236 assert len(output_store.items) == 1
237 assert output_store.items[0].data == 0x3333
238
239
240class TestOutputFormatterSingleMode:
241 """AC1.5: SINGLE mode emits one token to dest_l."""
242
243 def test_single_mode_one_output(self):
244 """AC1.5: SINGLE mode emits exactly one token."""
245 env = simpy.Environment()
246
247 # ADD with only dest_l (SINGLE mode)
248 add_inst = Instruction(
249 opcode=ArithOp.ADD,
250 output=OutputStyle.INHERIT,
251 has_const=False,
252 dest_count=1,
253 wide=False,
254 fref=8,
255 )
256
257 dest = FrameDest(
258 target_pe=2, offset=1, act_id=0,
259 port=Port.L, token_kind=TokenKind.DYADIC,
260 )
261
262 config = PEConfig(
263 pe_id=0,
264 iram={0: add_inst},
265 initial_frames={0: {8: dest}},
266 initial_tag_store={0: (0, 0)},
267 )
268
269 pe = ProcessingElement(env=env, pe_id=0, config=config)
270 output_store = simpy.Store(env, capacity=10)
271 pe.route_table[2] = output_store
272
273 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x0005, port=Port.L)
274 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0003, port=Port.R)
275
276 inject_two_and_run(env, pe, token_l, token_r)
277
278 # Exactly one output
279 assert len(output_store.items) == 1
280 assert output_store.items[0].data == 0x0008 # 5 + 3
281
282
283class TestOutputFormatterDualMode:
284 """AC1.6: DUAL mode emits two tokens with same data."""
285
286 def test_dual_mode_two_outputs(self):
287 """AC1.6: DUAL mode emits two tokens with same data."""
288 env = simpy.Environment()
289
290 # ADD with both dest_l and dest_r (DUAL mode)
291 add_inst = Instruction(
292 opcode=ArithOp.ADD,
293 output=OutputStyle.INHERIT,
294 has_const=False,
295 dest_count=2,
296 wide=False,
297 fref=8,
298 )
299
300 dest_l = FrameDest(
301 target_pe=2, offset=1, act_id=0,
302 port=Port.L, token_kind=TokenKind.DYADIC,
303 )
304 dest_r = FrameDest(
305 target_pe=3, offset=2, act_id=0,
306 port=Port.L, token_kind=TokenKind.DYADIC,
307 )
308
309 config = PEConfig(
310 pe_id=0,
311 iram={0: add_inst},
312 initial_frames={0: {8: dest_l, 9: dest_r}},
313 initial_tag_store={0: (0, 0)},
314 )
315
316 pe = ProcessingElement(env=env, pe_id=0, config=config)
317 output_l = simpy.Store(env, capacity=10)
318 output_r = simpy.Store(env, capacity=10)
319 pe.route_table[2] = output_l
320 pe.route_table[3] = output_r
321
322 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x0010, port=Port.L)
323 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0020, port=Port.R)
324
325 inject_two_and_run(env, pe, token_l, token_r)
326
327 # Two outputs with same data
328 assert len(output_l.items) == 1
329 assert len(output_r.items) == 1
330 assert output_l.items[0].data == 0x0030
331 assert output_r.items[0].data == 0x0030
332
333
334class TestOutputFormatterSwitchMode:
335 """AC1.7: SWITCH mode routes data and trigger separately."""
336
337 def test_switch_mode_true_condition(self):
338 """AC1.7: SWITCH with true condition sends data to dest_l, trigger to dest_r."""
339 env = simpy.Environment()
340
341 # SWEQ with both dests
342 sweq_inst = Instruction(
343 opcode=RoutingOp.SWEQ,
344 output=OutputStyle.INHERIT,
345 has_const=False,
346 dest_count=2,
347 wide=False,
348 fref=8,
349 )
350
351 dest_l = FrameDest(
352 target_pe=2, offset=1, act_id=0,
353 port=Port.L, token_kind=TokenKind.MONADIC,
354 )
355 dest_r = FrameDest(
356 target_pe=3, offset=2, act_id=0,
357 port=Port.L, token_kind=TokenKind.MONADIC,
358 )
359
360 config = PEConfig(
361 pe_id=0,
362 iram={0: sweq_inst},
363 initial_frames={0: {8: dest_l, 9: dest_r}},
364 initial_tag_store={0: (0, 0)},
365 )
366
367 pe = ProcessingElement(env=env, pe_id=0, config=config)
368 output_l = simpy.Store(env, capacity=10)
369 output_r = simpy.Store(env, capacity=10)
370 pe.route_table[2] = output_l
371 pe.route_table[3] = output_r
372
373 # Equal tokens: bool_out = True
374 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1234, port=Port.L)
375 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x1234, port=Port.R)
376
377 inject_two_and_run(env, pe, token_l, token_r)
378
379 # Data to dest_l, trigger to dest_r
380 assert len(output_l.items) == 1
381 assert len(output_r.items) == 1
382
383 data_token = output_l.items[0]
384 assert isinstance(data_token, MonadToken)
385 assert data_token.data == 0x1234
386
387 trigger = output_r.items[0]
388 assert isinstance(trigger, MonadToken)
389 assert trigger.data == 0 # Trigger has zero data
390
391 def test_switch_mode_false_condition(self):
392 """AC1.7: SWITCH with false condition sends data to dest_r, trigger to dest_l."""
393 env = simpy.Environment()
394
395 sweq_inst = Instruction(
396 opcode=RoutingOp.SWEQ,
397 output=OutputStyle.INHERIT,
398 has_const=False,
399 dest_count=2,
400 wide=False,
401 fref=8,
402 )
403
404 dest_l = FrameDest(
405 target_pe=2, offset=1, act_id=0,
406 port=Port.L, token_kind=TokenKind.MONADIC,
407 )
408 dest_r = FrameDest(
409 target_pe=3, offset=2, act_id=0,
410 port=Port.L, token_kind=TokenKind.MONADIC,
411 )
412
413 config = PEConfig(
414 pe_id=0,
415 iram={0: sweq_inst},
416 initial_frames={0: {8: dest_l, 9: dest_r}},
417 initial_tag_store={0: (0, 0)},
418 )
419
420 pe = ProcessingElement(env=env, pe_id=0, config=config)
421 output_l = simpy.Store(env, capacity=10)
422 output_r = simpy.Store(env, capacity=10)
423 pe.route_table[2] = output_l
424 pe.route_table[3] = output_r
425
426 # Unequal tokens: bool_out = False
427 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1234, port=Port.L)
428 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x5678, port=Port.R)
429
430 inject_two_and_run(env, pe, token_l, token_r)
431
432 # Data to dest_r, trigger to dest_l
433 assert len(output_l.items) == 1
434 assert len(output_r.items) == 1
435
436 trigger = output_l.items[0]
437 assert isinstance(trigger, MonadToken)
438 assert trigger.data == 0 # Trigger has zero data
439
440 data_token = output_r.items[0]
441 assert isinstance(data_token, MonadToken)
442 assert data_token.data == 0x1234
443
444
445class TestOutputFormatterSuppressMode:
446 """AC1.8: SUPPRESS mode emits no tokens."""
447
448 def test_suppress_free_frame_instruction(self):
449 """AC1.8: FREE_FRAME instruction suppresses output."""
450 env = simpy.Environment()
451
452 # FREE_FRAME (monadic, SUPPRESS)
453 free_inst = Instruction(
454 opcode=RoutingOp.FREE_FRAME,
455 output=OutputStyle.SINK,
456 has_const=False,
457 dest_count=0,
458 wide=False,
459 fref=8,
460 )
461
462 config = PEConfig(
463 pe_id=0,
464 iram={0: free_inst},
465 initial_tag_store={0: (0, 0)},
466 )
467
468 pe = ProcessingElement(env=env, pe_id=0, config=config)
469 output_store = simpy.Store(env, capacity=10)
470 pe.route_table[1] = output_store
471
472 token = MonadToken(target=0, offset=0, act_id=0, data=0x4567, inline=False)
473
474 inject_and_run(env, pe, token)
475
476 # No output
477 assert len(output_store.items) == 0
478
479 def test_suppress_gate_false(self):
480 """AC1.8: GATE with false condition suppresses output."""
481 env = simpy.Environment()
482
483 gate_inst = Instruction(
484 opcode=RoutingOp.GATE,
485 output=OutputStyle.INHERIT,
486 has_const=False,
487 dest_count=1,
488 wide=False,
489 fref=8,
490 )
491
492 dest = FrameDest(
493 target_pe=1, offset=0, act_id=0,
494 port=Port.L, token_kind=TokenKind.DYADIC,
495 )
496
497 config = PEConfig(
498 pe_id=0,
499 iram={0: gate_inst},
500 initial_frames={0: {8: dest}},
501 initial_tag_store={0: (0, 0)},
502 )
503
504 pe = ProcessingElement(env=env, pe_id=0, config=config)
505 output_store = simpy.Store(env, capacity=10)
506 pe.route_table[1] = output_store
507
508 # L=42, R=0 (false)
509 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x002A, port=Port.L)
510 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0000, port=Port.R)
511
512 inject_two_and_run(env, pe, token_l, token_r)
513
514 # No output (suppressed by false condition)
515 assert len(output_store.items) == 0
516
517 def test_gate_true_passes(self):
518 """AC1.8: GATE with true condition passes output."""
519 env = simpy.Environment()
520
521 gate_inst = Instruction(
522 opcode=RoutingOp.GATE,
523 output=OutputStyle.INHERIT,
524 has_const=False,
525 dest_count=1,
526 wide=False,
527 fref=8,
528 )
529
530 dest = FrameDest(
531 target_pe=1, offset=0, act_id=0,
532 port=Port.L, token_kind=TokenKind.DYADIC,
533 )
534
535 config = PEConfig(
536 pe_id=0,
537 iram={0: gate_inst},
538 initial_frames={0: {8: dest}},
539 initial_tag_store={0: (0, 0)},
540 )
541
542 pe = ProcessingElement(env=env, pe_id=0, config=config)
543 output_store = simpy.Store(env, capacity=10)
544 pe.route_table[1] = output_store
545
546 # L=42, R=1 (true)
547 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x002A, port=Port.L)
548 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0001, port=Port.R)
549
550 inject_two_and_run(env, pe, token_l, token_r)
551
552 # One output (gate opened)
553 assert len(output_store.items) == 1
554 assert output_store.items[0].data == 0x002A
555
556
557class TestNonExistentOffset:
558 """AC1.9: Non-existent IRAM offset doesn't crash."""
559
560 def test_missing_iram_offset_no_crash(self):
561 """AC1.9: Token targeting non-existent offset doesn't crash."""
562 env = simpy.Environment()
563
564 config = PEConfig(pe_id=0, iram={})
565
566 pe = ProcessingElement(env=env, pe_id=0, config=config)
567 output_store = simpy.Store(env, capacity=10)
568 pe.route_table[1] = output_store
569
570 token = MonadToken(target=0, offset=99, act_id=0, data=0xDEAD, inline=False)
571
572 inject_and_run(env, pe, token)
573
574 # Should not crash, no output (instruction doesn't exist)
575 assert len(output_store.items) == 0
576
577
578class TestMatchingStoreCleared:
579 """Matching store is cleared after firing."""
580
581 @given(dyad_token(target=0, offset=5, act_id=1))
582 def test_matching_store_cleared_after_firing(self, token_l: DyadToken):
583 """After token pair fires, matching store slot is reset."""
584 env = simpy.Environment()
585
586 add_inst = Instruction(
587 opcode=ArithOp.ADD,
588 output=OutputStyle.INHERIT,
589 has_const=False,
590 dest_count=1,
591 wide=False,
592 fref=8,
593 )
594
595 dest = FrameDest(
596 target_pe=1, offset=5, act_id=1,
597 port=Port.L, token_kind=TokenKind.DYADIC,
598 )
599
600 config = PEConfig(
601 pe_id=0,
602 iram={5: add_inst},
603 initial_frames={0: {8: dest}},
604 initial_tag_store={1: (0, 0)},
605 )
606
607 pe = ProcessingElement(env=env, pe_id=0, config=config)
608 output_store = simpy.Store(env, capacity=10)
609 pe.route_table[1] = output_store
610
611 # Create matching right token
612 token_r = DyadToken(
613 target=0,
614 offset=token_l.offset,
615 act_id=token_l.act_id,
616 data=0x5555,
617 port=Port.R,
618 )
619
620 inject_two_and_run(env, pe, token_l, token_r)
621
622 # After firing, matching store should be clear
623 frame_id, _lane = pe.tag_store[token_l.act_id]
624 assert pe.presence[frame_id][token_l.offset % pe.matchable_offsets][0] is False
625
626
627class TestOutputTokenCountMatchesMode:
628 """Output token count matches output mode."""
629
630 @given(dyad_token(target=0, offset=0, act_id=0))
631 def test_suppress_mode_produces_zero_tokens(self, token_l: DyadToken):
632 """SUPPRESS mode produces zero output tokens."""
633 env = simpy.Environment()
634
635 free_inst = Instruction(
636 opcode=RoutingOp.FREE_FRAME,
637 output=OutputStyle.SINK,
638 has_const=False,
639 dest_count=0,
640 wide=False,
641 fref=8,
642 )
643
644 config = PEConfig(
645 pe_id=0,
646 iram={0: free_inst},
647 initial_tag_store={0: (0, 0)},
648 )
649
650 pe = ProcessingElement(env=env, pe_id=0, config=config)
651 output_store = simpy.Store(env, capacity=10)
652 pe.route_table[1] = output_store
653
654 token_r = DyadToken(
655 target=0,
656 offset=token_l.offset,
657 act_id=token_l.act_id,
658 data=0x2222,
659 port=Port.R,
660 )
661
662 inject_two_and_run(env, pe, token_l, token_r)
663
664 # SUPPRESS mode produces zero outputs
665 assert len(output_store.items) == 0
666
667 @given(dyad_token(target=0, offset=0, act_id=0))
668 def test_single_mode_produces_one_token(self, token_l: DyadToken):
669 """SINGLE mode produces one output token."""
670 env = simpy.Environment()
671
672 add_inst = Instruction(
673 opcode=ArithOp.ADD,
674 output=OutputStyle.INHERIT,
675 has_const=False,
676 dest_count=1,
677 wide=False,
678 fref=8,
679 )
680
681 dest = FrameDest(
682 target_pe=1, offset=0, act_id=0,
683 port=Port.L, token_kind=TokenKind.DYADIC,
684 )
685
686 config = PEConfig(
687 pe_id=0,
688 iram={0: add_inst},
689 initial_frames={0: {8: dest}},
690 initial_tag_store={0: (0, 0)},
691 )
692
693 pe = ProcessingElement(env=env, pe_id=0, config=config)
694 output_store = simpy.Store(env, capacity=10)
695 pe.route_table[1] = output_store
696
697 token_r = DyadToken(
698 target=0,
699 offset=token_l.offset,
700 act_id=token_l.act_id,
701 data=0x2222,
702 port=Port.R,
703 )
704
705 inject_two_and_run(env, pe, token_l, token_r)
706
707 # SINGLE mode produces exactly one output
708 assert len(output_store.items) == 1
709
710 @given(dyad_token(target=0, offset=0, act_id=0))
711 def test_dual_mode_produces_two_tokens(self, token_l: DyadToken):
712 """DUAL mode produces two output tokens (one per destination)."""
713 env = simpy.Environment()
714
715 add_inst = Instruction(
716 opcode=ArithOp.ADD,
717 output=OutputStyle.INHERIT,
718 has_const=False,
719 dest_count=2,
720 wide=False,
721 fref=8,
722 )
723
724 dest_l = FrameDest(
725 target_pe=1, offset=0, act_id=0,
726 port=Port.L, token_kind=TokenKind.DYADIC,
727 )
728 dest_r = FrameDest(
729 target_pe=2, offset=1, act_id=0,
730 port=Port.L, token_kind=TokenKind.DYADIC,
731 )
732
733 config = PEConfig(
734 pe_id=0,
735 iram={0: add_inst},
736 initial_frames={0: {8: dest_l, 9: dest_r}},
737 initial_tag_store={0: (0, 0)},
738 )
739
740 pe = ProcessingElement(env=env, pe_id=0, config=config)
741 output_store_l = simpy.Store(env, capacity=10)
742 output_store_r = simpy.Store(env, capacity=10)
743 pe.route_table[1] = output_store_l
744 pe.route_table[2] = output_store_r
745
746 token_r = DyadToken(
747 target=0,
748 offset=token_l.offset,
749 act_id=token_l.act_id,
750 data=0x2222,
751 port=Port.R,
752 )
753
754 inject_two_and_run(env, pe, token_l, token_r)
755
756 # DUAL mode produces two outputs
757 assert len(output_store_l.items) == 1
758 assert len(output_store_r.items) == 1
759
760 @given(dyad_token(target=0, offset=0, act_id=0))
761 def test_switch_mode_produces_two_tokens(self, token_l: DyadToken):
762 """SWITCH mode produces two output tokens (data + trigger)."""
763 env = simpy.Environment()
764
765 sweq_inst = Instruction(
766 opcode=RoutingOp.SWEQ,
767 output=OutputStyle.INHERIT,
768 has_const=False,
769 dest_count=2,
770 wide=False,
771 fref=8,
772 )
773
774 dest_l = FrameDest(
775 target_pe=1, offset=0, act_id=0,
776 port=Port.L, token_kind=TokenKind.DYADIC,
777 )
778 dest_r = FrameDest(
779 target_pe=2, offset=1, act_id=0,
780 port=Port.L, token_kind=TokenKind.DYADIC,
781 )
782
783 config = PEConfig(
784 pe_id=0,
785 iram={0: sweq_inst},
786 initial_frames={0: {8: dest_l, 9: dest_r}},
787 initial_tag_store={0: (0, 0)},
788 )
789
790 pe = ProcessingElement(env=env, pe_id=0, config=config)
791 output_store_l = simpy.Store(env, capacity=10)
792 output_store_r = simpy.Store(env, capacity=10)
793 pe.route_table[1] = output_store_l
794 pe.route_table[2] = output_store_r
795
796 # Use same value to trigger bool_out=True
797 token_r = DyadToken(
798 target=0,
799 offset=token_l.offset,
800 act_id=token_l.act_id,
801 data=token_l.data,
802 port=Port.R,
803 )
804
805 inject_two_and_run(env, pe, token_l, token_r)
806
807 # SWITCH mode produces two outputs
808 assert len(output_store_l.items) == 1
809 assert len(output_store_r.items) == 1
810
811
812class TestStaleTokensProduceNoOutput:
813 """Stale tokens (act_id mismatch) produce no output."""
814
815 @given(dyad_token(target=0, offset=0, act_id=0))
816 def test_stale_token_no_output(self, token_l: DyadToken):
817 """Token with invalid act_id is rejected, produces no output."""
818 env = simpy.Environment()
819
820 add_inst = Instruction(
821 opcode=ArithOp.ADD,
822 output=OutputStyle.INHERIT,
823 has_const=False,
824 dest_count=1,
825 wide=False,
826 fref=8,
827 )
828
829 dest = FrameDest(
830 target_pe=1, offset=0, act_id=0,
831 port=Port.L, token_kind=TokenKind.DYADIC,
832 )
833
834 config = PEConfig(
835 pe_id=0,
836 iram={0: add_inst},
837 initial_frames={0: {8: dest}},
838 initial_tag_store={0: (0, 0)},
839 # Note: only act_id 0 is allocated; other act_ids will be invalid
840 )
841
842 pe = ProcessingElement(env=env, pe_id=0, config=config)
843 output_store = simpy.Store(env, capacity=10)
844 pe.route_table[1] = output_store
845
846 # Use act_id that was NOT allocated
847 invalid_act_id = 3
848 token = DyadToken(
849 target=0,
850 offset=0,
851 act_id=invalid_act_id,
852 data=0x1234,
853 port=Port.L,
854 )
855
856 inject_and_run(env, pe, token)
857
858 # Invalid act_id produces no output
859 assert len(output_store.items) == 0
860
861
862class TestBoundaryEdgeCases:
863 """Boundary edge cases."""
864
865 def test_iram_write_multiple_instructions(self):
866 """Multiple instructions can be loaded and executed."""
867 env = simpy.Environment()
868
869 add_inst = Instruction(
870 opcode=ArithOp.ADD,
871 output=OutputStyle.INHERIT,
872 has_const=False,
873 dest_count=1,
874 wide=False,
875 fref=8,
876 )
877
878 inc_inst = Instruction(
879 opcode=ArithOp.INC,
880 output=OutputStyle.INHERIT,
881 has_const=False,
882 dest_count=1,
883 wide=False,
884 fref=8,
885 )
886
887 dest = FrameDest(
888 target_pe=1, offset=0, act_id=0,
889 port=Port.L, token_kind=TokenKind.MONADIC,
890 )
891
892 config = PEConfig(
893 pe_id=0,
894 iram={0: add_inst, 1: inc_inst},
895 initial_frames={0: {8: dest}},
896 initial_tag_store={0: (0, 0)},
897 )
898
899 pe = ProcessingElement(env=env, pe_id=0, config=config)
900 output_store = simpy.Store(env, capacity=10)
901 pe.route_table[1] = output_store
902
903 # Execute ADD at offset 0
904 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x10, port=Port.L)
905 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x20, port=Port.R)
906
907 inject_two_and_run(env, pe, token_l, token_r)
908
909 # Execute INC at offset 1
910 token_inc = MonadToken(target=0, offset=1, act_id=0, data=0x10, inline=False)
911
912 inject_and_run(env, pe, token_inc)
913
914 # Both instructions executed
915 assert len(output_store.items) == 2
916 assert output_store.items[0].data == 0x30 # ADD result
917 assert output_store.items[1].data == 0x11 # INC result